...
 
Commits (2)
...@@ -7,6 +7,7 @@ Created on Wed Jan 22 13:57:28 2020 ...@@ -7,6 +7,7 @@ Created on Wed Jan 22 13:57:28 2020
""" """
from helpers import ParticleBinSorter from helpers import ParticleBinSorter
class ResultComparer(object): class ResultComparer(object):
def _get_mp_count_error_per_bin(self, allParticles, subParticles, fractionMeasured): def _get_mp_count_error_per_bin(self, allParticles, subParticles, fractionMeasured):
...@@ -27,7 +28,7 @@ class ResultComparer(object): ...@@ -27,7 +28,7 @@ class ResultComparer(object):
elif numMPEstimate == 0: elif numMPEstimate == 0:
mpCountError = 0 mpCountError = 0
else: else:
raise Exception #> 0 particles in subsample, whereas none in entire sample. This cannot be! raise Exception # >0 particles in subsample, whereas none in entire sample. This cannot be!
return mpCountError return mpCountError
......
import numpy as np import numpy as np
from itertools import combinations from itertools import combinations
from methods import BoxSelectionSubsamplingMethod from methods import SubsamplingMethod
from helpers import box_contains_contour 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
class CrossBoxSelector(BoxSelectionSubsamplingMethod): class CrossBoxSelector(BoxSelectionSubsamplingMethod):
def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None: def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction) super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
self.filterWidth: float = 1000
self.filterHeight: float = 1000
self.numBoxesAcross: int = 3 # either 3 or 5 self.numBoxesAcross: int = 3 # either 3 or 5
@property @property
...@@ -17,7 +68,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): ...@@ -17,7 +68,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
if maxFraction < self.fraction: if maxFraction < self.fraction:
self.fraction = maxFraction self.fraction = maxFraction
totalBoxArea: float = ((self.filterWidth*self.filterHeight)*self.fraction) totalBoxArea: float = self.filterArea * self.fraction
boxArea: float = totalBoxArea / (2*self.numBoxesAcross - 1) boxArea: float = totalBoxArea / (2*self.numBoxesAcross - 1)
return boxArea**0.5 return boxArea**0.5
...@@ -39,14 +90,20 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): ...@@ -39,14 +90,20 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
def get_maximum_achievable_fraction(self) -> float: def get_maximum_achievable_fraction(self) -> float:
""" """
Returns the maximum achievable fraction, given the desired number of boxes across. 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 It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
:return float: :return float:
""" """
maxBoxWidth: float = self.filterWidth / self.numBoxesAcross alpha: float = np.deg2rad(135)
maxBoxHeight: float = self.filterHeight / self.numBoxesAcross r: float = self.filterDiameter/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)
numBoxes: int = 2*self.numBoxesAcross - 1 numBoxes: int = 2*self.numBoxesAcross - 1
totalBoxArea: float = numBoxes * (maxBoxWidth * maxBoxHeight) totalBoxArea: float = numBoxes * (maxBoxSize**2)
return totalBoxArea / (self.filterHeight * self.filterWidth) maxFraction: float = totalBoxArea / self.filterArea
return maxFraction
def _get_horizontal_box_starts(self, boxSize: float) -> list: def _get_horizontal_box_starts(self, boxSize: float) -> list:
""" """
...@@ -54,7 +111,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): ...@@ -54,7 +111,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
:param boxSize: :param boxSize:
:return list: :return list:
""" """
return self._get_box_starts(self.filterWidth, boxSize) return self._get_box_starts(boxSize)
def _get_vertical_box_starts(self, boxSize: float) -> list: def _get_vertical_box_starts(self, boxSize: float) -> list:
""" """
...@@ -62,14 +119,14 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): ...@@ -62,14 +119,14 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
:param boxSize: :param boxSize:
:return list: :return list:
""" """
return self._get_box_starts(self.filterHeight, boxSize) return self._get_box_starts(boxSize)
def _get_box_starts(self, filterSize: float, boxSize: float) -> list: def _get_box_starts(self, boxSize: float) -> list:
maxBoxSize: float = filterSize / self.numBoxesAcross maxBoxSize: float = self.filterDiameter / self.numBoxesAcross
assert maxBoxSize >= boxSize assert maxBoxSize >= boxSize
tileStarts: list = [] tileStarts: list = []
for i in range(self.numBoxesAcross): for i in range(self.numBoxesAcross):
start: float = i * filterSize / self.numBoxesAcross + (maxBoxSize - boxSize) / 2 start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2
tileStarts.append(start) tileStarts.append(start)
return tileStarts return tileStarts
...@@ -78,15 +135,11 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): ...@@ -78,15 +135,11 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
class SpiralSelector(BoxSelectionSubsamplingMethod): class SpiralSelector(BoxSelectionSubsamplingMethod):
def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None: def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
super(SpiralSelector, self).__init__(particleContainer, desiredFraction) super(SpiralSelector, self).__init__(particleContainer, desiredFraction)
self.filterWidth: float
self.filterHeight: float
self.boxSize: float = 50 self.boxSize: float = 50
self.numBoxes = 20 self.numBoxes = 20
@property @property
def spiralSlope(self) -> float: def spiralSlope(self) -> float:
assert self.filterHeight == self.filterWidth
return self.armDistance / (2*np.pi) return self.armDistance / (2*np.pi)
@property @property
...@@ -95,14 +148,14 @@ class SpiralSelector(BoxSelectionSubsamplingMethod): ...@@ -95,14 +148,14 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
@property @property
def actuallyCoveredFraction(self) -> float: def actuallyCoveredFraction(self) -> float:
return self.numBoxes*self.boxSize**2 / (self.filterHeight*self.filterWidth) return self.numBoxes*self.boxSize**2 / self.filterArea
def get_topLeft_of_boxes(self) -> list: def get_topLeft_of_boxes(self) -> list:
""" """
Calculates the topLeft-points (x, y) of all measure boxes Calculates the topLeft-points (x, y) of all measure boxes
The method uses an approximation for the spiral and is not purely accurate. The method uses an approximation for the spiral and is not purely accurate.
:return list:""" :return list:"""
filterCenter: tuple = self.filterWidth/2 - self.boxSize/2, self.filterHeight/2 - self.boxSize/2 filterCenter: tuple = self.filterDiameter/2 - self.boxSize/2, self.filterDiameter/2 - self.boxSize/2
slope = self.spiralSlope slope = self.spiralSlope
theta: float = 0 theta: float = 0
boxDistance = self.boxSize * 1.1 boxDistance = self.boxSize * 1.1
...@@ -123,27 +176,25 @@ class SpiralSelector(BoxSelectionSubsamplingMethod): ...@@ -123,27 +176,25 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
This function moves and scales the topLeft-Points so that all measure boxes lie within the filter limits. This function moves and scales the topLeft-Points so that all measure boxes lie within the filter limits.
:return list: :return list:
""" """
assert self.filterHeight == self.filterWidth # elliptical filters are not supportet here.. 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
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 xCoordsBoxMiddles: np.ndarray= xCoords + self.boxSize/2
yCoordsBoxMiddles: np.array = yCoords + self.boxSize/2 yCoordsBoxMiddles: np.ndarray= yCoords + self.boxSize/2
lastBoxCenter: tuple = (xCoordsBoxMiddles[-1], yCoordsBoxMiddles[-1]) lastBoxCenter: tuple = (xCoordsBoxMiddles[-1], yCoordsBoxMiddles[-1])
distanceLastCenter: float = np.linalg.norm(lastBoxCenter) distanceLastCenter: float = np.linalg.norm(lastBoxCenter)
maxDistanceInLastBox: float = self._get_max_distance_of_boxCenter_to_center(lastBoxCenter) maxDistanceInLastBox: float = self._get_max_distance_of_boxCenter_to_center(lastBoxCenter)
halfBoxDistance: float = maxDistanceInLastBox - distanceLastCenter halfBoxDistance: float = maxDistanceInLastBox - distanceLastCenter
desiredDistanceTotal: float = self.filterHeight / 2 desiredDistanceTotal: float = self.filterDiameter / 2
desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance desiredDistanceCenter: float = desiredDistanceTotal - halfBoxDistance
scaleFactor: float = desiredDistanceCenter / distanceLastCenter scaleFactor: float = desiredDistanceCenter / distanceLastCenter
xCoordsBoxMiddles *= scaleFactor xCoordsBoxMiddles *= scaleFactor
yCoordsBoxMiddles *= scaleFactor yCoordsBoxMiddles *= scaleFactor
xCoords = xCoordsBoxMiddles + (self.filterWidth - self.boxSize)/2 xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
yCoords = yCoordsBoxMiddles + (self.filterHeight - self.boxSize)/2 yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
newTopLefts = zip(np.round(xCoords), np.round(yCoords)) newTopLefts = zip(np.round(xCoords), np.round(yCoords))
return list(tuple(newTopLefts)) return list(tuple(newTopLefts))
...@@ -157,12 +208,12 @@ class SpiralSelector(BoxSelectionSubsamplingMethod): ...@@ -157,12 +208,12 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
""" """
center = np.array(center) center = np.array(center)
boxSize = self.boxSize boxSize = self.boxSize
coords: np.array = np.array([[boxCenter[0] - 0.5*boxSize, boxCenter[1] - 0.5*boxSize], 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], [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) distances: np.ndarray= np.linalg.norm(coords - center, axis=1)
return np.max(distances) return np.max(distances)
def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple: def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple:
......
from PyQt5 import QtGui, QtWidgets, QtCore from PyQt5 import QtGui, QtWidgets, QtCore
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
import gepard
from gepard import dataset
import helpers
class FilterView(QtWidgets.QGraphicsView): class FilterView(QtWidgets.QGraphicsView):
...@@ -7,6 +12,8 @@ class FilterView(QtWidgets.QGraphicsView): ...@@ -7,6 +12,8 @@ class FilterView(QtWidgets.QGraphicsView):
super(FilterView, self).__init__() super(FilterView, self).__init__()
self.setWindowTitle('FilterView') self.setWindowTitle('FilterView')
self.dataset: dataset.DataSet = None
scene = QtWidgets.QGraphicsScene(self) scene = QtWidgets.QGraphicsScene(self)
scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
scene.setBackgroundBrush(QtCore.Qt.darkGray) scene.setBackgroundBrush(QtCore.Qt.darkGray)
...@@ -14,37 +21,124 @@ class FilterView(QtWidgets.QGraphicsView): ...@@ -14,37 +21,124 @@ class FilterView(QtWidgets.QGraphicsView):
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QtGui.QPainter.Antialiasing) self.setRenderHint(QtGui.QPainter.Antialiasing)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.drag = None
self.filter = FilterGraphItem() self.filter: FilterGraphItem = FilterGraphItem()
self.scene().addItem(self.filter) self.scene().addItem(self.filter)
self.measuringBoxes: list = [] self.measuringBoxes: list = []
self.contourItems: list = []
def update_measure_boxes(self, viewItems: list) -> None: def update_measure_boxes(self, topLefts: list, boxSize: float) -> None:
self._remove_measure_boxes() self._remove_measure_boxes()
for item in viewItems: offset = self.filter.circleOffset
self.measuringBoxes.append(item) for x, y in topLefts:
self.scene().addItem(item) newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x+offset[0], y+offset[1], boxSize, boxSize)
self.measuringBoxes.append(newBox)
self.scene().addItem(newBox)
self._update_measured_contours()
def _remove_measure_boxes(self) -> None: def _remove_measure_boxes(self) -> None:
for item in self.measuringBoxes: for item in self.measuringBoxes:
self.scene().removeItem(item) self.scene().removeItem(item)
self.measuringBoxes = [] self.measuringBoxes = []
def load_and_update_from_dataset(self, fname: str) -> None:
self.dataset = dataset.loadData(fname)
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
offsetx = helpers.convert_length_to_pixels(self.dataset, offset[0])
offsety = helpers.convert_length_to_pixels(self.dataset, offset[1])
diameter = helpers.convert_length_to_pixels(self.dataset, diameter)
width = helpers.convert_length_to_pixels(self.dataset, widthHeight[0])
height = helpers.convert_length_to_pixels(self.dataset, widthHeight[1])
self.filter.update_filterSize(width, height, diameter, (offsetx, offsety))
self._update_particle_contours()
self._fit_to_window()
def _update_particle_contours(self) -> None:
self._remove_particle_contours()
if self.dataset is not None:
for particle in self.dataset.particleContainer.particles:
newContour: ParticleContour = ParticleContour(particle.contour)
self.scene().addItem(newContour)
self.contourItems.append(newContour)
def _remove_particle_contours(self) -> None:
for cntItem in self.contourItems:
self.scene().removeItem(cntItem)
self.contourItems = []
def _update_measured_contours(self) -> None:
# offset = self.filter.circleOffset
# offset = (-offset[0], -offset[1])
offset = (0, 0)
for contourItem in self.contourItems:
contourItem.isMeasured = False
for measBox in self.measuringBoxes:
topLeftXY = (measBox.posX, measBox.posY)
boxWidthHeight = (measBox.width, measBox.height)
if helpers.box_overlaps_contour(topLeftXY, boxWidthHeight, contourItem.polygon, offset=offset):
contourItem.isMeasured = True
contourItem.update()
break
def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
factor: float = 1.01 ** (event.angleDelta().y() / 8)
newScale: float = self.filter.scale() * factor
self.scale(newScale, newScale)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
self.drag = event.pos()
def mouseMoveEvent(self, event):
if self.drag is not None:
p0 = event.pos()
move = self.drag - p0
self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value())
self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value())
self.drag = p0
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
self.drag = None
def _fit_to_window(self) -> None:
brect = self.scene().itemsBoundingRect()
self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio)
class FilterGraphItem(QtWidgets.QGraphicsItem): class FilterGraphItem(QtWidgets.QGraphicsItem):
""" """
The Graphical Representation of the actual filter The Graphical Representation of the actual filter
""" """
def __init__(self, filterWidth: float = 500, filterHeight: float = 500): def __init__(self, filterWidth: float = 500, filterHeight: float = 500, diameter: float = 500):
super(FilterGraphItem, self).__init__() super(FilterGraphItem, self).__init__()
self.width = filterWidth self.width: float = filterWidth
self.height = filterHeight self.height: float = filterHeight
self.diameter: float = diameter
self.circleOffset: tuple = (0, 0)
self.setPos(0, 0) self.setPos(0, 0)
self.rect = QtCore.QRectF(0, 0, self.width, self.height) self.rect = QtCore.QRectF(0, 0, self.width, self.height)
self.circleRect = QtCore.QRectF(self.circleOffset[0], self.circleOffset[1], self.diameter, self.diameter)
def boundingRect(self) -> QtCore.QRectF: def boundingRect(self) -> QtCore.QRectF:
return self.rect return self.rect
def update_filterSize(self, width: float, height: float, diameter: float, offset: tuple) -> None:
self.width = width
self.height = height
self.rect = QtCore.QRectF(0, 0, self.width, self.height)
self.diameter = diameter
self.circleOffset = offset
self.circleRect = QtCore.QRectF(self.circleOffset[0], self.circleOffset[1], self.diameter, self.diameter)
def paint(self, painter: QtGui.QPainter, option, widget) -> None: def paint(self, painter: QtGui.QPainter, option, widget) -> None:
painter.setPen(QtCore.Qt.black) painter.setPen(QtCore.Qt.black)
painter.setBrush(QtCore.Qt.white) painter.setBrush(QtCore.Qt.white)
...@@ -52,15 +146,16 @@ class FilterGraphItem(QtWidgets.QGraphicsItem): ...@@ -52,15 +146,16 @@ class FilterGraphItem(QtWidgets.QGraphicsItem):
painter.setPen(QtCore.Qt.darkGray) painter.setPen(QtCore.Qt.darkGray)
painter.setBrush(QtCore.Qt.lightGray) painter.setBrush(QtCore.Qt.lightGray)
painter.drawEllipse(self.rect) painter.drawEllipse(self.circleRect)
class MeasureBoxGraphItem(QtWidgets.QGraphicsItem): class MeasureBoxGraphItem(QtWidgets.QGraphicsItem):
""" """
Displays a box in which particles will be measured Displays a box in which particles will be measured
""" """
def __init__(self, posX:float=50, posY:float=50, width:float=50, height:float=50) -> None: def __init__(self, posX: float = 50, posY: float = 50, width: float = 50, height: float = 50) -> None:
super(MeasureBoxGraphItem, self).__init__() super(MeasureBoxGraphItem, self).__init__()
self.setZValue(5)
self.posX: float = posX self.posX: float = posX
self.posY: float = posY self.posY: float = posY
self.height: float = height self.height: float = height
...@@ -74,14 +169,49 @@ class MeasureBoxGraphItem(QtWidgets.QGraphicsItem): ...@@ -74,14 +169,49 @@ class MeasureBoxGraphItem(QtWidgets.QGraphicsItem):
return self.rect return self.rect
def paint(self, painter, option, widget) -> None: def paint(self, painter, option, widget) -> None:
painter.setBrush(QtCore.Qt.green) painter.setBrush(QtGui.QColor(0, 255, 0, 180))
painter.setPen(QtCore.Qt.darkGreen) painter.setPen(QtCore.Qt.darkGreen)
painter.drawRects(self.rect) painter.drawRects(self.rect)
if __name__ == '__main__': class ParticleContour(QtWidgets.QGraphicsItem):
import sys def __init__(self, contourData, pos=(0, 0)) -> None:
app = QtWidgets.QApplication(sys.argv) super(ParticleContour, self).__init__()
filterView = FilterView() self.setZValue(1)
filterView.show() self.setPos(pos[0], pos[1])
ret = app.exec_() self.brect: QtCore.QRectF = QtCore.QRectF(0, 0, 1, 1)
\ No newline at end of file
self.isMeasured: bool = False # Wether the particle overlaps with a measuring box or nt
self.contourData = contourData
self.polygon = None
self.getBrectAndPolygon()
def getBrectAndPolygon(self) -> None:
"""
Calculates the bounding rect (needed for drawing the QGraphicsView) and converts the contourdata to a polygon.
:return:
"""
self.polygon = QtGui.QPolygonF()
x0 = self.contourData[:, 0, 0].min()
x1 = self.contourData[:, 0, 0].max()
y0 = self.contourData[:, 0, 1].min()
y1 = self.contourData[:, 0, 1].max()
for point in self.contourData:
self.polygon.append(QtCore.QPointF(point[0, 0], point[0, 1]))
self.brect.setCoords(x0, y0, x1, y1)
def boundingRect(self) -> QtCore.QRectF:
return self.brect
def paint(self, painter, option, widget) -> None:
if self.polygon is not None:
if self.isMeasured:
painter.setPen(QtCore.Qt.darkRed)
painter.setBrush(QtCore.Qt.red)
else:
painter.setPen(QtCore.Qt.darkCyan)
painter.setBrush(QtCore.Qt.cyan)
painter.drawPolygon(self.polygon)
...@@ -17,10 +17,14 @@ class MainView(QtWidgets.QWidget): ...@@ -17,10 +17,14 @@ class MainView(QtWidgets.QWidget):
self.activeMode: MeasureMode = None self.activeMode: MeasureMode = None
self.activeModeControl: QtWidgets.QGroupBox = QtWidgets.QGroupBox() self.activeModeControl: QtWidgets.QGroupBox = QtWidgets.QGroupBox()
loadDsetBtn = QtWidgets.QPushButton('Load Dataset')
loadDsetBtn.released.connect(self._load_dataset)
self.controlGroup = QtWidgets.QGroupBox() self.controlGroup = QtWidgets.QGroupBox()
self.controlGroupLayout = QtWidgets.QHBoxLayout() self.controlGroupLayout = QtWidgets.QHBoxLayout()
self.controlGroup.setLayout(self.controlGroupLayout) self.controlGroup.setLayout(self.controlGroupLayout)
self.controlGroupLayout.addWidget(loadDsetBtn)
self.controlGroupLayout.addWidget(QtWidgets.QLabel('Select Subsampling Mode:')) self.controlGroupLayout.addWidget(QtWidgets.QLabel('Select Subsampling Mode:'))
self.controlGroupLayout.addWidget(self.modeSelector) self.controlGroupLayout.addWidget(self.modeSelector)
self.controlGroupLayout.addWidget(self.activeModeControl) self.controlGroupLayout.addWidget(self.activeModeControl)
...@@ -57,6 +61,12 @@ class MainView(QtWidgets.QWidget): ...@@ -57,6 +61,12 @@ class MainView(QtWidgets.QWidget):
self.activeMode.update_measure_viewItems() self.activeMode.update_measure_viewItems()
def _load_dataset(self) -> None:
fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Select .pkl file', filter='pkl file (*.pkl)')
if fname[0] != '':
self.filterView.load_and_update_from_dataset(fname[0])
self.activeMode.update_measure_viewItems()
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
......
...@@ -24,8 +24,7 @@ class CrossBoxMode(MeasureMode): ...@@ -24,8 +24,7 @@ class CrossBoxMode(MeasureMode):
self.update_measure_viewItems() self.update_measure_viewItems()
def update_measure_viewItems(self) -> None: def update_measure_viewItems(self) -> None:
self.crossBoxGenerator.filterHeight = self.filterView.filter.height self.crossBoxGenerator.filterDiameter = self.filterView.filter.diameter
self.crossBoxGenerator.filterWidth = self.filterView.filter.width
self.crossBoxGenerator.numBoxesAcross = int(self.uiControls.numBoxesSelector.currentText()) self.crossBoxGenerator.numBoxesAcross = int(self.uiControls.numBoxesSelector.currentText())
desiredCoverage: int = self.uiControls.coverageSpinbox.value() desiredCoverage: int = self.uiControls.coverageSpinbox.value()
...@@ -36,14 +35,9 @@ class CrossBoxMode(MeasureMode): ...@@ -36,14 +35,9 @@ class CrossBoxMode(MeasureMode):
self.crossBoxGenerator.fraction = desiredCoverage / 100 self.crossBoxGenerator.fraction = desiredCoverage / 100
viewItems = []
topLefts: list = self.crossBoxGenerator.get_topLeft_of_boxes() topLefts: list = self.crossBoxGenerator.get_topLeft_of_boxes()
boxSize = self.crossBoxGenerator.boxSize boxSize = self.crossBoxGenerator.boxSize
for (x, y) in topLefts: self.filterView.update_measure_boxes(topLefts, boxSize)
newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize)
viewItems.append(newBox)
self.filterView.update_measure_boxes(viewItems)
class CrossBoxesControls(QtWidgets.QGroupBox): class CrossBoxesControls(QtWidgets.QGroupBox):
...@@ -78,7 +72,7 @@ class CrossBoxesControls(QtWidgets.QGroupBox): ...@@ -78,7 +72,7 @@ class CrossBoxesControls(QtWidgets.QGroupBox):
def _config_changed(self): def _config_changed(self):
self.measureModeParent.update_measure_viewItems() self.measureModeParent.update_measure_viewItems()
def set_to_max_possible_coverage(self, maxCoverage:int): def set_to_max_possible_coverage(self, maxCoverage: int):
self.coverageSpinbox.setMaximum(maxCoverage) self.coverageSpinbox.setMaximum(maxCoverage)
if maxCoverage < self.coverageSpinbox.value(): if maxCoverage < self.coverageSpinbox.value():
self.coverageSpinbox.valueChanged.disconnect() self.coverageSpinbox.valueChanged.disconnect()
...@@ -94,20 +88,19 @@ class SpiralBoxMode(MeasureMode): ...@@ -94,20 +88,19 @@ class SpiralBoxMode(MeasureMode):
self.update_measure_viewItems() self.update_measure_viewItems()
def update_measure_viewItems(self) -> None: def update_measure_viewItems(self) -> None:
self.spiralBoxGenerator.filterHeight = self.filterView.filter.height self.spiralBoxGenerator.filterDiameter = self.filterView.filter.diameter
self.spiralBoxGenerator.filterWidth = self.filterView.filter.width
self.spiralBoxGenerator.boxSize = self.uiControls.boxSizeSpinbox.value() self.spiralBoxGenerator.boxSize = self.uiControls.boxSizeSpinbox.value()
# minBoxSize: float = self.spiralBoxGenerator.filterDiameter*0.1
# if self.spiralBoxGenerator.boxSize < minBoxSize:
# self.spiralBoxGenerator.boxSize = minBoxSize
# self.uiControls.boxSizeSpinbox.setValue(int(round(minBoxSize)))
self.spiralBoxGenerator.numBoxes = self.uiControls.numBoxesSpinbox.value() self.spiralBoxGenerator.numBoxes = self.uiControls.numBoxesSpinbox.value()
viewItems = []
topLefts: list = self.spiralBoxGenerator.get_topLeft_of_boxes() topLefts: list = self.spiralBoxGenerator.get_topLeft_of_boxes()
boxSize = self.spiralBoxGenerator.boxSize boxSize = self.spiralBoxGenerator.boxSize
for (x, y) in topLefts: self.filterView.update_measure_boxes(topLefts, boxSize)
newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize)
viewItems.append(newBox)
self.filterView.update_measure_boxes(viewItems)
class SpiralBoxControls(QtWidgets.QGroupBox): class SpiralBoxControls(QtWidgets.QGroupBox):
...@@ -125,7 +118,7 @@ class SpiralBoxControls(QtWidgets.QGroupBox): ...@@ -125,7 +118,7 @@ class SpiralBoxControls(QtWidgets.QGroupBox):
layout.addWidget(QtWidgets.QLabel('Box Size:')) layout.addWidget(QtWidgets.QLabel('Box Size:'))
self.boxSizeSpinbox = QtWidgets.QSpinBox() self.boxSizeSpinbox = QtWidgets.QSpinBox()
self.boxSizeSpinbox.setValue(50) self.boxSizeSpinbox.setValue(50)
self.boxSizeSpinbox.setMaximum(1000) self.boxSizeSpinbox.setMaximum(10000)
self.boxSizeSpinbox.valueChanged.connect(self._config_changed) self.boxSizeSpinbox.valueChanged.connect(self._config_changed)
layout.addWidget(self.boxSizeSpinbox) layout.addWidget(self.boxSizeSpinbox)
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Jan 28 19:32:50 2020
@author: luna
"""
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
import numpy as np import numpy as np
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
class ParticleBinSorter(object): class ParticleBinSorter(object):
...@@ -36,15 +32,108 @@ class ParticleBinSorter(object): ...@@ -36,15 +32,108 @@ class ParticleBinSorter(object):
return binIndex return binIndex
def box_contains_contour(boxXY: tuple, boxWidthHeight: tuple, contour: np.array) -> bool: def box_overlaps_contour(boxTopLeftXY: tuple, boxWidthHeight: tuple, contour, offset: tuple = (0, 0)) -> bool:
"""
Calculates, if a contour is overlapping a box.
:param boxTopLeftXY: topLeft of Box
:param boxWidthHeight: Width and height of box
:param contour: np.ndarrayof contour data
:param offset: optional offset (x, y) of the box (i.e., the (0, 0) of the contours coord system does not match
the (0, 0) of the box coord system.
:return:
"""
contourPolygon = QtGui.QPolygonF() contourPolygon = QtGui.QPolygonF()
for point in contour: if type(contour) == np.ndarray:
contourPolygon.append(QtCore.QPointF(point[0, 0], point[0, 1])) for point in contour:
contourPolygon.append(QtCore.QPointF(point[0, 0], point[0, 1]))
elif type(contour) == QtGui.QPolygonF:
contourPolygon = contour
else:
raise TypeError
boxPolygon = QtGui.QPolygonF() boxPolygon = QtGui.QPolygonF()
boxPolygon.append(QtCore.QPointF(boxXY[0], boxXY[1])) boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0], boxTopLeftXY[1]+offset[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0], boxXY[1] + boxWidthHeight[1])) boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0], boxTopLeftXY[1] + boxWidthHeight[1]+offset[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0] + boxWidthHeight[0], boxXY[1])) boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0] + boxWidthHeight[0], boxTopLeftXY[1]+offset[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0] + boxWidthHeight[0], boxXY[1] + boxWidthHeight[1])) boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0] + boxWidthHeight[0],
boxTopLeftXY[1] + boxWidthHeight[1]+offset[1]))
isOverlapping: bool = boxPolygon.intersects(contourPolygon)
if not isOverlapping:
# sometimes, the polygon.intersects method does not capture everything... We test the brects therefore..
polygonBrect: QtCore.QRectF = contourPolygon.boundingRect()
boxBrect: QtCore.QRectF = boxPolygon.boundingRect()
if boxBrect.contains(polygonBrect) or boxBrect.intersects(polygonBrect):
isOverlapping = True
return isOverlapping
def get_overlapping_fraction(polygon1: QtGui.QPolygonF, polygon2: QtGui.QPolygonF) -> float:
"""
Takes two polygons and returns the overlapping fraction (in terms of area)
:param polygon1: The polygon that the fraction shall be calculated of.
:param polygon2: The overlapping polygon, which's size is not of interest
:return:
"""
overlap: float = 0
overlapPoly: QtGui.QPolygonF = polygon1.intersected(polygon2)
if overlapPoly.size() > 0:
origSize: float = get_polygon_area(polygon1)
overlapSize: float = get_polygon_area(overlapPoly)
overlap = overlapSize/origSize
return overlap
def get_polygon_area(polygon: QtGui.QPolygonF) -> float:
"""
Calculates the area of a polygon, adapted from:
https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates
:param polygon:
:return: area
"""
x: list = []
y: list = []
for index in range(polygon.size()):
point: QtCore.QPointF = polygon.at(index)
x.append(point.x())
y.append(point.y())
x: np.ndarray = np.array(x)
y: np.ndarray = np.array(y)
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
return area
def get_filterDimensions_from_dataset(dataset) -> tuple:
"""
Processes the datasets boundary items to calculate diameter and offset (coord system offset of circular filter
with respect to actual dataset). This is used to set circular filter dimensions to use in the geometric
subsampling approaches.
The return values are in micrometer dimensions.
:param dataset: The dataset to read.
:return: (radius, offset, widthHeight) in µm
"""
maxDim = dataset.maxdim
imgDim = dataset.imagedim_df if dataset.imagescanMode == 'df' else dataset.imagedim_bf
minX, maxY, = maxDim[0] - imgDim[0] / 2, maxDim[1] + imgDim[1] / 2
maxX, minY = maxDim[2] + imgDim[0] / 2, maxDim[3] - imgDim[1] / 2
width = maxX - minX
height = maxY - minY
diameter: float = min([width, height])
offset: tuple = (width - diameter)/2, (height-diameter)/2
return offset, diameter, [width, height]
return contourPolygon.intersects(boxPolygon) def convert_length_to_pixels(dataset: dataset.DataSet, length: float) -> float:
"""
:param dataset: dataset to use for conversion
:param length: length in µm
:return: length in px
"""
imgMode: str = dataset.imagescanMode
pixelScale: float = (dataset.pixelscale_df if imgMode == 'df' else dataset.pixelscale_bf)
length /= pixelScale
return length
...@@ -26,28 +26,6 @@ class SubsamplingMethod(object): ...@@ -26,28 +26,6 @@ class SubsamplingMethod(object):
raise NotImplementedError raise NotImplementedError
class BoxSelectionSubsamplingMethod(SubsamplingMethod):
def __init__(self, *args):
super(BoxSelectionSubsamplingMethod, self).__init__(*args)
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_contains_contour(topLeft, (boxSize, boxSize), particle.contour):
subParticles.append(particle)
return self.fraction, subParticles
def get_topLeft_of_boxes(self) -> list:
raise NotImplementedError
def get_parameterCombos_for_fraction(self) -> list:
raise NotImplementedError
class RandomSampling(SubsamplingMethod): class RandomSampling(SubsamplingMethod):
def apply_subsampling_method(self): def apply_subsampling_method(self):
numOrigParticles = len(self.particleContainer.particles) numOrigParticles = len(self.particleContainer.particles)
......
...@@ -6,6 +6,7 @@ sys.path.append("C://Users//xbrjos//Desktop//Python") ...@@ -6,6 +6,7 @@ sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset from gepard import dataset
from methods import IvlevaSubsampling, RandomSampling, SizeBinFractioning from methods import IvlevaSubsampling, RandomSampling, SizeBinFractioning
from geometricMethods import BoxSelectionCreator
from helpers import ParticleBinSorter from helpers import ParticleBinSorter
from evaluation import ResultComparer from evaluation import ResultComparer
...@@ -19,58 +20,63 @@ fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190313_Soil_5_A_50_5_1_50_1\19031 ...@@ -19,58 +20,63 @@ fname: str = r'C:\Users\xbrjos\Desktop\temp MP\190313_Soil_5_A_50_5_1_50_1\19031
dset = dataset.loadData(fname) dset = dataset.loadData(fname)
print('loaded dataset') print('loaded dataset')
pc = dset.particleContainer boxCreator = BoxSelectionCreator(dset)
origParticles = pc.particles center, size = boxCreator.get_filterDimensions_from_dataset()
print(center, size)
print(dset.mapToPixel(center, force=True))
resultComparer = ResultComparer() # pc = dset.particleContainer
numOrigMP = resultComparer._get_number_of_MP_particles(origParticles) # origParticles = pc.particles
print(f'orig particles: {len(origParticles)}, of which are mp: {numOrigMP}')
# ivlevaSampling = IvlevaSubsampling(pc)
# ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method()
t0 = time.time() # resultComparer = ResultComparer()
fractions = np.arange(0.05, .55, 0.05) # numOrigMP = resultComparer._get_number_of_MP_particles(origParticles)
errors = [] # print(f'orig particles: {len(origParticles)}, of which are mp: {numOrigMP}')
binErrors = [] # # ivlevaSampling = IvlevaSubsampling(pc)
numIterations = 1000 # # ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method()
for fraction in fractions: # t0 = time.time()
print('random sampling, fraction:', fraction) # fractions = np.arange(0.05, .55, 0.05)
# randomSampling = RandomSampling(pc, desiredFraction=fraction) # errors = []
randomSampling = SizeBinFractioning(pc, fraction) # binErrors = []
iterErrors = [] # numIterations = 1000
binIterErrors = []
for _ in range(numIterations):
randomFraction, randomParticles = randomSampling.apply_subsampling_method()
iterErrors.append(resultComparer._get_mp_count_error(origParticles, randomParticles, randomFraction))
bins, errorsPerBin = resultComparer._get_mp_count_error_per_bin(origParticles, randomParticles, randomFraction)
binIterErrors.append(errorsPerBin)
errors.append(round(np.mean(iterErrors)*100)) #from fraction to % # for fraction in fractions:
fractionBinErrors = [] # print('random sampling, fraction:', fraction)
for binIndex in range(len(bins)+1): # # randomSampling = RandomSampling(pc, desiredFraction=fraction)
binError = round(np.mean([binIterErrors[i][binIndex] for i in range(numIterations)]) * 100) # randomSampling = SizeBinFractioning(pc, fraction)
fractionBinErrors.append(binError) # iterErrors = []
binErrors.append(fractionBinErrors) # binIterErrors = []
# for _ in range(numIterations):
# randomFraction, randomParticles = randomSampling.apply_subsampling_method()
# iterErrors.append(resultComparer._get_mp_count_error(origParticles, randomParticles, randomFraction))
# bins, errorsPerBin = resultComparer._get_mp_count_error_per_bin(origParticles, randomParticles, randomFraction)
# binIterErrors.append(errorsPerBin)
print('random sampling took', np.round(time.time()-t0, 2), 'seonds') # errors.append(round(np.mean(iterErrors)*100)) #from fraction to %
binLowerLimits = bins.copy() # fractionBinErrors = []
binLowerLimits.insert(0, 0) # for binIndex in range(len(bins)+1):
plt.subplot(121) # binError = round(np.mean([binIterErrors[i][binIndex] for i in range(numIterations)]) * 100)
plt.plot(fractions, errors) # fractionBinErrors.append(binError)
# plt.title(f'Random Sampling, averaged from {numIterations} trials, orig particle count: {len(origParticles)}') # binErrors.append(fractionBinErrors)
plt.xlabel('Fraction measured')
plt.ylabel('Average error in MP particle count (%)')
plt.subplot(122) # print('random sampling took', np.round(time.time()-t0, 2), 'seonds')
for fracMeas, curBinErrors in zip(fractions, binErrors): # binLowerLimits = bins.copy()
plt.plot(binLowerLimits, curBinErrors, label=np.round(fracMeas, 1)) # binLowerLimits.insert(0, 0)
# plt.title('Error in MP count (%) per size bin') # plt.subplot(121)
plt.xlabel('particle size') # plt.plot(fractions, errors)
plt.ylabel('Average error in MP particle count (%)') # # plt.title(f'Random Sampling, averaged from {numIterations} trials, orig particle count: {len(origParticles)}')
plt.legend() # plt.xlabel('Fraction measured')
plt.show() # plt.ylabel('Average error in MP particle count (%)')
# plt.subplot(122)
# for fracMeas, curBinErrors in zip(fractions, binErrors):
# plt.plot(binLowerLimits, curBinErrors, label=np.round(fracMeas, 1))
# # plt.title('Error in MP count (%) per size bin')
# plt.xlabel('particle size')
# plt.ylabel('Average error in MP particle count (%)')
# plt.legend()
# plt.show()
# sizeBinSampling = SizeBinFractioning(pc)
# sizeBinParticles = sizeBinSampling.apply_subsampling_method() # # sizeBinSampling = SizeBinFractioning(pc)
\ No newline at end of file # # sizeBinParticles = sizeBinSampling.apply_subsampling_method()
\ No newline at end of file
import sys
import unittest import unittest
import numpy as np import numpy as np
from geometricMethods import CrossBoxSelector, SpiralSelector from geometricMethods import CrossBoxSelector, SpiralSelector, BoxSelectionCreator
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
class TestSelectCrossBoxes(unittest.TestCase): class TestSelectCrossBoxes(unittest.TestCase):
...@@ -8,7 +12,7 @@ class TestSelectCrossBoxes(unittest.TestCase): ...@@ -8,7 +12,7 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.crossBoxSelector = CrossBoxSelector(None) self.crossBoxSelector = CrossBoxSelector(None)
def test_get_topLeft_of_boxes(self): def test_get_topLeft_of_boxes(self):
self.crossBoxSelector.filterWidth = self.crossBoxSelector.filterHeight = 100 self.crossBoxSelector.filterDiameter = 100
self.crossBoxSelector.fraction = 0.1 self.crossBoxSelector.fraction = 0.1
self.crossBoxSelector.numBoxesAcross = 3 self.crossBoxSelector.numBoxesAcross = 3
...@@ -20,9 +24,9 @@ class TestSelectCrossBoxes(unittest.TestCase): ...@@ -20,9 +24,9 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(len(topLeftCorners), 9) self.assertEqual(len(topLeftCorners), 9)
def test_get_tile_topLefts(self): def test_get_tile_topLefts(self):
self.crossBoxSelector.filterHeight = self.crossBoxSelector.filterWidth = 100 self.crossBoxSelector.filterDiameter = 100
self.crossBoxSelector.numBoxesAcross = 3 self.crossBoxSelector.numBoxesAcross = 3
maxBoxSize: float = self.crossBoxSelector.filterWidth/self.crossBoxSelector.numBoxesAcross maxBoxSize: float = self.crossBoxSelector.filterDiameter/self.crossBoxSelector.numBoxesAcross
horizontalTileStarts: list = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize) horizontalTileStarts: list = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize)
verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize) verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize)
self.assertEqual(horizontalTileStarts, [0, 100 / 3, 2 * 100 / 3]) self.assertEqual(horizontalTileStarts, [0, 100 / 3, 2 * 100 / 3])
...@@ -36,7 +40,7 @@ class TestSelectCrossBoxes(unittest.TestCase): ...@@ -36,7 +40,7 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(horizontalTileStarts, verticalTileStarts) self.assertEqual(horizontalTileStarts, verticalTileStarts)
self.crossBoxSelector.numBoxesAcross = 5 self.crossBoxSelector.numBoxesAcross = 5
maxBoxSize = self.crossBoxSelector.filterWidth / self.crossBoxSelector.numBoxesAcross maxBoxSize = self.crossBoxSelector.filterDiameter / self.crossBoxSelector.numBoxesAcross
horizontalTileStarts = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize) horizontalTileStarts = self.crossBoxSelector._get_horizontal_box_starts(maxBoxSize)
verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize) verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize)
self.assertEqual(horizontalTileStarts, [0, 100/5, 2*100/5, 3*100/5, 4*100/5]) self.assertEqual(horizontalTileStarts, [0, 100/5, 2*100/5, 3*100/5, 4*100/5])
...@@ -51,11 +55,11 @@ class TestSelectCrossBoxes(unittest.TestCase): ...@@ -51,11 +55,11 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(horizontalTileStarts, verticalTileStarts) self.assertEqual(horizontalTileStarts, verticalTileStarts)
def test_get_box_size(self) -> None: def test_get_box_size(self) -> None:
self.crossBoxSelector.filterHeight = self.crossBoxSelector.filterWidth = 100 # in pixel self.crossBoxSelector.filterDiameter = 100 # in pixel
self.crossBoxSelector.fraction = 0.1 self.crossBoxSelector.fraction = 0.1
self.crossBoxSelector.numBoxesAcross = 3 self.crossBoxSelector.numBoxesAcross = 3
filterArea: float = self.crossBoxSelector.filterHeight * self.crossBoxSelector.filterWidth filterArea: float = np.pi * (self.crossBoxSelector.filterDiameter/2)**2
totalBoxArea: float = filterArea*self.crossBoxSelector.fraction totalBoxArea: float = filterArea*self.crossBoxSelector.fraction
numBoxes: int = 2*self.crossBoxSelector.numBoxesAcross - 1 numBoxes: int = 2*self.crossBoxSelector.numBoxesAcross - 1
...@@ -71,7 +75,7 @@ class TestSelectSpiralBoxes(unittest.TestCase): ...@@ -71,7 +75,7 @@ class TestSelectSpiralBoxes(unittest.TestCase):
def test_move_and_scale_toplefts(self): def test_move_and_scale_toplefts(self):
self.spiralBoxSelector.filterHeight = 100 self.spiralBoxSelector.filterHeight = 100
self.spiralBoxSelector.filterWidth = 100 self.spiralBoxSelector.filterDiameter = 100
self.spiralBoxSelector.boxSize = 10 self.spiralBoxSelector.boxSize = 10
topLefts = [(45, 45), (0, 45), (90, 45)] topLefts = [(45, 45), (0, 45), (90, 45)]
...@@ -118,3 +122,38 @@ class TestSelectSpiralBoxes(unittest.TestCase): ...@@ -118,3 +122,38 @@ class TestSelectSpiralBoxes(unittest.TestCase):
topLefts: list = [(-2, 0), (5, 9)] topLefts: list = [(-2, 0), (5, 9)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts) overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
self.assertEqual(overlaps, True) self.assertEqual(overlaps, True)
class TestBoxCreator(unittest.TestCase):
def setUp(self) -> None:
self.dataset: dataset.DataSet = dataset.DataSet('test')
self.boxCreator: BoxSelectionCreator = BoxSelectionCreator(self.dataset)
# def test_get_crossBoxSelectors_for_fraction(self):
# def getBoxSize(numBoxes):
# totalBoxArea: float = filterArea * desiredFraction
# areaPerBox: float = totalBoxArea / numBoxes
# boxSize: float = areaPerBox ** 0.5
# return boxSize
#
# self.dataset.lastpos = [0, 0]
# self.dataset.boundary = np.array([[0, 0], [0, 10], [10, 0], [10, 10]])
# self.dataset.imagescanMode = 'df'
# self.dataset.pixelscale_df = 1.0
# self.dataset.imagedim_df = [10, 10]
#
# filterArea: float = np.pi * 5**2
#
# desiredFraction: float = 0.1
# expectedResults: dict = {} # key: numBoxesAcross, value: boxSize
# for numBoxesAcross in [3, 5]:
# expectedResults[numBoxesAcross] = getBoxSize(2*numBoxesAcross - 1)
#
# crossBoxSelectors: list = self.boxCreator.get_crossBoxSelectors_for_fraction(desiredFraction)
# self.assertEqual(len(crossBoxSelectors), 2)
# for boxSelector in crossBoxSelectors:
# self.assertEqual(expectedResults[boxSelector.numBoxesAcross], boxSelector.boxSize)
...@@ -7,17 +7,19 @@ Created on Tue Jan 28 19:35:04 2020 ...@@ -7,17 +7,19 @@ Created on Tue Jan 28 19:35:04 2020
""" """
import unittest import unittest
from helpers import ParticleBinSorter, box_contains_contour import helpers
import numpy as np import numpy as np
import sys import sys
sys.path.append("C://Users//xbrjos//Desktop//Python") sys.path.append("C://Users//xbrjos//Desktop//Python")
from PyQt5 import QtCore, QtGui
import gepard import gepard
from gepard.analysis.particleAndMeasurement import Particle from gepard.analysis.particleAndMeasurement import Particle
from gepard import dataset
class TestBinSorter(unittest.TestCase): class TestBinSorter(unittest.TestCase):
def setUp(self): def setUp(self):
self.sorter = ParticleBinSorter() self.sorter = helpers.ParticleBinSorter()
self.bins = self.sorter.bins self.bins = self.sorter.bins
def test_sort_particles_into_bins(self): def test_sort_particles_into_bins(self):
...@@ -66,15 +68,126 @@ class TestBinSorter(unittest.TestCase): ...@@ -66,15 +68,126 @@ class TestBinSorter(unittest.TestCase):
class TestOther(unittest.TestCase): class TestOther(unittest.TestCase):
def test_box_contains_contour(self): def test_box_overlaps_contour(self):
boxXY: tuple = 0, 0 boxXY: tuple = 0, 0
boxWidthHeight: tuple = 10, 10 boxWidthHeight: tuple = 10, 10
contourPoints = np.array([[[0, 0]], [[5, 5]], [[3, 3]]]) # fully enclosed contourPoints = np.array([[[0, 0]], [[5, 5]], [[3, 3]]]) # fully enclosed
self.assertTrue(box_contains_contour(boxXY, boxWidthHeight, contourPoints)) self.assertTrue(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints))
contourPoints = np.array([[[1, 1]], [[5, 5]], [[3, 3]]]) # fully enclosed
self.assertTrue(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints))
contourPoints = np.array([[[-5, -5]], [[0, 5]], [[-5, -10]]]) # only one point touches border contourPoints = np.array([[[-5, -5]], [[0, 5]], [[-5, -10]]]) # only one point touches border
self.assertTrue(box_contains_contour(boxXY, boxWidthHeight, contourPoints)) self.assertTrue(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints))
offset: tuple = (1, 0) # now it does not touch it anymore
self.assertFalse(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints, offset))
contourPoints = np.array([[[-5, -5]], [[-1, 5]], [[-5, -10]]]) # outside the box contourPoints = np.array([[[-5, -5]], [[-1, 5]], [[-5, -10]]]) # outside the box
self.assertFalse(box_contains_contour(boxXY, boxWidthHeight, contourPoints)) self.assertFalse(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints))
offset = (-5, -5) # now it overlaps
self.assertTrue(helpers.box_overlaps_contour(boxXY, boxWidthHeight, contourPoints, offset))
def test_get_overlapping_fraction(self):
polygon1: QtGui.QPolygonF = QtGui.QPolygonF()
polygon1.append(QtCore.QPointF(0, 0))
polygon1.append(QtCore.QPointF(10, 0))
polygon1.append(QtCore.QPointF(10, 10))
polygon1.append(QtCore.QPointF(0, 10))
polygon2: QtGui.QPolygonF = QtGui.QPolygonF() # shifted +5 in x, so half of it should overlap
polygon2.append(QtCore.QPointF(5, 0))
polygon2.append(QtCore.QPointF(15, 0))
polygon2.append(QtCore.QPointF(15, 10))
polygon2.append(QtCore.QPointF(5, 10))
self.assertEqual(helpers.get_overlapping_fraction(polygon1, polygon2), 0.5)
polygon2: QtGui.QPolygonF = QtGui.QPolygonF() # made the second poly much larger, this shouldn't have an impact
polygon2.append(QtCore.QPointF(5, 0))
polygon2.append(QtCore.QPointF(100, 0))
polygon2.append(QtCore.QPointF(100, 100))
polygon2.append(QtCore.QPointF(5, 100))
self.assertEqual(helpers.get_overlapping_fraction(polygon1, polygon2), 0.5)
polygon2: QtGui.QPolygonF = QtGui.QPolygonF() # shifted another +5 in x, it should not overlap at all
polygon2.append(QtCore.QPointF(15, 0))
polygon2.append(QtCore.QPointF(15, 10))
polygon2.append(QtCore.QPointF(25, 10))
polygon2.append(QtCore.QPointF(25, 0))
self.assertEqual(helpers.get_overlapping_fraction(polygon1, polygon2), 0)
polygon2: QtGui.QPolygonF = QtGui.QPolygonF() # not the second poly completely encloses the first one
polygon2.append(QtCore.QPointF(-100, -100))
polygon2.append(QtCore.QPointF(100, -100))
polygon2.append(QtCore.QPointF(100, 100))
polygon2.append(QtCore.QPointF(-100, 100))
self.assertEqual(helpers.get_overlapping_fraction(polygon1, polygon2), 1.0)
def test_get_polygon_area(self):
polygon1: QtGui.QPolygonF = QtGui.QPolygonF()
polygon1.append(QtCore.QPointF(0, 0))
polygon1.append(QtCore.QPointF(10, 0))
polygon1.append(QtCore.QPointF(10, 10))
polygon1.append(QtCore.QPointF(0, 10))
self.assertEqual(helpers.get_polygon_area(polygon1), 100)
polygon1: QtGui.QPolygonF = QtGui.QPolygonF()
polygon1.append(QtCore.QPointF(0, 0))
polygon1.append(QtCore.QPointF(20, 0))
polygon1.append(QtCore.QPointF(20, 10))
polygon1.append(QtCore.QPointF(0, 10))
self.assertEqual(helpers.get_polygon_area(polygon1), 200)
polygon1: QtGui.QPolygonF = QtGui.QPolygonF()
polygon1.append(QtCore.QPointF(-10, 0))
polygon1.append(QtCore.QPointF(10, 0))
polygon1.append(QtCore.QPointF(10, 10))
polygon1.append(QtCore.QPointF(-10, 10))
self.assertEqual(helpers.get_polygon_area(polygon1), 200)
class TestDatasetOperations(unittest.TestCase):
def setUp(self) -> None:
self.dataset: dataset.DataSet = dataset.DataSet('test')
self.dataset.imagescanMode = 'df'
def test_get_filtersize(self):
def setMaxDim():
self.dataset.maxdim = minX + imgdim / 2, maxY - imgdim / 2, maxX - imgdim / 2, minY + imgdim / 2
imgdim = 10
self.dataset.imagedim_df = [imgdim, imgdim]
minX, maxX, minY, maxY = 0, 10, 0, 10
setMaxDim()
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
self.assertEqual(diameter, 10)
self.assertEqual(offset, (0, 0))
self.assertEqual(widthHeight, [10, 10])
minX, maxX, minY, maxY = -10, 10, -10, 10
setMaxDim()
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
self.assertEqual(diameter, 20)
self.assertEqual(widthHeight, [20, 20])
self.assertEqual(offset, (0, 0))
minX, maxX, minY, maxY = 0, 20, 0, 10
setMaxDim()
offset, diameter, widthHeight = helpers.get_filterDimensions_from_dataset(self.dataset)
self.assertEqual(diameter, 10)
self.assertEqual(widthHeight, [20, 10])
self.assertEqual(offset, (5, 0))
def testconvert_microns_to_pixels(self):
self.dataset.lastpos = [0, 0] # in microns
self.dataset.pixelscale_bf = 0.5 # micrometer / px
self.dataset.imagedim_bf = [10, 10] # in microns
self.dataset.imagescanMode = 'bf'
diameter = 10 # in microns
widthHeight = [20, 10] # in microns
self.assertEqual(helpers.convert_length_to_pixels(self.dataset, diameter), 20)
self.assertEqual(helpers.convert_length_to_pixels(self.dataset, widthHeight[0]), 40)
self.assertEqual(helpers.convert_length_to_pixels(self.dataset, widthHeight[1]), 20)