import numpy as np from itertools import combinations from methods import SubsamplingMethod import sys sys.path.append("C://Users//xbrjos//Desktop//Python") from gepard import dataset import helpers class BoxSelectionSubsamplingMethod(SubsamplingMethod): def __init__(self, *args): super(BoxSelectionSubsamplingMethod, self).__init__(*args) self.filterDiameter: float = 500 self.offset: tuple = (0, 0) @property def label(self) -> str: raise NotImplementedError @property def filterArea(self) -> float: return np.pi * (self.filterDiameter / 2) ** 2 def apply_subsampling_method(self) -> tuple: subParticles: list = [] topLefts: list = self.get_topLeft_of_boxes() boxSize = self.boxSize for particle in self.particleContainer.particles: for topLeft in topLefts: if box_overlaps_contour(topLeft, (boxSize, boxSize), particle.contour, self.offset): subParticles.append(particle) return self.fraction, subParticles def get_topLeft_of_boxes(self) -> list: raise NotImplementedError class BoxSelectionCreator(object): def __init__(self, dataset: dataset.DataSet): super(BoxSelectionCreator, self).__init__() self.dataset: dataset.DataSet = dataset self.minNumberOfBoxes: int = 10 self.maxNumberOfBoxes: int = 20 def get_crossBoxSelectors_for_fraction(self, desiredFraction: float) -> list: """ Creates CrossBoxSelectors that fullfill the desired fraction criterium. :param desiredFraction: :return list of CrossBoxSelectors: """ crossBoxSelectors = [] offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset) diameter = helpers.convert_length_to_pixels(self.dataset, diameter) offset = helpers.convert_length_to_pixels(self.dataset, offset[0]), \ helpers.convert_length_to_pixels(self.dataset, offset[1]) for numBoxesAcross in [3, 5]: newBoxSelector: CrossBoxSelector = CrossBoxSelector(self.dataset.particleContainer, desiredFraction) newBoxSelector.filterDiameter = diameter newBoxSelector.offset = offset newBoxSelector.numBoxesAcross = numBoxesAcross crossBoxSelectors.append(newBoxSelector) return crossBoxSelectors def get_spiralBoxSelectors_for_fraction(self, desiredFraction: float) -> list: """ Creates CrossBoxSelectors that fullfill the desired fraction criterium. :param desiredFraction: :return list of SpiralBoxSelectors: """ spiralBoxSelectors = [] offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset) diameter = helpers.convert_length_to_pixels(self.dataset, diameter) offset = helpers.convert_length_to_pixels(self.dataset, offset[0]), \ helpers.convert_length_to_pixels(self.dataset, offset[1]) for numBoxes in SpiralSelector.possibleBoxNumbers: newBoxSelector: SpiralSelector = SpiralSelector(self.dataset.particleContainer, desiredFraction) newBoxSelector.filterDiameter = diameter newBoxSelector.offset = offset newBoxSelector.numBoxes = numBoxes if newBoxSelector.noBoxOverlap: spiralBoxSelectors.append(newBoxSelector) return spiralBoxSelectors class CrossBoxSelector(BoxSelectionSubsamplingMethod): def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None: super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction) self.numBoxesAcross: int = 3 # either 3 or 5 @property def label(self) -> str: return f'Boxes CrossLayout ({self.numBoxesAcross} boxes across)' @property def boxSize(self) -> float: maxFraction = self.get_maximum_achievable_fraction() if maxFraction < self.fraction: self.fraction = maxFraction totalBoxArea: float = self.filterArea * self.fraction boxArea: float = totalBoxArea / (2 * self.numBoxesAcross - 1) return boxArea ** 0.5 def get_topLeft_of_boxes(self) -> list: topLeftCorners: list = [] boxSize = self.boxSize xStartCoordinates: list = self._get_horizontal_box_starts(boxSize) yStartCoordinates: list = self._get_vertical_box_starts(boxSize) middleXCoordinate: float = xStartCoordinates[self.numBoxesAcross // 2] middleYCoordinate: float = yStartCoordinates[self.numBoxesAcross // 2] for i in range(self.numBoxesAcross): topLeftCorners.append((middleXCoordinate, yStartCoordinates[i])) if i != self.numBoxesAcross // 2: topLeftCorners.append((xStartCoordinates[i], middleYCoordinate)) return topLeftCorners def get_maximum_achievable_fraction(self) -> float: """ Returns the maximum achievable fraction, given the desired number of boxes across. It is with respect to a circular filter, fitting in a rectangle of dimensions width*height :return float: """ alpha: float = np.deg2rad(135) r: float = self.filterDiameter / 2 d: float = (self.numBoxesAcross - 1) * r / self.numBoxesAcross # 2/3*r for numAcross = 3, 4/5*r numAcross = 5 delta: float = np.arcsin((np.sin(alpha) * d) / r) gamma: float = np.pi - alpha - delta longestBoxHalfDiagonal: float = r / np.sin(alpha) * np.sin(gamma) maxBoxSize: float = 2 * longestBoxHalfDiagonal / np.sqrt(2) numBoxes: int = 2 * self.numBoxesAcross - 1 totalBoxArea: float = numBoxes * (maxBoxSize ** 2) maxFraction: float = totalBoxArea / self.filterArea return maxFraction def _get_horizontal_box_starts(self, boxSize: float) -> list: """ Returns a list of width-values at which the individual boxes start :param boxSize: :return list: """ return self._get_box_starts(boxSize) def _get_vertical_box_starts(self, boxSize: float) -> list: """ Returns a list of height-values at which the individual boxes start :param boxSize: :return list: """ return self._get_box_starts(boxSize) def _get_box_starts(self, boxSize: float) -> list: maxBoxSize: float = self.filterDiameter / self.numBoxesAcross assert maxBoxSize >= boxSize tileStarts: list = [] for i in range(self.numBoxesAcross): start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2 tileStarts.append(start) return tileStarts class SpiralSelector(BoxSelectionSubsamplingMethod): possibleBoxNumbers: list = [10, 15, 20] def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None: super(SpiralSelector, self).__init__(particleContainer, desiredFraction) self.numBoxes = 20 @property def label(self) -> str: return f'Boxes SpiralLayout ({self.numBoxes} boxes)' @property def noBoxOverlap(self) -> bool: return not self._boxes_are_overlapping(self.get_topLeft_of_boxes()) @property def boxSize(self) -> float: totalBoxArea: float = self.filterArea * self.fraction boxArea: float = totalBoxArea / self.numBoxes return boxArea ** 0.5 @property def spiralSlope(self) -> float: return self.armDistance / (2 * np.pi) @property def armDistance(self) -> float: return np.sqrt(2) * self.boxSize def get_topLeft_of_boxes(self) -> list: """ Calculates the topLeft-points (x, y) of all measure boxes The method uses an approximation for the spiral and is not purely accurate. :return list:""" filterCenter: tuple = self.filterDiameter / 2 - self.boxSize / 2, self.filterDiameter / 2 - self.boxSize / 2 slope = self.spiralSlope theta: float = 0 boxDistance = self.boxSize * 1.1 topLefts: list = [] for i in range(self.numBoxes): newPoint: tuple = self._get_xy_at_angle(theta, filterCenter) topLefts.append(newPoint) theta += boxDistance / (slope * np.sqrt(1 + theta ** 2)) boxDistance *= 1.05 topLefts = self._move_and_scale_toplefts(topLefts) return topLefts def _move_and_scale_toplefts(self, topLefts: list) -> list: """ The spiral approximation leads to boxes that are outside the filter size limits. This function moves and scales the topLeft-Points so that all measure boxes lie within the filter limits. :return list: """ xCoords: np.ndarray = np.array([float(point[0]) for point in topLefts]) - self.filterDiameter / 2 yCoords: np.ndarray = np.array([float(point[1]) for point in topLefts]) - self.filterDiameter / 2 xCoordsBoxMiddles: np.ndarray = xCoords + self.boxSize / 2 yCoordsBoxMiddles: np.ndarray = yCoords + self.boxSize / 2 lastBoxCenter: tuple = (xCoordsBoxMiddles[-1], yCoordsBoxMiddles[-1]) distanceLastCenter: float = np.linalg.norm(lastBoxCenter) maxDistanceInLastBox: float = self._get_max_distance_of_boxCenter_to_center(lastBoxCenter) halfBoxDistance: float = maxDistanceInLastBox - distanceLastCenter desiredDistanceTotal: float = self.filterDiameter / 2 desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance scaleFactor: float = desiredDistanceCenter / distanceLastCenter xCoordsBoxMiddles *= scaleFactor yCoordsBoxMiddles *= scaleFactor xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2 yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2 newTopLefts = zip(np.round(xCoords), np.round(yCoords)) return list(tuple(newTopLefts)) def _get_max_distance_of_boxCenter_to_center(self, boxCenter: tuple, center: tuple = (0, 0)) -> float: """ Calculates the maximal distance of a box to the given center :param topLeft: :param boxSize: :return: """ center = np.array(center) boxSize = self.boxSize coords: np.ndarray = np.array([[boxCenter[0] - 0.5 * boxSize, boxCenter[1] - 0.5 * boxSize], [boxCenter[0] + 0.5 * boxSize, boxCenter[1] - 0.5 * boxSize], [boxCenter[0] - 0.5 * boxSize, boxCenter[1] + 0.5 * boxSize], [boxCenter[0] + 0.5 * boxSize, boxCenter[1] + 0.5 * boxSize]]) distances: np.ndarray = np.linalg.norm(coords - center, axis=1) return np.max(distances) def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple: distance: float = self.spiralSlope * theta return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1] def _boxes_are_overlapping(self, topLefts: list) -> bool: """ Calculates if there is any overlap of the boxes :return: """ overlaps: bool = False for topLeft1, topLeft2 in combinations(topLefts, 2): if abs(topLeft1[0] - topLeft2[0]) < self.boxSize and abs(topLeft1[1] - topLeft2[1]) < self.boxSize: overlaps = True break return overlaps