diff --git a/__main__.py b/__main__.py index 4de0d9718e0b90c4a37cfacdf0243f77b59625f6..9cc5ed6378b4fa6e5e513f26a068edb77ba80abb 100644 --- a/__main__.py +++ b/__main__.py @@ -308,6 +308,9 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): if self.view.simulatedRaman: self.configRamanCtrlAct.setDisabled(True) + self.exportSLFAct: QtWidgets.QAction = QtWidgets.QAction("&Export FTIR Apertures to .slf") + self.exportSLFAct.triggered.connect(self.view.exportAptsToSLF) + self.testAct: QtWidgets.QAction = QtWidgets.QAction("&Run Automated Gepard Test") self.testAct.setShortcut("Ctrl+T") self.testAct.triggered.connect(runGepardTest(self)) @@ -357,6 +360,8 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): self.toolsMenu = QtWidgets.QMenu("&Tools") self.toolsMenu.addAction(self.snapshotAct) self.toolsMenu.addAction(self.configRamanCtrlAct) + self.toolsMenu.addAction(self.exportSLFAct) + self.toolsMenu.addSeparator() self.toolsMenu.addAction(self.testAct) self.dispMenu = QtWidgets.QMenu("&Display", self) diff --git a/analysis/ftirAperture.py b/analysis/ftirAperture.py index a7a9b3b6c0b1ec47fa0f1c9673e6259ac48c7b61..938ed27a88b71885a38e5cb0e6fe80c25809a44b 100644 --- a/analysis/ftirAperture.py +++ b/analysis/ftirAperture.py @@ -22,7 +22,7 @@ If not, see . import numpy as np from scipy import optimize import cv2 -# from ..cythonModules.getMaxRect import findMaxRect # TODO: UNCOMMENT!!! +from ..cythonModules.getMaxRect import findMaxRect # TODO: UNCOMMENT!!! #################################################### diff --git a/analysis/particleCharacterization.py b/analysis/particleCharacterization.py index 0c8a8e6143694076633e9aabbd293cd9758b2bd1..55427329aed9244002fc7f50bafca29dd847b4ac 100644 --- a/analysis/particleCharacterization.py +++ b/analysis/particleCharacterization.py @@ -23,6 +23,7 @@ If not, see . import numpy as np import cv2 from copy import deepcopy +from typing import TYPE_CHECKING from .particleClassification.colorClassification import ColorClassifier from .particleClassification.shapeClassification import ShapeClassifier @@ -31,17 +32,21 @@ from ..segmentation import closeHolesOfSubImage from ..errors import InvalidParticleError from ..helperfunctions import cv2imread_fix +if TYPE_CHECKING: + from ..analysis.particleAndMeasurement import Particle + from ..scenePyramid import ScenePyramid + class FTIRAperture(object): """ Configuration for an FTIR aperture. CenterCoords, width and height in Pixels """ - centerX: float = None - centerY: float = None - centerZ: float = None - width: float = None - height: float = None - angle: float = None + centerX: float = None # in px + centerY: float = None # in px + centerZ: float = None # in px + width: float = None # in px + height: float = None # in px + angle: float = None # in degree rectCoords: list = None @@ -118,28 +123,41 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, scenePyrami newStats.shortSize *= pixelscale partImg = None + assert scenePyramid is not None or fullimage is not None + if scenePyramid is None and fullimage is not None: partImg, extrema = getParticleImageFromFullimage(cnt, fullimage) elif scenePyramid is not None and fullimage is None: partImg, extrema = getParticleImageFromScenePyramid(cnt, scenePyramid) + assert partImg is not None, "error in getting particle image" newStats.color = getParticleColor(partImg) if ftir: - padding: int = int(2) - imgforAperture = np.zeros((partImg.shape[0]+2*padding, partImg.shape[1]+2*padding)) - imgforAperture[padding:partImg.shape[0]+padding, padding:partImg.shape[1]+padding] = np.mean(partImg, axis=2) - imgforAperture[imgforAperture > 0] = 1 # convert to binary image - imgforAperture = np.uint8(1 - imgforAperture) # background has to be 1, particle has to be 0 - aperture: FTIRAperture = getFTIRAperture(imgforAperture) - aperture.centerX = aperture.centerX + extrema[0] - padding - aperture.centerY = aperture.centerY + extrema[2] - padding - aperture.centerZ = newStats.height - newStats.aperture = aperture + newStats.aperture = getApertureObjectForParticleImage(partImg, extrema, newStats.height) return newStats +def calculateFTIRAperturesForParticle(particle: 'Particle', pyramid: 'ScenePyramid') -> 'FTIRAperture': + partImg, extrema = getParticleImageFromScenePyramid(particle.contour, pyramid) + aperture: 'FTIRAperture' = getApertureObjectForParticleImage(partImg, extrema, particle.height) + return aperture + + +def getApertureObjectForParticleImage(particleImage: np.ndarray, extrema: tuple, + particleHeight: float, padding: int = 2) -> 'FTIRAperture': + imgforAperture = np.zeros((particleImage.shape[0] + 2 * padding, particleImage.shape[1] + 2 * padding)) + imgforAperture[padding:particleImage.shape[0] + padding, padding:particleImage.shape[1] + padding] = np.mean(particleImage, axis=2) + imgforAperture[imgforAperture > 0] = 1 # convert to binary image + imgforAperture = np.uint8(1 - imgforAperture) # background has to be 1, particle has to be 0 + aperture: FTIRAperture = getFTIRAperture(imgforAperture) + aperture.centerX = aperture.centerX + extrema[0] - padding + aperture.centerY = aperture.centerY + extrema[2] - padding + aperture.centerZ = particleHeight + return aperture + + def getFibreDimension(contour): longSize = cv2.arcLength(contour, True)/2 img = contoursToImg([contour])[0] diff --git a/analysis/particleContainer.py b/analysis/particleContainer.py index a6347267d69bc10c14da5524d190364376d67551..ffbbc2fab61da14be84d73cc05bfef3cec3e19ee 100644 --- a/analysis/particleContainer.py +++ b/analysis/particleContainer.py @@ -25,6 +25,7 @@ from PyQt5 import QtWidgets from typing import List, TYPE_CHECKING from .particleAndMeasurement import Particle, Measurement +from .particleCharacterization import calculateFTIRAperturesForParticle specImportEnabled: bool = True try: from ..analysis import importSpectra @@ -33,6 +34,7 @@ except ModuleNotFoundError: if TYPE_CHECKING: from ..dataset import DataSet + from ..scenePyramid import ScenePyramid class ParticleContainer(object): @@ -121,7 +123,23 @@ class ParticleContainer(object): assert len(self.particles) == len(particlestats), f'numParticles = {len(self.particles)}, len partStats = {len(particlestats)}' for index, particle in enumerate(self.particles): particle.__dict__.update(particlestats[index].__dict__) - + + def updateFTIRApertures(self, pyramid: 'ScenePyramid', progressbar: QtWidgets.QProgressDialog = None) -> None: + """ + Calculates FTIR Apertures if they were not already calculated... + """ + if progressbar is not None: + progressbar.setWindowTitle("Calculating Particle Apertures") + progressbar.setMaximum(len(self.particles)-1) + progressbar.setValue(0) + + for i, particle in enumerate(self.particles): + if not hasattr(particle, 'aperture'): + particle.aperture = calculateFTIRAperturesForParticle(particle, pyramid) + elif particle.aperture is None: + particle.aperture = calculateFTIRAperturesForParticle(particle, pyramid) + progressbar.setValue(i) + def testForInconsistentParticleAssignments(self): # Find particles that have multiple measurements with different assignments self.inconsistentParticles = [] diff --git a/dataset.py b/dataset.py index 04489801dc7b1bf6f0e5c098979870968a9724bf..ee225329f9077ba9ae82722bd03be8b3ad4ea7e4 100644 --- a/dataset.py +++ b/dataset.py @@ -284,8 +284,9 @@ class DataSet(object): z0, z1 = self.zpositions[0], self.zpositions[-1] return zp / 255. * (z1 - z0) + z0 - def mapHeight(self, x, y): - assert not self.readin + def mapHeight(self, x, y, force=False): + if not force: + assert not self.readin assert self.heightmap is not None return self.heightmap[0] * x + self.heightmap[1] * y + self.heightmap[2] @@ -325,7 +326,7 @@ class DataSet(object): z = None if (returnz and self.zvalimg is not None) or self.coordinatetransform is not None: - z = self.mapHeight(x, y) + z = self.mapHeight(x, y, force=force) z += self.getZval(pixelpos) if self.coordinatetransform is not None: diff --git a/exportSLF.py b/exportSLF.py new file mode 100644 index 0000000000000000000000000000000000000000..1d853bcbd6b1944a60589b19cb5ac2ab2c0ff47d --- /dev/null +++ b/exportSLF.py @@ -0,0 +1,85 @@ +""" +GEPARD - Gepard-Enabled PARticle Detection +Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für +Polymerforschung Dresden e. V. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program, see COPYING. +If not, see . +""" +import xml.etree.cElementTree as ET +import numpy as np +import os +from typing import * + +if TYPE_CHECKING: + from PyQt5 import QtWidgets + from .dataset import DataSet + from .analysis.particleContainer import ParticleContainer + from .analysis.particleCharacterization import FTIRAperture + + +def exportSLFFile(dataset: 'DataSet', progressBar: 'QtWidgets.QProgressDialog', minAptSize: float = 3, + maxAptSize: float = 1000) -> str: + """ + Writes an SLF file for import in the Perkin Elmer Spotlight Instrument. + :param dataset: Dataset of the currently loaded experimetn + :param progressBar: Statusbar for updating progress + :param minAptSize: Minimal Aperture Size (in µm) + :param maxAptSize: Maximum Aperture Size (in µm) + :returns: Filename of saved file + """ + partContainer: 'ParticleContainer' = dataset.particleContainer + progressBar.setWindowTitle("Creating SLF File") + progressBar.setMaximum(partContainer.getNumberOfParticles() - 1) + pxScale: float = dataset.getPixelScale() # µm/px + + # prepare xml file + root = ET.Element("configuration") + shapes = ET.SubElement(root, "shapes") + + for i, particle in enumerate(partContainer.particles): + apt: 'FTIRAperture' = particle.aperture + width = np.clip(apt.width * pxScale, minAptSize, maxAptSize) + height = np.clip(apt.height * pxScale, minAptSize, maxAptSize) + + x, y, z = dataset.mapToLength((apt.centerX, apt.centerY), force=True, returnz=True) + x = round(x, 5) + y = round(y, 5) + z = round(z, 5) + + shape = ET.SubElement(shapes, "shape", type="Pki.MolSpec.MSOverlayControls.Marker", hasAperture="True") + ET.SubElement(shape, "dimensions", x=str(x), y=str(y), z=str(z), width="0", height="0", color="4294901760") + ET.SubElement(shape, "aperture", width=str(width), height=str(height), rotation=str(apt.angle)) + ET.SubElement(shape, "sample", id="Marker", measured="false") + progressBar.setValue(i) + + # append last entry to slf file: + shape = ET.SubElement(shapes, "shape", type="Pki.MolSpec.MSOverlayControls.CrosshairCursor", hasAperture="False") + ET.SubElement(shape, "dimensions", x=str(3856), y=str(-739), z=str(-1643), width="0", height="0", + color="4294901760") + ET.SubElement(shape, "aperture", width=str(width), height=str(height), rotation=str(apt.angle)) + ET.SubElement(shape, "sample", id="Background", measured="true") + + # write slf-file + savename = os.path.join(dataset.path, dataset.name) + tree = ET.ElementTree(root) + inc = 1 + filename = savename + '.slf' + + while os.path.exists(savename): + filename = savename + '(' + str(inc) + ').slf' + inc += 1 + tree.write(filename) + return os.path.basename(filename) + diff --git a/gui/viewItems/detectItems.py b/gui/viewItems/detectItems.py index ac2a9e0359202e286a5ce2a12743f261bc8c6341..6e3b5eee5b46769d0fb3f6e41c2961291deb79b5 100644 --- a/gui/viewItems/detectItems.py +++ b/gui/viewItems/detectItems.py @@ -20,6 +20,7 @@ If not, see . from PyQt5 import QtWidgets, QtCore, QtGui from copy import deepcopy from typing import TYPE_CHECKING +import numpy as np from ...analysis.particleEditor import ParticleContextMenu from ...analysis.particleCharacterization import FTIRAperture if TYPE_CHECKING: @@ -184,7 +185,7 @@ class FTIRApertureIndicator(QtWidgets.QGraphicsItem): def paint(self, painter, option, widget): if not self.hidden: - color: QtGui.QColor = QtCore.Qt.green + color: QtGui.QColor = QtGui.QColor(0, 255, 0, 255) if self.transparent: color.setAlpha(128) painter.setPen(color) diff --git a/sampleview.py b/sampleview.py index 5302f98b4e3afb5afc09f276849cea3f36cfdd83..e3dbd695484b7581df1ca0b9f92aa8a3a0d90c5b 100644 --- a/sampleview.py +++ b/sampleview.py @@ -34,6 +34,8 @@ from .gui.configInstrumentUI import InstrumentConfigWin from .scenePyramid import ScenePyramid from .gepardlogging import setDefaultLoggingConfig from .gui.viewItemHandler import ViewItemHandler +from .exportSLF import exportSLFFile + if TYPE_CHECKING: from __main__ import GEPARDMainWindow @@ -124,7 +126,17 @@ class SampleView(QtWidgets.QGraphicsView): """ self.configWin = InstrumentConfigWin(self) self.configWin.show() - + + def exportAptsToSLF(self) -> None: + progress: QtWidgets.QProgressDialog = QtWidgets.QProgressDialog() + progress.setCancelButton(None) + progress.setWindowModality(QtCore.Qt.WindowModal) + + self.dataset.particleContainer.updateFTIRApertures(self.pyramid, progressbar=progress) + self.dataset.save() + fname = exportSLFFile(self.dataset, progress) + QtWidgets.QMessageBox.about(self, "Done", f"Particle and Aperture data saved to {fname} in project directory") + def saveDataSet(self): if self.dataset is not None: self.dataset.save() diff --git a/unittests/testhelpers.py b/unittests/testhelpers.py index 26493711eb7f0d924f8c862b9bb3a652040f1ff1..5a591eabe63838d3893efc6693c39bb9e41cf7f1 100644 --- a/unittests/testhelpers.py +++ b/unittests/testhelpers.py @@ -1,6 +1,9 @@ import logging +import numpy as np from typing import TYPE_CHECKING from ..dataset import DataSet +from ..analysis.particleContainer import ParticleContainer +from ..analysis.particleAndMeasurement import Particle, Measurement from ..__main__ import GEPARDMainWindow if TYPE_CHECKING: from ..sampleview import SampleView @@ -31,3 +34,18 @@ def getDefaultMainWin() -> GEPARDMainWindow: def getDefaultSampleview() -> 'SampleView': gepard: GEPARDMainWindow = getDefaultMainWin() return gepard.view + + +def getDefaultParticleContainer(numParticles: int = 4) -> ParticleContainer: + partContainer: ParticleContainer = ParticleContainer() + partContainer.initializeParticles(numParticles) + contours: list = [] + for i in range(numParticles): + x = 10*i + contours.append(np.array([[[x, 0]], [[x+10, 0]], [[x+10, 10]], [[x, 10]]], dtype=np.int32)) + partContainer.setParticleContours(contours) + partContainer.particles[0].color = 'red' + partContainer.particles[1].color = 'blue' + partContainer.particles[2].color = 'green' + partContainer.particles[3].color = 'transparent' + return partContainer