Commit bc14998e authored by Josef Brandt's avatar Josef Brandt

Spiral working more robust

parent 1eba109f
import numpy as np
from itertools import combinations
from methods import BoxSelectionSubsamplingMethod
from helpers import box_contains_contour
......@@ -82,7 +83,6 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
self.boxSize: float = 50
self.numBoxes = 20
# self.numTurns: float = 4
@property
def spiralSlope(self) -> float:
......@@ -94,32 +94,91 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
return np.sqrt(2) * self.boxSize
@property
def numTurns(self) -> float:
assert self.filterHeight == self.filterWidth
return (self.filterHeight / 2) / self.armDistance
@property
def finalAngle(self) -> float:
return self.numTurns * 2*np.pi
def actuallyCoveredFraction(self) -> float:
return self.numBoxes*self.boxSize**2 / (self.filterHeight*self.filterWidth)
def get_topLeft_of_boxes(self) -> list:
totalLength: float = self._get_spiralLength_after_angle(self.finalAngle)
boxDistance: float = totalLength / self.numBoxes * 0.5
filterCenter: tuple = self.filterWidth / 2, self.filterHeight / 2
"""
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
slope = self.spiralSlope
theta: float = 0
boxDistance = self.boxSize * 1.1
topLefts: list = []
for _ in range(self.numBoxes):
for i in range(self.numBoxes):
newPoint: tuple = self._get_xy_at_angle(theta, filterCenter)
topLefts.append(newPoint)
theta += boxDistance / (slope * np.sqrt(1 + theta**2))
boxDistance *= 1.05
topLefts = self._move_and_scale_toplefts(topLefts)
return topLefts
def _get_spiralLength_after_angle(self, theta: float):
return 0.5*self.spiralSlope*(theta*(1+theta**2)**0.5 + np.sinh(theta)**(-1))
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)
def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple:
distance: float = self.spiralSlope * theta
return distance * np.cos(theta) + centerXY[0], distance * np.sin(theta) + centerXY[1]
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
......@@ -89,7 +89,7 @@ class CrossBoxesControls(QtWidgets.QGroupBox):
class SpiralBoxMode(MeasureMode):
def __init__(self, *args):
super(SpiralBoxMode, self).__init__(*args)
self.uiControls = QtWidgets.QGroupBox('Spiral Box Controls')
self.uiControls: SpiralBoxControls = SpiralBoxControls(self)
self.spiralBoxGenerator: SpiralSelector = SpiralSelector(None)
self.update_measure_viewItems()
......@@ -97,13 +97,8 @@ class SpiralBoxMode(MeasureMode):
self.spiralBoxGenerator.filterHeight = self.filterView.filter.height
self.spiralBoxGenerator.filterWidth = self.filterView.filter.width
# desiredCoverage: int = self.uiControls.coverageSpinbox.value()
# maxCoverage: int = int(self.crossBoxGenerator.get_maximum_achievable_fraction() * 100)
# self.uiControls.set_to_max_possible_coverage(maxCoverage)
# if desiredCoverage > maxCoverage:
# desiredCoverage = maxCoverage
# self.crossBoxGenerator.fraction = desiredCoverage / 100
self.spiralBoxGenerator.boxSize = self.uiControls.boxSizeSpinbox.value()
self.spiralBoxGenerator.numBoxes = self.uiControls.numBoxesSpinbox.value()
viewItems = []
topLefts: list = self.spiralBoxGenerator.get_topLeft_of_boxes()
......@@ -112,4 +107,35 @@ class SpiralBoxMode(MeasureMode):
newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize)
viewItems.append(newBox)
self.filterView.update_measure_boxes(viewItems)
\ No newline at end of file
self.filterView.update_measure_boxes(viewItems)
class SpiralBoxControls(QtWidgets.QGroupBox):
"""
Gives a groupbox with the controls for setting up the cross boxes.
"""
def __init__(self, measureModeParent: MeasureMode):
super(SpiralBoxControls, self).__init__()
self.setTitle('Spiral Box Controls')
self.measureModeParent = measureModeParent
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
layout.addWidget(QtWidgets.QLabel('Box Size:'))
self.boxSizeSpinbox = QtWidgets.QSpinBox()
self.boxSizeSpinbox.setValue(50)
self.boxSizeSpinbox.setMaximum(1000)
self.boxSizeSpinbox.valueChanged.connect(self._config_changed)
layout.addWidget(self.boxSizeSpinbox)
layout.addStretch()
layout.addWidget(QtWidgets.QLabel('Num Boxes:'))
self.numBoxesSpinbox = QtWidgets.QSpinBox()
self.numBoxesSpinbox.setValue(10)
self.numBoxesSpinbox.valueChanged.connect(self._config_changed)
layout.addWidget(self.numBoxesSpinbox)
def _config_changed(self):
self.measureModeParent.update_measure_viewItems()
......@@ -44,6 +44,9 @@ class BoxSelectionSubsamplingMethod(SubsamplingMethod):
def get_topLeft_of_boxes(self) -> list:
raise NotImplementedError
def get_parameterCombos_for_fraction(self) -> list:
raise NotImplementedError
class RandomSampling(SubsamplingMethod):
def apply_subsampling_method(self):
......
import unittest
from geometricMethods import CrossBoxSelector
import numpy as np
from geometricMethods import CrossBoxSelector, SpiralSelector
class TestSelectBoxes(unittest.TestCase):
class TestSelectCrossBoxes(unittest.TestCase):
def setUp(self) -> None:
self.crossBoxSelector = CrossBoxSelector(None)
......@@ -62,3 +63,58 @@ class TestSelectBoxes(unittest.TestCase):
boxSize: float = areaPerBox**0.5
self.assertEqual(self.crossBoxSelector.boxSize, boxSize)
class TestSelectSpiralBoxes(unittest.TestCase):
def setUp(self) -> None:
self.spiralBoxSelector: SpiralSelector = SpiralSelector(None)
def test_move_and_scale_toplefts(self):
self.spiralBoxSelector.filterHeight = 100
self.spiralBoxSelector.filterWidth = 100
self.spiralBoxSelector.boxSize = 10
topLefts = [(45, 45), (0, 45), (90, 45)]
newTopLefts = self.spiralBoxSelector._move_and_scale_toplefts(topLefts)
self.assertEqual(newTopLefts[0], (45, 45))
self.assertEqual(newTopLefts[1], (0, 45))
self.assertEqual(newTopLefts[2], (90, 45))
def test_get_max_distance_of_boxCenter_to_center(self):
boxCenter = 0, 0
self.spiralBoxSelector.boxSize = 1
maxDistance: float = self.spiralBoxSelector._get_max_distance_of_boxCenter_to_center(boxCenter)
self.assertEqual(maxDistance, np.sqrt(0.5**2 + 0.5**2))
self.spiralBoxSelector.boxSize = 2
maxDistance: float = self.spiralBoxSelector._get_max_distance_of_boxCenter_to_center(boxCenter)
self.assertEqual(maxDistance, np.sqrt(2))
boxCenter = 1, 0
self.spiralBoxSelector.boxSize = 2
maxDistance: float = self.spiralBoxSelector._get_max_distance_of_boxCenter_to_center(boxCenter)
self.assertEqual(maxDistance, np.sqrt(2**2 + 1**2))
boxCenter = -1, -3
self.spiralBoxSelector.boxSize = 2
maxDistance: float = self.spiralBoxSelector._get_max_distance_of_boxCenter_to_center(boxCenter)
self.assertEqual(maxDistance, np.sqrt(4**2 + 2**2))
def test_boxes_are_overlapping(self):
self.spiralBoxSelector.boxSize = 10
topLefts: list = [(0, 0), (10, 10), (20, 10)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
self.assertEqual(overlaps, False)
topLefts: list = [(0, 0), (9, 10)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
self.assertEqual(overlaps, False)
topLefts: list = [(0, 0), (9, 9)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
self.assertEqual(overlaps, True)
topLefts: list = [(-2, 0), (5, 9)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
self.assertEqual(overlaps, True)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment