geometricMethods.py 12.2 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

Josef Brandt's avatar
Josef Brandt committed
58

59 60 61 62 63
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
64
    def get_crossBoxSubsamplers_for_fraction(self, desiredFraction: float) -> list:
65
        """
Josef Brandt's avatar
Josef Brandt committed
66
        Creates CrossBoxSubsamplers that fullfill the desired fraction criterium.
67
        :param desiredFraction:
Josef Brandt's avatar
Josef Brandt committed
68
        :return list of CrossBoxSubsamplers:
69
        """
Josef Brandt's avatar
Josef Brandt committed
70
        crossBoxSubsamplers = []
Josef Brandt's avatar
Josef Brandt committed
71 72 73 74
        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])
75

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

Josef Brandt's avatar
Josef Brandt committed
82 83 84
            maxFraction: float = newBoxSelector.get_maximum_achievable_fraction()
            if desiredFraction <= maxFraction:
                crossBoxSubsamplers.append(newBoxSelector)
85

Josef Brandt's avatar
Josef Brandt committed
86
        return crossBoxSubsamplers
87

Josef Brandt's avatar
Josef Brandt committed
88
    def get_spiralBoxSubsamplers_for_fraction(self, desiredFraction: float) -> list:
Josef Brandt's avatar
Josef Brandt committed
89
        """
Josef Brandt's avatar
Josef Brandt committed
90
        Creates CrossBoxSubsamplers that fullfill the desired fraction criterium.
Josef Brandt's avatar
Josef Brandt committed
91 92 93
        :param desiredFraction:
        :return list of SpiralBoxSelectors:
        """
Josef Brandt's avatar
Josef Brandt committed
94
        spiralBoxSubsamplers = []
Josef Brandt's avatar
Josef Brandt committed
95 96 97 98 99
        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])

Josef Brandt's avatar
Josef Brandt committed
100 101
        for numBoxes in SpiralBoxSubsampling.possibleBoxNumbers:
            newBoxSelector: SpiralBoxSubsampling = SpiralBoxSubsampling(self.dataset.particleContainer, desiredFraction)
Josef Brandt's avatar
Josef Brandt committed
102 103 104 105 106
            newBoxSelector.filterDiameter = diameter
            newBoxSelector.offset = offset
            newBoxSelector.numBoxes = numBoxes

            if newBoxSelector.noBoxOverlap:
Josef Brandt's avatar
Josef Brandt committed
107
                spiralBoxSubsamplers.append(newBoxSelector)
Josef Brandt's avatar
Josef Brandt committed
108

Josef Brandt's avatar
Josef Brandt committed
109
        return spiralBoxSubsamplers
Josef Brandt's avatar
Josef Brandt committed
110

111

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

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

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

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

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

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

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

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

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

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

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

188 189 190
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
191 192
class SpiralBoxSubsampling(BoxSelectionSubsamplingMethod):
    possibleBoxNumbers: list = [7, 10, 15]
Josef Brandt's avatar
Josef Brandt committed
193

194
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
Josef Brandt's avatar
Josef Brandt committed
195 196
        super(SpiralBoxSubsampling, self).__init__(particleContainer, desiredFraction)
        self.numBoxes = 10
197

Josef Brandt's avatar
Josef Brandt committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211
    @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

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

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

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

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

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

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

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

        xCoordsBoxMiddles *= scaleFactor
        yCoordsBoxMiddles *= scaleFactor

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

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

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

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

    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