geometricMethods.py 7.6 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 10 11
        super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
        self.filterWidth: float = 1000
        self.filterHeight: float = 1000
Josef Brandt's avatar
Josef Brandt committed
12
        self.numBoxesAcross: int = 3      # either 3 or 5
13 14 15

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

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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
        totalBoxArea: float = ((self.filterWidth*self.filterHeight)*self.fraction)
        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.
        It is with respect to a rectangular filter of width*height, for circular filter this has to be divided by 0.786
        :return float:
        """
        maxBoxWidth: float = self.filterWidth / self.numBoxesAcross
        maxBoxHeight: float = self.filterHeight / self.numBoxesAcross
        numBoxes: int = 2*self.numBoxesAcross - 1
        totalBoxArea: float = numBoxes * (maxBoxWidth * maxBoxHeight)
        return totalBoxArea / (self.filterHeight * self.filterWidth)

    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:
        """
        return self._get_box_starts(self.filterWidth, boxSize)

    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:
        """
        return self._get_box_starts(self.filterHeight, boxSize)

    def _get_box_starts(self, filterSize: float, boxSize: float) -> list:
        maxBoxSize: float = filterSize / self.numBoxesAcross
        assert maxBoxSize >= boxSize
        tileStarts: list = []
        for i in range(self.numBoxesAcross):
            start: float = i * filterSize / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
            tileStarts.append(start)

75 76 77
        return tileStarts


Josef Brandt's avatar
Josef Brandt committed
78
class SpiralSelector(BoxSelectionSubsamplingMethod):
79 80 81 82 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.filterWidth: float
        self.filterHeight: float

        self.boxSize: float = 50
        self.numBoxes = 20

    @property
    def spiralSlope(self) -> float:
        assert self.filterHeight == self.filterWidth
        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 98
    def actuallyCoveredFraction(self) -> float:
        return self.numBoxes*self.boxSize**2 / (self.filterHeight*self.filterWidth)
99 100

    def get_topLeft_of_boxes(self) -> list:
Josef Brandt's avatar
Josef Brandt committed
101 102 103 104 105
        """
        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:"""
        filterCenter: tuple = self.filterWidth/2 - self.boxSize/2, self.filterHeight/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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    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:
        """
        assert self.filterHeight == self.filterWidth  # elliptical filters are not supportet here..

        xCoords: np.array = np.array([float(point[0]) for point in topLefts]) - self.filterWidth / 2
        yCoords: np.array = np.array([float(point[1]) for point in topLefts]) - self.filterHeight / 2

        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

        xCoords = xCoordsBoxMiddles + (self.filterWidth - self.boxSize)/2
        yCoords = yCoordsBoxMiddles + (self.filterHeight - self.boxSize)/2

        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)
167 168 169

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

    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