geometricMethods.py 12 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
    @property
    def filterArea(self) -> float:
        return np.pi * (self.filterDiameter / 2) ** 2

25 26 27 28
    def apply_subsampling_method(self) -> list:
        def distanceToCnt(topleft: tuple):
            return abs(topleft[0] - cntStart[0]) + abs(topleft[1] - cntStart[1])

29 30
        subParticles: list = []
        topLefts: list = self.get_topLeft_of_boxes()
31 32
        boxWidthHeight: tuple = (self.boxSize, self.boxSize)

33
        for particle in self.particleContainer.particles:
34 35 36 37 38
            cntStart: tuple = (particle.contour[0, 0, 0], particle.contour[0, 0, 1])
            sortedTopLefts = sorted(topLefts, key=distanceToCnt)

            for topLeftXY in sortedTopLefts:
                if helpers.box_overlaps_contour(topLeftXY, boxWidthHeight, particle.contour):
39
                    subParticles.append(particle)
40
                    break
41

42
        return subParticles
43 44 45 46

    def get_topLeft_of_boxes(self) -> list:
        raise NotImplementedError

47 48 49 50 51 52 53 54 55 56
    def _apply_offset_to_toplefts(self, topLefts: list) -> list:
        """
        Applies the filter offset to the calculated topLefts of the measure boxes.
        :param topLefts:
        :return:
        """
        newTopLefts: list = []
        for topLeft in topLefts:
            newTopLefts.append((topLeft[0] + self.offset[0], topLeft[1] + self.offset[1]))
        return newTopLefts
57 58 59 60 61

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
62 63
        self.minNumberOfBoxes: int = 10
        self.maxNumberOfBoxes: int = 20
64 65 66

    def get_crossBoxSelectors_for_fraction(self, desiredFraction: float) -> list:
        """
Josef Brandt's avatar
Josef Brandt committed
67
        Creates CrossBoxSelectors that fullfill the desired fraction criterium.
68
        :param desiredFraction:
Josef Brandt's avatar
Josef Brandt committed
69
        :return list of CrossBoxSelectors:
70 71
        """
        crossBoxSelectors = []
Josef Brandt's avatar
Josef Brandt committed
72 73 74 75
        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])
76

Josef Brandt's avatar
Josef Brandt committed
77 78 79 80 81
        for numBoxesAcross in [3, 5]:
            newBoxSelector: CrossBoxSelector = CrossBoxSelector(self.dataset.particleContainer, desiredFraction)
            newBoxSelector.filterDiameter = diameter
            newBoxSelector.offset = offset
            newBoxSelector.numBoxesAcross = numBoxesAcross
82

Josef Brandt's avatar
Josef Brandt committed
83
            crossBoxSelectors.append(newBoxSelector)
84 85

        return crossBoxSelectors
86

Josef Brandt's avatar
Josef Brandt committed
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
    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

110

Josef Brandt's avatar
Josef Brandt committed
111 112
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
113
        super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
Josef Brandt's avatar
Josef Brandt committed
114 115 116 117 118
        self.numBoxesAcross: int = 3  # either 3 or 5

    @property
    def label(self) -> str:
        return f'Boxes CrossLayout ({self.numBoxesAcross} boxes across)'
