Commit 4fcd0f47 authored by ohmacht@zwei-g.de's avatar ohmacht@zwei-g.de

-working draft of a particle scout import

parent 2fc10a62
......@@ -117,16 +117,34 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.view.open(self.fname)
self.scalingChanged()
@QtCore.pyqtSlot()
@QtCore.pyqtSlot()
def importProject(self, fileName=False):
if fileName is False:
fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Import Zeiss Zen Project", defaultPath, "*.xml")[0]
fileName = QtWidgets.QFileDialog.getOpenFileName(
self,
"Import Zeiss Zen Project",
defaultPath,
"*.xml"
)[0]
if fileName:
self.fname = str(fileName)
self.view.importProject(self.fname)
self.scalingChanged()
@QtCore.pyqtSlot()
@QtCore.pyqtSlot()
def importPSProject(self, filename=False):
if filename is False:
filename = QtWidgets.QFileDialog.getOpenFileName(
self,
"Import Particle Scout Project",
defaultPath,
"Image (*.bmp);;Text (*.txt);;Table (*.csv)"
)[0]
if filename:
self.view.importPSProject(filename)
self.scalingChanged()
@QtCore.pyqtSlot()
def new(self, fileName=False):
if fileName is False:
fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project", defaultPath, "*.pkl")[0]
......@@ -208,6 +226,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
def unblockUI(self, connected):
self.openAct.setEnabled(True)
self.importAct.setEnabled(True)
self.importPSAct.setEnabled(True)
self.newAct.setEnabled(True)
self.updateConnected(connected)
self.exitAct.setEnabled(True)
......@@ -215,6 +234,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
def blockUI(self):
self.openAct.setEnabled(False)
self.importAct.setEnabled(False)
self.importPSAct.setEnabled(False)
self.newAct.setEnabled(False)
self.connectRamanAct.setEnabled(False)
self.disconnectRamanAct.setEnabled(False)
......@@ -247,6 +267,9 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.importAct.setShortcut("Ctrl+I")
self.importAct.triggered.connect(self.importProject)
self.importPSAct = QtWidgets.QAction("&Import Particle Scout Project...", self)
self.importPSAct.triggered.connect(self.importPSProject)
self.newAct = QtWidgets.QAction("&New Measurement...", self)
self.newAct.setShortcut("Ctrl+N")
self.newAct.triggered.connect(self.new)
......@@ -348,6 +371,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.fileMenu = QtWidgets.QMenu("&File", self)
self.fileMenu.addAction(self.newAct)
self.fileMenu.addAction(self.importAct)
self.fileMenu.addAction(self.importPSAct)
self.fileMenu.addAction(self.openAct)
self.fileMenu.addSeparator()
self.fileMenu.addAction(self.exitAct)
......@@ -398,6 +422,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.toolbar.addAction(self.aboutAct)
self.toolbar.addAction(self.newAct)
self.toolbar.addAction(self.importAct)
self.toolbar.addAction(self.importPSAct)
self.toolbar.addAction(self.openAct)
self.toolbar.addSeparator()
self.toolbar.addAction(self.connectRamanAct)
......
......@@ -210,6 +210,7 @@ class DataSet(object):
'seedRad': 3}
self.particleContainer = ParticleContainer()
self.opticalScanDone = False
self.particleDetectionDone = False
self.specscandone = False
......@@ -217,6 +218,9 @@ class DataSet(object):
self.colorSeed = 'default'
self.resultsUploadedToSQL = []
self.seedParticles = None
self.stickToSeedPoints = False
self.readin = True # a value that is always set to True at loadData
# and mark that the coordinate system might be changed in the meantime
self.mode = "prepare"
......@@ -224,10 +228,6 @@ class DataSet(object):
self.fname = self.newProject(fname)
self.updatePath()
@property
def opticalScanDone(self) -> bool:
return os.path.exists(self.getZvalImageName())
def __eq__(self, other):
return recursiveDictCompare(self.__dict__, other.__dict__)
......
......@@ -19,6 +19,7 @@ along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import numpy as np
import cv2
import os
import logging
import logging.handlers
......@@ -495,8 +496,15 @@ class ParticleDetectionView(QtWidgets.QWidget):
self.setLayout(hbox)
self.setWindowTitle("Particle Detection")
positionWidgetOnScreen(self)
self.initDone = True
if 0 < self.dataset.seedpoints.size:
self.updateImageSeeds()
viewItemHandler.updateSeedPointsInDisplay()
self.autoUpdateIfDesired()
def updatePixelScaleLabel(self):
newScale = self.dataset.getPixelScale(self.dataset.imagescanMode) / self.globalScaleFactor.value()
newScale = np.round(newScale, 2)
......@@ -678,22 +686,29 @@ class ParticleDetectionView(QtWidgets.QWidget):
self.seg.setParameters(**kwargs)
seedradius = self.seedradiusedit.value()
if showname is not None:
try:
stepImg, imgtype = self.seg.apply2Image(img, self.imglabel.seedpoints,
self.imglabel.seeddeletepoints,
self.dataset, self.logger, return_step=showname)
stepImg, imgtype = self.seg.apply2Image(
img,
self.imglabel.seedpoints,
self.imglabel.seeddeletepoints,
self.dataset,
self.logger,
return_step=showname
)
self.imglabel.showStep(stepImg, imgtype)
except:
showErrorMessageAsWidget('Fatal error in particle detection, see detectionlog for info')
self.logger.exception('Fatal error in particle detection')
else:
try:
measurementpoints, contours = self.seg.apply2Image(img, self.imglabel.seedpoints,
self.imglabel.seeddeletepoints,
self.dataset, self.logger)
measurementpoints, contours = self.seg.apply2Image(
img,
self.imglabel.seedpoints,
self.imglabel.seeddeletepoints,
self.dataset,
self.logger
)
self.imglabel.updateDetectionResults(contours, measurementpoints)
except:
showErrorMessageAsWidget('Fatal error in particle detection, see detectionlog for info')
......@@ -831,6 +846,35 @@ class ParticleDetectionView(QtWidgets.QWidget):
def applyResultsToDataset(self, measurementPoints, contours):
self.dataset.specscandone = False
if self.dataset.stickToSeedPoints:
self.dataset.specscandone = True
# remove all detected particles that do not contain seedpoints
finalContours = []
finalPoints = []
usedSeeds = []
# check contours
for i, c in enumerate(contours):
# for having a seedpoint
hasSeedPoint = False
for j, p in enumerate(self.dataset.seedpoints):
# inside of or on them
hasSeedPoint = hasSeedPoint or 0 <= cv2.pointPolygonTest(c, (p[0], p[1]), False)
if hasSeedPoint:
# found, save index
usedSeeds.append(j)
# next
break
if hasSeedPoint:
# seedpoint found inside contour
finalContours.append(c)
finalPoints.append(measurementPoints[i])
# order seed particles and spectra by measurement/contour order
self.dataset.seedParticles = [self.dataset.seedParticles[i] for i in usedSeeds]
contours = finalContours
measurementPoints = finalPoints
particlestats, invalidParticleIndices = self.getParticleStats(contours)
contours = removeInvalidContours(contours, invalidParticleIndices)
measurementPoints = removeInvalidMeasurementPoints(measurementPoints, invalidParticleIndices)
......
......@@ -148,11 +148,11 @@ def loadAndPasteImage(srcnames: List[str], pyramid, fullzval: np.ndarray, width:
:param list of str srcnames: list of stacked scan files to merge
:param ScenePyramid pyramid: the scene pyramid
:param numpy.ndarray fullzval: full size zval image
:param float width: width of camera
:param float height: height camera
:param float width: width of camera image [um]
:param float height: height of camera image [um]
:param float rotationvalue: angle of camera
:param list of float p0: (min x; max y) of scan tile positions
:param list of float p1: (max x; min y) of scan tile positions
:param list of float p0: (min x; max y) of scan tile positions [um]
:param list of float p1: (max x; min y) of scan tile positions [um]
:param list of float p: position of current scan tile
:param logger: the logger to use
:param numpy.ndarray background:
......@@ -778,6 +778,7 @@ class OpticalScanUI(QtWidgets.QWidget):
if i == Ngrid-1:
self.dataset.saveZvalImg()
self.dataset.opticalScanDone = True
self.process.join()
self.dataqueue.close()
self.dataqueue.join_thread()
......
......@@ -113,6 +113,11 @@ class SpecScanUI(QtWidgets.QWidget):
self.paramsLayout.addRow(self.prun)
self.paramsGroup.setLayout(self.paramsLayout)
def show(self):
# suppress showing the ui, if scan data was imported
if not self.dataset.specscandone:
super(SpecScanUI, self).show()
def makeGetFnameLambda(self, msg, path, fileType, btn):
return lambda: self.getFName(msg, path, fileType, btn)
......@@ -126,10 +131,38 @@ class SpecScanUI(QtWidgets.QWidget):
self.dataset = ds
numParticles = self.dataset.particleContainer.getNumberOfParticles()
numMeasurements = self.dataset.particleContainer.getNumberOfMeasurements()
if numParticles>0:
if numParticles > 0:
self.prun.setEnabled(True)
self.setWindowTitle(f'{numParticles} Particles ({numMeasurements} Measurements)')
# if imported from particle scout
if self.dataset.stickToSeedPoints:
spectra = []
# particles are in the same order as the measurements/spectra
for i, p in enumerate(self.dataset.seedParticles):
# first particle
if 0 == len(spectra):
# get header
spectra = np.array([np.array(p['spectrum'])[:, 0]])
# get particle spectrum
spectra = np.concatenate((spectra, np.array([np.array(p['spectrum'])[:, 1]])))
# associate spectrum idx with measurement
self.dataset.particleContainer.setMeasurementScanIndex(i, i)
# write spectrum file
# 2d array with
# num(cols) = num(particle/measurements/spectra)
# num(rows) = num(wavelengths)
# transpose spectra
spectra = np.transpose(spectra)
# and save
np.save(os.path.join(self.dataset.path, 'spectra.npy'), spectra)
# build TrueMatch file
fname = os.path.join(self.dataset.path, f'{self.dataset.name}_TrueMatchResults.txt')
with open(fname, 'w') as fp:
fp.write("Search Spectrum Name;HQI 1;Found Spectrum Name 1;IsMarked;\n")
for i, p in enumerate(self.dataset.seedParticles):
fp.write(f"Spectrum {i+1};100;{p['material']};yes;\n")
@QtCore.pyqtSlot()
def cancelScan(self):
if self.process is not None and self.process.is_alive():
......@@ -180,6 +213,10 @@ class SpecScanUI(QtWidgets.QWidget):
except:
self.logger.critical('SpecScanParameter not found' + str(param))
# take list of measurement points (ordered by particle detection sequence):
# [p0, p1, p2, ...]
# returns list of indexes corresponding to measurement points
# e.g. [i_p3, i_p0, i_p1, ...]
cmin = getShortestPath(points)
if not self.instrctrl.name == 'ThermoFTIRCom':
......@@ -224,6 +261,8 @@ class SpecScanUI(QtWidgets.QWidget):
if reply == QtWidgets.QMessageBox.Yes:
self.dataset.mode = "SpectrumScan"
for specScanIndex, measIndex in enumerate(cmin):
# spectra are generated in order of cmin
# tell particle the idx of the spectrum that belongs to it
self.dataset.particleContainer.setMeasurementScanIndex(measIndex, specScanIndex)
self.view.saveDataSet()
......
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>
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 <https://www.gnu.org/licenses/>.
"""
import os
from PyQt5 import QtCore, QtWidgets
from .helperfunctions import cv2imread_fix, cv2imwrite_fix
from .instrumentcom.instrumentConfig import defaultPath
from .dataset import DataSet
from .scenePyramid import ScenePyramid
import numpy as np
import re
import csv
import math
from typing import TYPE_CHECKING
if TYPE_CHECKING: # avoids cyclic imports due to type hints..
from .sampleview import SampleView
class ParticleScoutImporter:
def __init__(self, fname, parent=None):
self.parent: SampleView = parent
self.dataSet = None
self.path = None
self.basename = None
self.validImport = self.checkFiles(fname)
def importValid(self):
return self.validImport
def getDataSet(self):
return self.dataSet
def checkFiles(self, filename):
# e.g. test-particles.csv
(self.path, filename) = os.path.split(filename)
# basename now should be test
self.basename = filename.split('.')[0].split('-')[0]
'''
with basename
we expect these files to exist:
f'{basename}.txt
f'{basename}.bmp
f'{basename}-particles.csv
f'{basename}-spectra.txt
'''
missing = []
if not os.path.exists(os.path.join(self.path, f"{self.basename}.txt")):
missing.append(f"{self.basename}.txt")
if not os.path.exists(os.path.join(self.path, f"{self.basename}.bmp")):
missing.append(f"{self.basename}.bmp")
if not os.path.exists(os.path.join(self.path, f'{self.basename}-particles.csv')):
missing.append(f'{self.basename}-particles.csv')
if not os.path.exists(os.path.join(self.path, f'{self.basename}-spectra.txt')):
missing.append(f'{self.basename}-spectra.txt')
# if some of the files are missing
if 0 < len(missing):
# be a Karen, scream at the manager
missing = '\n '.join(missing)
QtWidgets.QMessageBox.warning(
self.parent,
'INCOMPLETE DATASET!',
f'Following files are missing:\n {missing}',
'Make sure following files exist:\n',
f' {self.basename}.txt\n',
f' {self.basename}.bmp\n',
f' {self.basename}-particles.csv\n',
f' {self.basename}-spectra.txt',
)
return False
return True
def convertData(self):
# read coordinates from txt file
coords = self.readCoordinatesFile()
# read particle information from csv
particles = self.readParticleFile()
# read spectrum information
spectra = self.readSpectraFile()
# merge spectrum data and particle data
missingSpectra, particles = self.mergeSpectraIntoParticles(particles, spectra)
if 0 < len(missingSpectra):
# be a Karen, scream at the manager
missing = '\n '.join(missingSpectra)
QtWidgets.QMessageBox.warning(
self.parent,
'INCOMPLETE DATASET!',
f'Following particles are missing spectrum data:\n {missing}'
)
return False
# build dataset
return self.buildDataSet(coords, particles)
def readCoordinatesFile(self):
'''
extracts information from coords text file
:return:
'''
coords = {}
with open(os.path.join(self.path, f"{self.basename}.txt"), 'r') as fp:
for line in fp:
match = re.match(r'^\s*(?P<var>[^:]+):\s*(?P<value>[0-9-,]+)$', line)
if match:
coords[match.group('var').lower()] = float(match.group('value').replace(',', '.'))
return coords
def readParticleFile(self):
'''
extracts particle coordinates from csv file
:return:
'''
particles = {}
with open(os.path.join(self.path, f"{self.basename}-particles.csv"), 'r') as fp:
preader = csv.DictReader(fp, delimiter=';')
for row in preader:
id = int(row['Particle Name'])
particles[id] = {
'id': id,
'material': row['Material'],
'x': float(row['Visual Center Point X [µm]']),
'y': float(row['Visual Center Point Y [µm]']),
'seedpoint': None,
'spectrum': None
}
return particles
def readSpectraFile(self):
spectra = {}
particle = None
spectrumData = False
with open(os.path.join(self.path, f"{self.basename}-spectra.txt"), 'r') as fp:
for line in fp:
line = line.strip()
newParticle = '[SpectrumHeader]' == line
if newParticle:
particle = {'id': 0, 'spectrum': []}
match = re.match(r'^Title = Particle (?P<id>\d+)$', line)
if particle and match:
particle['id'] = int(match.group('id'))
if particle and spectrumData:
match = re.match(r'^(?P<key>[0-9.]+)\s+(?P<value>[0-9.]+)$', line)
if match:
particle['spectrum'].append((float(match.group('key')), float(match.group('value'))))
else:
# end of spectrum data, end of particle data
spectrumData = False
spectra[particle['id']] = particle
if not spectrumData:
spectrumData = '[SpectrumData]' == line
return spectra
def mergeSpectraIntoParticles(self, particles, spectra):
missing = []
merged = []
for particleId in particles:
if spectra[particleId]:
particle = particles[particleId]
particle['spectrum'] = spectra[particleId]['spectrum']
merged.append(particle)
else:
missing.append(f"{particleId}")
return missing, merged
def buildDataSet(self, coords, particles):
fname = QtWidgets.QFileDialog.getSaveFileName(
self.parent,
'Create New GEPARD Project',
defaultPath,
'*.pkl'
)[0]
if fname == '':
return False
self.dataSet = DataSet(fname, newProject=True)
self.dataSet.zpositions = np.array([coords['z']])
self.dataSet.heightmap = np.zeros(3)
img = cv2imread_fix(os.path.join(self.path, f'{self.basename}.bmp'))
cv2imwrite_fix(self.dataSet.getImageName(), img)
imgDim = img.shape
# save memory
del img
# use input image as single image acquired in one shot
pixelscale = coords['width'] / imgDim[1]
self.dataSet.imagedim_df = (
coords['width'],
coords['height'],
coords['rotation']
)
self.dataSet.pixelscale_df = pixelscale
self.dataSet.imagedim_bf = (
coords['width'],
coords['height'],
coords['rotation']
)
self.dataSet.pixelscale_bf = pixelscale
self.dataSet.readin = False
# set image center as reference point (assume just one tile)
# in upright coord system the given coords point to center point of image
coords['y'] += coords['height'] / 2
coords['x'] -= coords['width'] / 2
# in upright coord system now the coords point to upper left point of image
# coord of (unrotated) center with origin in upper left corner
m = (coords['width'] / 2, -coords['height'] / 2)
# given rotation, deg to rad
a = -coords['rotation'] # * 180 / math.pi
# rotated center in µm
center = [
m[0] * math.cos(a) + m[1] * math.sin(a) + coords['x'],
-m[0] * math.sin(a) + m[1] * math.cos(a) + coords['y'],
]
self.dataSet.lastpos = np.array(center)
self.dataSet.maxdim = np.array(center)
# we have no zval img, create a very flat one
zimgname = self.dataSet.getZvalImageName()
cv2imwrite_fix(zimgname, np.zeros((imgDim[0], imgDim[1], 3), np.uint8))
self.dataSet.zvalimg = "saved"
sp = []
for i, p in enumerate(particles):
# pos are given in µm, we are still in orig upright coord system
# seedpoints are in image coord sys with y pointing down
x = math.floor((p['x'] - coords['x']) / pixelscale)
# transform y
y = math.floor((coords['y'] - p['y']) / pixelscale)
if 0 < x < imgDim[1] and 0 < y < imgDim[0]:
sp.append([float(x), float(y), 3.])
particles[i]['seedpoint'] = [float(x), float(y), 3.]
else:
QtWidgets.QMessageBox.warning(
self.parent,
'CORRUPT DATASET!',
'At least one particle is outside the image'
)
raise Exception('At least one particle is outside the image')
self.dataSet.seedParticles = particles
self.dataSet.seedpoints = np.array(sp, dtype=np.float)
# convert image
ScenePyramid.createFromFullImage(self.dataSet)
self.dataSet.stickToSeedPoints = True
self.dataSet.opticalScanDone = True
self.dataSet.particleDetectionDone = False
self.dataSet.save()
return True
......@@ -28,6 +28,7 @@ from typing import TYPE_CHECKING
from .dataset import DataSet, loadData
from .instrumentcom.instrumentConfig import InstrumentControl, simulatedRaman
from .zeissimporter import ZeissImporter
from .particlescoutimporter import ParticleScoutImporter
from .helperfunctions import polygoncovering, lightModeSwitchNeeded
from .analysis.particleEditor import ParticleEditor
from .gui.configInstrumentUI import InstrumentConfigWin
......@@ -214,6 +215,11 @@ class SampleView(QtWidgets.QGraphicsView):
if zimp.result() == QtWidgets.QDialog.Accepted:
self.open(zimp.gepardname)
def importPSProject(self, fname):
pimp = ParticleScoutImporter(fname, self)
if pimp.importValid() and pimp.convertData():
self.open(pimp.getDataSet().fname)
def setupLoggerToDataset(self):
"""
Adds a new handler to the logger to create a log also in the dataset directory
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment