geometricMethods.py 11.3 KB
Newer Older
1
import numpy as np
Josef Brandt's avatar
Josef Brandt committed
2
from itertools import combinations
3 4
from methods import SubsamplingMethod
import sys
Josef Brandt's avatar
Josef Brandt committed
5

6 7
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
Josef Brandt's avatar
Josef Brandt committed
8
import helpers
9 10 11 12 13 14 15 16


class BoxSelectionSubsamplingMethod(SubsamplingMethod):
    def __init__(self, *args):
        super(BoxSelectionSubsamplingMethod, self).__init__(*args)
        self.filterDiameter: float = 500
        self.offset: tuple = (0, 0)

Josef Brandt's avatar
Josef Brandt committed
17 18 19 20
    @property
    def label(self) -> str:
        raise NotImplementedError

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
    @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
Josef Brandt's avatar
Josef Brandt committed
44 45
        self.minNumberOfBoxes: int = 10
        self.maxNumberOfBoxes: int = 20
46 47 48

    def get_crossBoxSelectors_for_fraction(self, desiredFraction: float) -> list:
        """
Josef Brandt's avatar
Josef Brandt committed
49
        Creates CrossBoxSelectors that fullfill the desired fraction criterium.
50
        :param desiredFraction:
Josef Brandt's avatar
Josef Brandt committed
51
        :return list of CrossBoxSelectors:
52 53
        """
        crossBoxSelectors = []
Josef Brandt's avatar
Josef Brandt committed
54 55 56 57
        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])
58

Josef Brandt's avatar
Josef Brandt committed
59 60 61 62 63
        for numBoxesAcross in [3, 5]:
            newBoxSelector: CrossBoxSelector = CrossBoxSelector(self.dataset.particleContainer, desiredFraction)
            newBoxSelector.filterDiameter = diameter
            newBoxSelector.offset = offset
            newBoxSelector.numBoxesAcross = numBoxesAcross
64

Josef Brandt's avatar
Josef Brandt committed
65
            crossBoxSelectors.append(newBoxSelector)
66 67

        return crossBoxSelectors
68

Josef Brandt's avatar
Josef Brandt committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    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

92

Josef Brandt's avatar
Josef Brandt committed
93 94
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
95
        super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
Josef Brandt's avatar
Josef Brandt committed
96 97 98 99 100
        self.numBoxesAcross: int = 3  # either 3 or 5

    @property
    def label(self) -> str:
        return f'Boxes CrossLayout ({self.numBoxesAcross} boxes across)'
101 102 103

    @property
    def boxSize(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
104 105 106 107
        maxFraction = self.get_maximum_achievable_fraction()
        if maxFraction < self.fraction:
            self.fraction = maxFraction

Josef Brandt's avatar
Josef Brandt committed
108
        totalBoxArea: float = self.filterArea * self.fraction
Josef Brandt's avatar
Josef Brandt committed
109 110
        boxArea: float = totalBoxArea / (2 * self.numBoxesAcross - 1)
        return boxArea ** 0.5
111 112 113 114 115 116

    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)