119 120 121

    @property
    def boxSize(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
122 123 124 125
        maxFraction = self.get_maximum_achievable_fraction()
        if maxFraction < self.fraction:
            self.fraction = maxFraction

Josef Brandt's avatar
Josef Brandt committed
126
        totalBoxArea: float = self.filterArea * self.fraction
Josef Brandt's avatar
Josef Brandt committed
127 128
        boxArea: float = totalBoxArea / (2 * self.numBoxesAcross - 1)
        return boxArea ** 0.5
129 130 131 132 133 134

    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
135 136
        middleXCoordinate: float = xStartCoordinates[self.numBoxesAcross // 2]
        middleYCoordinate: float = yStartCoordinates[self.numBoxesAcross // 2]
137 138 139

        for i in range(self.numBoxesAcross):
            topLeftCorners.append((middleXCoordinate, yStartCoordinates[i]))
Josef Brandt's avatar
Josef Brandt committed
140
            if i != self.numBoxesAcross // 2:
141 142
                topLeftCorners.append((xStartCoordinates[i], middleYCoordinate))

143
        return self._apply_offset_to_toplefts(topLeftCorners)
144 145 146 147

    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
148
        It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
149 150
        :return float:
        """
Josef Brandt's avatar
Josef Brandt committed
151
        alpha: float = np.deg2rad(135)
Josef Brandt's avatar
Josef Brandt committed
152 153
        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
154 155 156 157
        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
158 159
        numBoxes: int = 2 * self.numBoxesAcross - 1
        totalBoxArea: float = numBoxes * (maxBoxSize ** 2)
Josef Brandt's avatar
Josef Brandt committed
160 161
        maxFraction: float = totalBoxArea / self.filterArea
        return maxFraction
162 163 164 165 166 167 168

    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
169
        return self._get_box_starts(boxSize)
170 171 172 173 174 175 176

    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
177
        return self._get_box_starts(boxSize)
178

Josef Brandt's avatar
Josef Brandt committed
179
    def _get_box_starts(self, boxSize: float) -> list:
180
        maxBoxSize: float = self.filterDiameter / self.numBoxesAcross
181 182 183
        assert maxBoxSize >= boxSize
        tileStarts: list = []
        for i in range(self.numBoxesAcross):
184
            start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
185 186
            tileStarts.append(start)

187 188 189
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
190
class SpiralSelector(BoxSelectionSubsamplingMethod):
Josef Brandt's avatar
Josef Brandt committed
191 192
    possibleBoxNumbers: list = [10, 15, 20]

193 194 195 196
    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
197 198 199 200 201 202 203 204 205 206 207 208 209 210
    @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

211 212
    @property
    def spiralSlope(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
213
        return self.armDistance / (2 * np.pi)
214 215 216 217 218 219

    @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
220 221 222 223
        """
        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
224
        filterCenter: tuple = self.filterDiameter / 2 - self.boxSize / 2, self.filterDiameter / 2 - self.boxSize / 2
225 226
        slope = self.spiralSlope
        theta: float = 0
Josef Brandt's avatar
Josef Brandt committed
227
        boxDistance = self.boxSize * 1.1
228 229

        topLefts: list = []
Josef Brandt's avatar
Josef Brandt committed
230
        for i in range(self.numBoxes):
231 232
            newPoint: tuple = self._get_xy_at_angle(theta, filterCenter)
            topLefts.append(newPoint)
Josef Brandt's avatar
Josef Brandt committed
233
            theta += boxDistance / (slope * np.sqrt(1 + theta ** 2))
Josef Brandt's avatar
Josef Brandt committed
234
            boxDistance *= 1.05
235

Josef Brandt's avatar
Josef Brandt committed
236
        topLefts = self._move_and_scale_toplefts(topLefts)
237
        return self._apply_offset_to_toplefts(topLefts)
238

Josef Brandt's avatar
Josef Brandt committed
239 240 241 242 243 244
    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
245 246
        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
247

Josef Brandt's avatar
Josef Brandt committed
248 249
        xCoordsBoxMiddles: np.ndarray = xCoords + self.boxSize / 2
        yCoordsBoxMiddles: np.ndarray = yCoords + self.boxSize / 2
Josef Brandt's avatar
Josef Brandt committed
250 251 252 253 254

        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
255
        desiredDistanceTotal: float = self.filterDiameter / 2
Josef Brandt's avatar
Josef Brandt committed
256 257 258 259 260 261
        desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance
        scaleFactor: float = desiredDistanceCenter / distanceLastCenter

        xCoordsBoxMiddles *= scaleFactor
        yCoordsBoxMiddles *= scaleFactor

Josef Brandt's avatar
Josef Brandt committed
262 263
        xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
        yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize) / 2
Josef Brandt's avatar
Josef Brandt committed
264 265 266 267 268 269 270 271 272 273 274 275 276

        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
277 278 279 280
        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
281

Josef Brandt's avatar
Josef Brandt committed
282
        distances: np.ndarray = np.linalg.norm(coords - center, axis=1)
Josef Brandt's avatar
Josef Brandt committed
283
        return np.max(distances)
284 285 286

    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
287
        return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1]
Josef Brandt's avatar
Josef Brandt committed
288 289 290 291 292 293 294 295 296 297 298 299 300 301

    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