...
 
Commits (2)
......@@ -7,6 +7,7 @@ Created on Wed Jan 22 13:57:28 2020
"""
from helpers import ParticleBinSorter
class ResultComparer(object):
def _get_mp_count_error_per_bin(self, allParticles, subParticles, fractionMeasured):
......@@ -27,7 +28,7 @@ class ResultComparer(object):
elif numMPEstimate == 0:
mpCountError = 0
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
......
import numpy as np
from itertools import combinations
from methods import BoxSelectionSubsamplingMethod
from helpers import box_contains_contour
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
class CrossBoxSelector(BoxSelectionSubsamplingMethod):
def __init__(self, particleContainer, desiredFraction: float = 0.1) -> None:
super(CrossBoxSelector, self).__init__(particleContainer, desiredFraction)
self.filterWidth: float = 1000
self.filterHeight: float = 1000
self.numBoxesAcross: int = 3 # either 3 or 5
@property
......@@ -17,7 +68,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
if maxFraction < self.fraction:
self.fraction = maxFraction
totalBoxArea: float = ((self.filterWidth*self.filterHeight)*self.fraction)
totalBoxArea: float = self.filterArea * self.fraction
boxArea: float = totalBoxArea / (2*self.numBoxesAcross - 1)
return boxArea**0.5
......@@ -39,14 +90,20 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
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
It is with respect to a circular filter, fitting in a rectangle of dimensions width*height
:return float:
"""
maxBoxWidth: float = self.filterWidth / self.numBoxesAcross
maxBoxHeight: float = self.filterHeight / self.numBoxesAcross
alpha: float = np.deg2rad(135)
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
totalBoxArea: float = numBoxes * (maxBoxWidth * maxBoxHeight)
return totalBoxArea / (self.filterHeight * self.filterWidth)
totalBoxArea: float = numBoxes * (maxBoxSize**2)
maxFraction: float = totalBoxArea / self.filterArea
return maxFraction
def _get_horizontal_box_starts(self, boxSize: float) -> list:
"""
......@@ -54,7 +111,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
:param boxSize:
: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:
"""
......@@ -62,14 +119,14 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
:param boxSize:
: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:
maxBoxSize: float = filterSize / self.numBoxesAcross
def _get_box_starts(self, boxSize: float) -> list:
maxBoxSize: float = self.filterDiameter / self.numBoxesAcross
assert maxBoxSize >= boxSize
tileStarts: list = []
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)
return tileStarts
......@@ -78,15 +135,11 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod):
class SpiralSelector(BoxSelectionSubsamplingMethod):
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
......@@ -95,14 +148,14 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
@property
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:
"""
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
filterCenter: tuple = self.filterDiameter/2 - self.boxSize/2, self.filterDiameter/2 - self.boxSize/2
slope = self.spiralSlope
theta: float = 0
boxDistance = self.boxSize * 1.1
......@@ -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.
: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
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
xCoordsBoxMiddles: np.array = xCoords + self.boxSize/2
yCoordsBoxMiddles: np.array = yCoords + self.boxSize/2
xCoordsBoxMiddles: np.ndarray= xCoords + self.boxSize/2
yCoordsBoxMiddles: np.ndarray= 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
desiredDistanceTotal: float = self.filterDiameter / 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
xCoords = xCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
yCoords = yCoordsBoxMiddles + (self.filterDiameter - self.boxSize)/2
newTopLefts = zip(np.round(xCoords), np.round(yCoords))
return list(tuple(newTopLefts))
......@@ -157,12 +208,12 @@ class SpiralSelector(BoxSelectionSubsamplingMethod):
"""
center = np.array(center)
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]])
distances: np.array = np.linalg.norm(coords - center, axis=1)
distances: np.ndarray= np.linalg.norm(coords - center, axis=1)
return np.max(distances)
def _get_xy_at_angle(self, theta: float, centerXY: tuple = (0, 0)) -> tuple:
......
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):
......@@ -7,6 +12,8 @@ class FilterView(QtWidgets.QGraphicsView):
super(FilterView, self).__init__()
self.setWindowTitle('FilterView')
self.dataset: dataset.DataSet = None
scene = QtWidgets.QGraphicsScene(self)
scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
scene.setBackgroundBrush(QtCore.Qt.darkGray)
......@@ -14,37 +21,124 @@ class FilterView(QtWidgets.QGraphicsView):
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
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.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()
for item in viewItems:
self.measuringBoxes.append(item)
self.scene().addItem(item)
offset = self.filter.circleOffset
for x, y in topLefts:
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:
for item in self.measuringBoxes:
self.scene().removeItem(item)
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):
"""
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__()
self.width = filterWidth
self.height = filterHeight
self.width: float = filterWidth
self.height: float = filterHeight
self.diameter: float = diameter
self.circleOffset: tuple = (0, 0)
self.setPos(0, 0)
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:
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:
painter.setPen(QtCore.Qt.black)
painter.setBrush(QtCore.Qt.white)
......@@ -52,15 +146,16 @@ class FilterGraphItem(QtWidgets.QGraphicsItem):
painter.setPen(QtCore.Qt.darkGray)
painter.setBrush(QtCore.Qt.lightGray)
painter.drawEllipse(self.rect)
painter.drawEllipse(self.circleRect)
class MeasureBoxGraphItem(QtWidgets.QGraphicsItem):
"""
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__()
self.setZValue(5)
self.posX: float = posX
self.posY: float = posY
self.height: float = height
......@@ -74,14 +169,49 @@ class MeasureBoxGraphItem(QtWidgets.QGraphicsItem):
return self.rect
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.drawRects(self.rect)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
filterView = FilterView()
filterView.show()
ret = app.exec_()
\ No newline at end of file
class ParticleContour(QtWidgets.QGraphicsItem):
def __init__(self, contourData, pos=(0, 0)) -> None:
super(ParticleContour, self).__init__()
self.setZValue(1)
self.setPos(pos[0], pos[1])
self.brect: QtCore.QRectF = QtCore.QRectF(0, 0, 1, 1)
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):
self.activeMode: MeasureMode = None
self.activeModeControl: QtWidgets.QGroupBox = QtWidgets.QGroupBox()
loadDsetBtn = QtWidgets.QPushButton('Load Dataset')
loadDsetBtn.released.connect(self._load_dataset)
self.controlGroup = QtWidgets.QGroupBox()
self.controlGroupLayout = QtWidgets.QHBoxLayout()
self.controlGroup.setLayout(self.controlGroupLayout)
self.controlGroupLayout.addWidget(loadDsetBtn)
self.controlGroupLayout.addWidget(QtWidgets.QLabel('Select Subsampling Mode:'))
self.controlGroupLayout.addWidget(self.modeSelector)
self.controlGroupLayout.addWidget(self.activeModeControl)
......@@ -57,6 +61,12 @@ class MainView(QtWidgets.QWidget):
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__':
import sys
......
......@@ -24,8 +24,7 @@ class CrossBoxMode(MeasureMode):
self.update_measure_viewItems()
def update_measure_viewItems(self) -> None:
self.crossBoxGenerator.filterHeight = self.filterView.filter.height
self.crossBoxGenerator.filterWidth = self.filterView.filter.width
self.crossBoxGenerator.filterDiameter = self.filterView.filter.diameter
self.crossBoxGenerator.numBoxesAcross = int(self.uiControls.numBoxesSelector.currentText())
desiredCoverage: int = self.uiControls.coverageSpinbox.value()
......@@ -36,14 +35,9 @@ class CrossBoxMode(MeasureMode):
self.crossBoxGenerator.fraction = desiredCoverage / 100
viewItems = []
topLefts: list = self.crossBoxGenerator.get_topLeft_of_boxes()
boxSize = self.crossBoxGenerator.boxSize
for (x, y) in topLefts:
newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize)
viewItems.append(newBox)
self.filterView.update_measure_boxes(viewItems)
self.filterView.update_measure_boxes(topLefts, boxSize)
class CrossBoxesControls(QtWidgets.QGroupBox):
......@@ -78,7 +72,7 @@ class CrossBoxesControls(QtWidgets.QGroupBox):
def _config_changed(self):
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)
if maxCoverage < self.coverageSpinbox.value():
self.coverageSpinbox.valueChanged.disconnect()
......@@ -94,20 +88,19 @@ class SpiralBoxMode(MeasureMode):
self.update_measure_viewItems()
def update_measure_viewItems(self) -> None:
self.spiralBoxGenerator.filterHeight = self.filterView.filter.height
self.spiralBoxGenerator.filterWidth = self.filterView.filter.width
self.spiralBoxGenerator.filterDiameter = self.filterView.filter.diameter
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()
viewItems = []
topLefts: list = self.spiralBoxGenerator.get_topLeft_of_boxes()
boxSize = self.spiralBoxGenerator.boxSize
for (x, y) in topLefts:
newBox: MeasureBoxGraphItem = MeasureBoxGraphItem(x, y, boxSize, boxSize)
viewItems.append(newBox)
self.filterView.update_measure_boxes(viewItems)
self.filterView.update_measure_boxes(topLefts, boxSize)
class SpiralBoxControls(QtWidgets.QGroupBox):
......@@ -125,7 +118,7 @@ class SpiralBoxControls(QtWidgets.QGroupBox):
layout.addWidget(QtWidgets.QLabel('Box Size:'))
self.boxSizeSpinbox = QtWidgets.QSpinBox()
self.boxSizeSpinbox.setValue(50)
self.boxSizeSpinbox.setMaximum(1000)
self.boxSizeSpinbox.setMaximum(10000)
self.boxSizeSpinbox.valueChanged.connect(self._config_changed)
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
import numpy as np
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
class ParticleBinSorter(object):
......@@ -36,15 +32,108 @@ class ParticleBinSorter(object):
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()
for point in contour:
contourPolygon.append(QtCore.QPointF(point[0, 0], point[0, 1]))
if type(contour) == np.ndarray:
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.append(QtCore.QPointF(boxXY[0], boxXY[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0], boxXY[1] + boxWidthHeight[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0] + boxWidthHeight[0], boxXY[1]))
boxPolygon.append(QtCore.QPointF(boxXY[0] + boxWidthHeight[0], boxXY[1] + boxWidthHeight[1]))
boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0], boxTopLeftXY[1]+offset[1]))
boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0], boxTopLeftXY[1] + boxWidthHeight[1]+offset[1]))
boxPolygon.append(QtCore.QPointF(boxTopLeftXY[0]+offset[0] + boxWidthHeight[0], boxTopLeftXY[1]+offset[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):
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):
def apply_subsampling_method(self):
numOrigParticles = len(self.particleContainer.particles)
......
......@@ -6,6 +6,7 @@ sys.path.append("C://Users//xbrjos//Desktop//Python")
from gepard import dataset
from methods import IvlevaSubsampling, RandomSampling, SizeBinFractioning
from geometricMethods import BoxSelectionCreator
from helpers import ParticleBinSorter
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
dset = dataset.loadData(fname)
print('loaded dataset')
pc = dset.particleContainer
origParticles = pc.particles
boxCreator = BoxSelectionCreator(dset)
center, size = boxCreator.get_filterDimensions_from_dataset()
print(center, size)
print(dset.mapToPixel(center, force=True))
resultComparer = ResultComparer()
numOrigMP = resultComparer._get_number_of_MP_particles(origParticles)
print(f'orig particles: {len(origParticles)}, of which are mp: {numOrigMP}')
# ivlevaSampling = IvlevaSubsampling(pc)
# ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method()
# pc = dset.particleContainer
# origParticles = pc.particles
t0 = time.time()
fractions = np.arange(0.05, .55, 0.05)
errors = []
binErrors = []
numIterations = 1000
# resultComparer = ResultComparer()
# numOrigMP = resultComparer._get_number_of_MP_particles(origParticles)
# print(f'orig particles: {len(origParticles)}, of which are mp: {numOrigMP}')
# # ivlevaSampling = IvlevaSubsampling(pc)
# # ivlevaFraction, ivlevaParticles = ivlevaSampling.apply_subsampling_method()
for fraction in fractions:
print('random sampling, fraction:', fraction)
# randomSampling = RandomSampling(pc, desiredFraction=fraction)
randomSampling = SizeBinFractioning(pc, fraction)
iterErrors = []
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)
# t0 = time.time()
# fractions = np.arange(0.05, .55, 0.05)
# errors = []
# binErrors = []
# numIterations = 1000
errors.append(round(np.mean(iterErrors)*100)) #from fraction to %
fractionBinErrors = []
for binIndex in range(len(bins)+1):
binError = round(np.mean([binIterErrors[i][binIndex] for i in range(numIterations)]) * 100)
fractionBinErrors.append(binError)
binErrors.append(fractionBinErrors)
# for fraction in fractions:
# print('random sampling, fraction:', fraction)
# # randomSampling = RandomSampling(pc, desiredFraction=fraction)
# randomSampling = SizeBinFractioning(pc, fraction)
# iterErrors = []
# 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')
binLowerLimits = bins.copy()
binLowerLimits.insert(0, 0)
plt.subplot(121)
plt.plot(fractions, errors)
# plt.title(f'Random Sampling, averaged from {numIterations} trials, orig particle count: {len(origParticles)}')
plt.xlabel('Fraction measured')
plt.ylabel('Average error in MP particle count (%)')
# errors.append(round(np.mean(iterErrors)*100)) #from fraction to %
# fractionBinErrors = []
# for binIndex in range(len(bins)+1):
# binError = round(np.mean([binIterErrors[i][binIndex] for i in range(numIterations)]) * 100)
# fractionBinErrors.append(binError)
# binErrors.append(fractionBinErrors)
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()
# print('random sampling took', np.round(time.time()-t0, 2), 'seonds')
# binLowerLimits = bins.copy()
# binLowerLimits.insert(0, 0)
# plt.subplot(121)
# plt.plot(fractions, errors)
# # plt.title(f'Random Sampling, averaged from {numIterations} trials, orig particle count: {len(origParticles)}')
# plt.xlabel('Fraction measured')
# 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()
\ No newline at end of file
# # sizeBinSampling = SizeBinFractioning(pc)
# # sizeBinParticles = sizeBinSampling.apply_subsampling_method()
\ No newline at end of file
import sys
import unittest
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):
......@@ -8,7 +12,7 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.crossBoxSelector = CrossBoxSelector(None)
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.numBoxesAcross = 3
......@@ -20,9 +24,9 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(len(topLeftCorners), 9)
def test_get_tile_topLefts(self):
self.crossBoxSelector.filterHeight = self.crossBoxSelector.filterWidth = 100
self.crossBoxSelector.filterDiameter = 100
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)
verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize)
self.assertEqual(horizontalTileStarts, [0, 100 / 3, 2 * 100 / 3])
......@@ -36,7 +40,7 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(horizontalTileStarts, verticalTileStarts)
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)
verticalTileStarts: list = self.crossBoxSelector._get_vertical_box_starts(maxBoxSize)
self.assertEqual(horizontalTileStarts, [0, 100/5, 2*100/5, 3*100/5, 4*100/5])
......@@ -51,11 +55,11 @@ class TestSelectCrossBoxes(unittest.TestCase):
self.assertEqual(horizontalTileStarts, verticalTileStarts)
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.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
numBoxes: int = 2*self.crossBoxSelector.numBoxesAcross - 1
......@@ -71,7 +75,7 @@ class TestSelectSpiralBoxes(unittest.TestCase):
def test_move_and_scale_toplefts(self):
self.spiralBoxSelector.filterHeight = 100
self.spiralBoxSelector.filterWidth = 100
self.spiralBoxSelector.filterDiameter = 100
self.spiralBoxSelector.boxSize = 10
topLefts = [(45, 45), (0, 45), (90, 45)]
......@@ -118,3 +122,38 @@ class TestSelectSpiralBoxes(unittest.TestCase):
topLefts: list = [(-2, 0), (5, 9)]
overlaps: bool = self.spiralBoxSelector._boxes_are_overlapping(topLefts)
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
"""
import unittest
from helpers import ParticleBinSorter, box_contains_contour
import helpers
import numpy as np
import sys
sys.path.append("C://Users//xbrjos//Desktop//Python")
from PyQt5 import QtCore, QtGui
import gepard
from gepard.analysis.particleAndMeasurement import Particle
from gepard import dataset
class TestBinSorter(unittest.TestCase):
def setUp(self):
self.sorter = ParticleBinSorter()
self.sorter = helpers.ParticleBinSorter()
self.bins = self.sorter.bins
def test_sort_particles_into_bins(self):
......@@ -66,15 +68,126 @@ class TestBinSorter(unittest.TestCase):
class TestOther(unittest.TestCase):
def test_box_contains_contour(self):
def test_box_overlaps_contour(self):
boxXY: tuple = 0, 0
boxWidthHeight: tuple = 10, 10
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
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
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)