geometricMethods.py 9.35 KB
Newer Older
1
import numpy as np
Josef Brandt's avatar
Josef Brandt committed
2
from itertools import combinations
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
from methods import SubsamplingMethod
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset


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

    @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

    def get_crossBoxSelectors_for_fraction(self, desiredFraction: float) -> list:
        """
        Creates CrossBoxSelectors that fullfil the desired fraction criterium.
        :param desiredFraction:
        :return:
        """
        crossBoxSelectors = []
        offset, diameter, widthHeight = self.get_filterDimensions_from_dataset()


        # 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
58

59

Josef Brandt's avatar
Josef Brandt committed
60 61
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
62
        super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
Josef Brandt's avatar
Josef Brandt committed
63
        self.numBoxesAcross: int = 3      # either 3 or 5
64 65 66

    @property
    def boxSize(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
67 68 69 70
        maxFraction = self.get_maximum_achievable_fraction()
        if maxFraction < self.fraction:
            self.fraction = maxFraction

Josef Brandt's avatar
Josef Brandt committed
71
        totalBoxArea: float = self.filterArea * self.fraction
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
        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.
Josef Brandt's avatar
Josef Brandt committed
93
        It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
94 95
        :return float:
        """
Josef Brandt's avatar
Josef Brandt committed
96
        alpha: float = np.deg2rad(135)
97
        r: float = self.filterDiameter/2
Josef Brandt's avatar
Josef Brandt committed
98 99 100 101 102
        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)
103
        numBoxes: int = 2*self.numBoxesAcross - 1
Josef Brandt's avatar
Josef Brandt committed
104 105 106
        totalBoxArea: float = numBoxes * (maxBoxSize**2)
        maxFraction: float = totalBoxArea / self.filterArea
        return maxFraction
107 108 109 110 111 112 113

    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
114
        return self._get_box_starts(boxSize)
115 116 117 118 119 120 121

    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
122
        return self._get_box_starts(boxSize)
123

Josef Brandt's avatar
Josef Brandt committed
124
    def _get_box_starts(self, boxSize: float) -> list:
125
        maxBoxSize: float = self.filterDiameter / self.numBoxesAcross
126 127 128
        assert maxBoxSize >= boxSize
        tileStarts: list = []
        for i in range(self.numBoxesAcross):
129
            start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
130 131
            tileStarts.append(start)

132 133 134
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
135
class SpiralSelector(BoxSelectionSubsamplingMethod):
136 137 138 139 140 141 142 143 144 145 146 147 148 149
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
        super(SpiralSelector, self).__init__(particleContainer, desiredFraction)
        self.boxSize: float = 50
        self.numBoxes = 20

    @property
    def spiralSlope(self) -> float:
        return self.armDistance / (2*np.pi)

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

    @property
Josef Brandt's avatar
Josef Brandt committed
150
    def actuallyCoveredFraction(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
151
        return self.numBoxes*self.boxSize**2 / self.filterArea
152 153

    def get_topLeft_of_boxes(self) -> list:
Josef Brandt's avatar
Josef Brandt committed
154 155 156 157
        """
        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:"""
158
        filterCenter: tuple = self.filterDiameter/2 - self.boxSize/2, self.filterDiameter/2 - self.boxSize/2
159 160
        slope = self.spiralSlope
        theta: float = 0
Josef Brandt's avatar
Josef Brandt committed
161
        boxDistance = self.boxSize * 1.1
162 163

        topLefts: list = []
Josef Brandt's avatar
Josef Brandt committed
164
        for i in range(self.numBoxes):
165 166 167
            newPoint: tuple = self._get_xy_at_angle(theta, filterCenter)
            topLefts.append(newPoint)
            theta += boxDistance / (slope * np.sqrt(1 + theta**2))
Josef Brandt's avatar
Josef Brandt committed
168
            boxDistance *= 1.05
169

Josef Brandt's avatar
Josef Brandt committed
170
        topLefts = self._move_and_scale_toplefts(topLefts)
171 172
        return topLefts

Josef Brandt's avatar
Josef Brandt committed
173 174 175 176 177 178
    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:
        """
179 180
        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
181

182 183
        xCoordsBoxMiddles: np.ndarray= xCoords + self.boxSize/2
        yCoordsBoxMiddles: np.ndarray= yCoords + self.boxSize/2
Josef Brandt's avatar
Josef Brandt committed
184 185 186 187 188

        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
189
        desiredDistanceTotal: float = self.filterDiameter / 2
Josef Brandt's avatar
Josef Brandt committed
190 191 192 193 194 195
        desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance
        scaleFactor: float = desiredDistanceCenter / distanceLastCenter

        xCoordsBoxMiddles *= scaleFactor
        yCoordsBoxMiddles *= scaleFactor

196 197
        xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
        yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
Josef Brandt's avatar
Josef Brandt committed
198 199 200 201 202 203 204 205 206 207 208 209 210

        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
211
        coords: np.ndarray= np.array([[boxCenter[0] - 0.5*boxSize, boxCenter[1] - 0.5*boxSize],
Josef Brandt's avatar
Josef Brandt committed
212 213 214 215
                                     [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]])

216
        distances: np.ndarray= np.linalg.norm(coords - center, axis=1)
Josef Brandt's avatar
Josef Brandt committed
217
        return np.max(distances)
218 219 220

    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
221
        return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1]
Josef Brandt's avatar
Josef Brandt committed
222 223 224 225 226 227 228 229 230 231 232 233 234 235

    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