geometricMethods.py 7.49 KB
Newer Older
1
import numpy as np
Josef Brandt's avatar
Josef Brandt committed
2
from itertools import combinations
Josef Brandt's avatar
Josef Brandt committed
3 4
from methods import BoxSelectionSubsamplingMethod
from helpers import box_contains_contour
5

6

Josef Brandt's avatar
Josef Brandt committed
7 8
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
    def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
9
        super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
Josef Brandt's avatar
Josef Brandt committed
10
        self.numBoxesAcross: int = 3      # either 3 or 5
11 12 13

    @property
    def boxSize(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
14 15 16 17
        maxFraction = self.get_maximum_achievable_fraction()
        if maxFraction < self.fraction:
            self.fraction = maxFraction

Josef Brandt's avatar
Josef Brandt committed
18
        totalBoxArea: float = self.filterArea * self.fraction
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
        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
40
        It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
41 42
        :return float:
        """
Josef Brandt's avatar
Josef Brandt committed
43 44 45 46 47 48 49
        alpha: float = np.deg2rad(135)
        r: float = self.filterSize/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)
50
        numBoxes: int = 2*self.numBoxesAcross - 1
Josef Brandt's avatar
Josef Brandt committed
51 52 53
        totalBoxArea: float = numBoxes * (maxBoxSize**2)
        maxFraction: float = totalBoxArea / self.filterArea
        return maxFraction
54 55 56 57 58 59 60

    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
61
        return self._get_box_starts(boxSize)
62 63 64 65 66 67 68

    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
69
        return self._get_box_starts(boxSize)
70

Josef Brandt's avatar
Josef Brandt committed
71 72
    def _get_box_starts(self, boxSize: float) -> list:
        maxBoxSize: float = self.filterSize / self.numBoxesAcross
73 74 75
        assert maxBoxSize >= boxSize
        tileStarts: list = []
        for i in range(self.numBoxesAcross):
Josef Brandt's avatar
Josef Brandt committed
76
            start: float = i * self.filterSize / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
77 78
            tileStarts.append(start)

79 80 81
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
82
class SpiralSelector(BoxSelectionSubsamplingMethod):
83 84 85 86 87 88 89 90 91 92 93 94 95 96
    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
97
    def actuallyCoveredFraction(self) -> float:
Josef Brandt's avatar
Josef Brandt committed
98
        return self.numBoxes*self.boxSize**2 / self.filterArea
99 100

    def get_topLeft_of_boxes(self) -> list:
Josef Brandt's avatar
Josef Brandt committed
101 102 103 104
        """
        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
105
        filterCenter: tuple = self.filterSize/2 - self.boxSize/2, self.filterSize/2 - self.boxSize/2
106 107
        slope = self.spiralSlope
        theta: float = 0
Josef Brandt's avatar
Josef Brandt committed
108
        boxDistance = self.boxSize * 1.1
109 110

        topLefts: list = []
Josef Brandt's avatar
Josef Brandt committed
111
        for i in range(self.numBoxes):
112 113 114
            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
115
            boxDistance *= 1.05
116

Josef Brandt's avatar
Josef Brandt committed
117
        topLefts = self._move_and_scale_toplefts(topLefts)
118 119
        return topLefts

Josef Brandt's avatar
Josef Brandt committed
120 121 122 123 124 125
    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
126 127
        xCoords: np.array = np.array([float(point[0]) for point in topLefts]) - self.filterSize / 2
        yCoords: np.array = np.array([float(point[1]) for point in topLefts]) - self.filterSize / 2
Josef Brandt's avatar
Josef Brandt committed
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

        xCoordsBoxMiddles: np.array = xCoords + self.boxSize/2
        yCoordsBoxMiddles: np.array = 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.filterHeight / 2
        desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance
        scaleFactor: float = desiredDistanceCenter / distanceLastCenter

        xCoordsBoxMiddles *= scaleFactor
        yCoordsBoxMiddles *= scaleFactor

Josef Brandt's avatar
Josef Brandt committed
143 144
        xCoords = xCoordsBoxMiddles + (self.filterSize - self.boxSize)/2
        yCoords = yCoordsBoxMiddles + (self.filterSize - self.boxSize)/2
Josef Brandt's avatar
Josef Brandt committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164

        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.array = 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.array = np.linalg.norm(coords - center, axis=1)
        return np.max(distances)
165 166 167

    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
168
        return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1]
Josef Brandt's avatar
Josef Brandt committed
169 170 171 172 173 174 175 176 177 178 179 180 181 182

    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