Josef Brandt's avatar
Josef Brandt committed
117 118
        middleXCoordinate: float = xStartCoordinates[self.numBoxesAcross // 2]
        middleYCoordinate: float = yStartCoordinates[self.numBoxesAcross // 2]
119 120 121

        for i in range(self.numBoxesAcross):
            topLeftCorners.append((middleXCoordinate, yStartCoordinates[i]))
Josef Brandt's avatar
Josef Brandt committed
122
            if i != self.numBoxesAcross // 2:
123 124 125 126 127 128 129
                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.
Josef Brandt's avatar
Josef Brandt committed
130
        It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
131 132
        :return float:
        """
Josef Brandt's avatar
Josef Brandt committed
133
        alpha: float = np.deg2rad(135)
Josef Brandt's avatar
Josef Brandt committed
134 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
Josef Brandt's avatar
Josef Brandt committed
136 137 138 139
        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)
Josef Brandt's avatar
Josef Brandt committed
140 141
        numBoxes: int = 2 * self.numBoxesAcross - 1
        totalBoxArea: float = numBoxes * (maxBoxSize ** 2)
Josef Brandt's avatar
Josef Brandt committed
142 143
        maxFraction: float = totalBoxArea / self.filterArea
        return maxFraction
144 145 146 147 148 149 150

    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:
        """
Josef Brandt's avatar
Josef Brandt committed
151
        return self._get_box_starts(boxSize)
152 153 154 155 156 157 158

    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:
        """
Josef Brandt's avatar
Josef Brandt committed
159
        return self._get_box_starts(boxSize)
160

Josef Brandt's avatar
Josef Brandt committed
161
    def _get_box_starts(self, boxSize: float) -> list:
162
        maxBoxSize: float = self.filterDiameter / self.numBoxesAcross
163 164 165
        assert maxBoxSize >= boxSize
        tileStarts: list = []
        for i in range(self.numBoxesAcross):
166
            start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
167 168
            tileStarts.append(start)

169 170 171
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
172
class SpiralSelector(BoxSelectionSubsamplingMethod):
Josef Brandt's avatar
Josef Brandt committed
173 174
    possibleBoxNumbers: list = [10, 15, 20]

175 176 177 178
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
        super(SpiralSelector, self).__init__(particleContainer, desiredFraction)
        self.numBoxes = 20

Josef Brandt's avatar
Josef Brandt committed
179 180 181 182 183 184 185 186 187 188 189 190 191 192
    @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

193 194
    @property
    def spiralSlope(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
195
        return self.armDistance / (2 * np.pi)
196 197 198 199 200 201

    @property
    def armDistance(self) -> float:
        return np.sqrt(2) * self.boxSize

    def get_topLeft_of_boxes(self) -> list:
Josef Brandt's avatar
Josef Brandt committed
202 203 204 205
        """
        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:"""
Josef Brandt's avatar
Josef Brandt committed
206
        filterCenter: tuple = self.filterDiameter / 2 - self.boxSize / 2, self.filterDiameter / 2 - self.boxSize / 2
207 208
        slope = self.spiralSlope
        theta: float = 0
Josef Brandt's avatar
Josef Brandt committed
209
        boxDistance = self.boxSize * 1.1
210 211

        topLefts: list = []
Josef Brandt's avatar
Josef Brandt committed
212
        for i in range(self.numBoxes):
213 214
            newPoint: tuple = self._get_xy_at_angle(theta, filterCenter)
            topLefts.append(newPoint)
Josef Brandt's avatar
Josef Brandt committed
215
            theta += boxDistance / (slope * np.sqrt(1 + theta ** 2))
Josef Brandt's avatar
Josef Brandt committed
216
            boxDistance *= 1.05
217

Josef Brandt's avatar
Josef Brandt committed
218
        topLefts = self._move_and_scale_toplefts(topLefts)
219 220
        return topLefts

Josef Brandt's avatar
Josef Brandt committed
221 222 223 224 225 226
    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:
        """
Josef Brandt's avatar
Josef Brandt committed
227 228
        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
Josef Brandt's avatar
Josef Brandt committed
229

Josef Brandt's avatar
Josef Brandt committed
230 231
        xCoordsBoxMiddles: np.ndarray = xCoords + self.boxSize / 2
        yCoordsBoxMiddles: np.ndarray = yCoords + self.boxSize / 2
Josef Brandt's avatar
Josef Brandt committed
232 233 234 235 236

        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
237
        desiredDistanceTotal: float = self.filterDiameter / 2
Josef Brandt's avatar
Josef Brandt committed
238 239 240 241 242 243
        desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance
        scaleFactor: float = desiredDistanceCenter / distanceLastCenter

        xCoordsBoxMiddles *= scaleFactor
        yCoordsBoxMiddles *= scaleFactor

Josef Brandt's avatar
Josef Brandt committed
244 245
        xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
        yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
Josef Brandt's avatar
Josef Brandt committed
246 247 248 249 250 251 252 253 254 255 256 257 258

        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
Josef Brandt's avatar
Josef Brandt committed
259 260 261 262
        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]])
Josef Brandt's avatar
Josef Brandt committed
263

Josef Brandt's avatar
Josef Brandt committed
264
        distances: np.ndarray = np.linalg.norm(coords - center, axis=1)
Josef Brandt's avatar
Josef Brandt committed
265
        return np.max(distances)
266 267 268

    def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple:
        distance: float = self.spiralSlope * theta
Josef Brandt's avatar
Josef Brandt committed
269
        return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1]
Josef Brandt's avatar
Josef Brandt committed
270 271 272 273 274 275 276 277 278 279 280 281 282 283

    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