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