From 064d26eb6fbdb63dbdcfb1833d3e6f1f95a4a42f Mon Sep 17 00:00:00 2001 From: Josef Brandt Date: Mon, 9 Mar 2020 11:12:20 +0100 Subject: [PATCH] Dataset can be loaded in mainView --- geometricMethods.py | 83 ++++++++++++++--- gui/filterView.py | 162 ++++++++++++++++++++++++++++++--- gui/mainView.py | 10 ++ gui/measureModes.py | 27 ++---- helpers.py | 119 +++++++++++++++++++++--- methods.py | 24 ----- subsampling.py | 100 ++++++++++---------- tests/test_geometricMethods.py | 55 +++++++++-- tests/test_helpers.py | 125 +++++++++++++++++++++++-- 9 files changed, 561 insertions(+), 144 deletions(-) diff --git a/geometricMethods.py b/geometricMethods.py index e3e1182..96b0556 100644 --- a/geometricMethods.py +++ b/geometricMethods.py @@ -1,7 +1,60 @@ 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): @@ -41,7 +94,7 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): :return float: """ alpha: float = np.deg2rad(135) - r: float = self.filterSize/2 + 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 @@ -69,11 +122,11 @@ class CrossBoxSelector(BoxSelectionSubsamplingMethod): return self._get_box_starts(boxSize) def _get_box_starts(self, boxSize: float) -> list: - maxBoxSize: float = self.filterSize / self.numBoxesAcross + maxBoxSize: float = self.filterDiameter / self.numBoxesAcross assert maxBoxSize >= boxSize tileStarts: list = [] for i in range(self.numBoxesAcross): - start: float = i * self.filterSize / self.numBoxesAcross + (maxBoxSize - boxSize) / 2 + start: float = i * self.filterDiameter / self.numBoxesAcross + (maxBoxSize - boxSize) / 2 tileStarts.append(start) return tileStarts @@ -102,7 +155,7 @@ class SpiralSelector(BoxSelectionSubsamplingMethod): 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.filterSize/2 - self.boxSize/2, self.filterSize/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,25 +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: """ - xCoords: np.array = np.array([float(point[0]) for point in topLefts]) - self.filterSize / 2 - yCoords: np.array = np.array([float(point[1]) for point in topLefts]) - self.filterSize / 2 + 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.filterSize - self.boxSize)/2 - yCoords = yCoordsBoxMiddles + (self.filterSize - 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)) @@ -155,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: diff --git a/gui/filterView.py b/gui/filterView.py index cbcfdae..a0275c7 100644 --- a/gui/filterView.py +++ b/gui/filterView.py @@ -1,4 +1,9 @@ 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,6 +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) \ No newline at end of file + painter.drawRects(self.rect) + + +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) diff --git a/gui/mainView.py b/gui/mainView.py index 8e8cb29..d0d166a 100644 --- a/gui/mainView.py +++ b/gui/mainView.py @@ -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 diff --git a/gui/measureModes.py b/gui/measureModes.py index 3c70a92..1dca9d2 100644 --- a/gui/measureModes.py +++ b/gui/measureModes.py @@ -24,8 +24,7 @@ class CrossBoxMode(MeasureMode): self.update_measure_viewItems() def update_measure_viewItems(self) -> None: - assert self.filterView.filter.height == self.filterView.filter.width - self.crossBoxGenerator.filterSize = self.filterView.filter.height + 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): @@ -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) diff --git a/helpers.py b/helpers.py index 80799ef..4064382 100644 --- a/helpers.py +++ b/helpers.py @@ -1,12 +1,8 @@ -#!/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 diff --git a/methods.py b/methods.py index 49d0148..eade8c6 100644 --- a/methods.py +++ b/methods.py @@ -26,30 +26,6 @@ class SubsamplingMethod(object): raise NotImplementedError -class BoxSelectionSubsamplingMethod(SubsamplingMethod): - def __init__(self, *args): - super(BoxSelectionSubsamplingMethod, self).__init__(*args) - self.filterSize: float = 500 - - @property - def filterArea(self) -> float: - return np.pi * (self.filterSize / 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_contains_contour(topLeft, (boxSize, boxSize), particle.contour): - subParticles.append(particle) - - return self.fraction, subParticles - - def get_topLeft_of_boxes(self) -> list: - raise NotImplementedError - - class RandomSampling(SubsamplingMethod): def apply_subsampling_method(self): numOrigParticles = len(self.particleContainer.particles) diff --git a/subsampling.py b/subsampling.py index 41e1c01..7e2e83a 100644 --- a/subsampling.py +++ b/subsampling.py @@ -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 diff --git a/tests/test_geometricMethods.py b/tests/test_geometricMethods.py index ef26762..63721bf 100644 --- a/tests/test_geometricMethods.py +++ b/tests/test_geometricMethods.py @@ -1,6 +1,10 @@ +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.filterSize = 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.filterSize = 100 + self.crossBoxSelector.filterDiameter = 100 self.crossBoxSelector.numBoxesAcross = 3 - maxBoxSize: float = self.crossBoxSelector.filterSize/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.filterSize / 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.filterSize = 100 # in pixel + self.crossBoxSelector.filterDiameter = 100 # in pixel self.crossBoxSelector.fraction = 0.1 self.crossBoxSelector.numBoxesAcross = 3 - filterArea: float = np.pi * (self.crossBoxSelector.filterSize/2)**2 + 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.filterSize = 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) + + diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3247c34..b08289e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -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) -- GitLab