diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gepard.py b/__main__.py similarity index 97% rename from gepard.py rename to __main__.py index a53e9ac1874476c5d4b6b0f41206187223993ee0..774c5fece8d63561453704ac406cbbde04fc8a75 100644 --- a/gepard.py +++ b/__main__.py @@ -18,13 +18,13 @@ You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ -from PyQt5 import QtCore, QtWidgets, QtGui -from sampleview import SampleView -from scalebar import ScaleBar -from ramancom.ramancontrol import defaultPath -from ramancom.ramanSwitch import RamanSwitch -from analysis.colorlegend import ColorLegend import os +from PyQt5 import QtCore, QtWidgets, QtGui +from .sampleview import SampleView +from .scalebar import ScaleBar +from .ramancom.ramancontrol import defaultPath +from .ramancom.ramanSwitch import RamanSwitch +from .analysis.colorlegend import ColorLegend class GEPARDMainWindow(QtWidgets.QMainWindow): def __init__(self, logpath): @@ -37,7 +37,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): self.view.imparent = self self.view.ScalingChanged.connect(self.scalingChanged) self.scalebar = ScaleBar(self) - self.legend = ColorLegend() + self.legend = ColorLegend(self) self.ramanSwitch = RamanSwitch(self) self.view.ScalingChanged.connect(self.scalebar.updateScale) @@ -126,7 +126,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): def createActions(self): fname = os.path.join(os.path.split(__file__)[0], - os.path.join("data","brand.png")) + os.path.join('data', 'brand.png')) self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname), "About Particle Measurment", self) self.aboutAct.triggered.connect(self.about) @@ -177,7 +177,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): self.opticalScanAct = QtWidgets.QAction("Optical Scan", self) self.opticalScanAct.setEnabled(False) - self.opticalScanAct.setCheckable(True)# self.importWindow.exec() + self.opticalScanAct.setCheckable(True) self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("OpticalScan"))) self.detectParticleAct = QtWidgets.QAction("Detect Particles", self) @@ -360,4 +360,4 @@ if __name__ == '__main__': gepard.showMaximized() ret = app.exec_() if fp is not None: - fp.close() \ No newline at end of file + fp.close() diff --git a/analysis/database.py b/analysis/database.py index 91ad8de5399506332ffbacd6fd2527b26655ee03..0f3989b2691ec1bedff9c560cdc4e03d59cafc4f 100644 --- a/analysis/database.py +++ b/analysis/database.py @@ -46,7 +46,7 @@ class DataBaseWindow(QtWidgets.QMainWindow): self.path = os.path.join(logpath, 'databases') self.importPath = self.path if not os.path.exists(self.path): - os.mkdir(self.path) + os.makedirs(self.path) self.activeDatabase = None self.activeSpectrum = None self.activeSpectrumName = None diff --git a/analysis/particleCharacterization.py b/analysis/particleCharacterization.py index 9e443322f65919ce6df456ef92d288907f9fc65b..2ac899589c397a8a5d0b3df2516dd1bd86abc96f 100644 --- a/analysis/particleCharacterization.py +++ b/analysis/particleCharacterization.py @@ -26,8 +26,8 @@ from copy import deepcopy from .particleClassification.colorClassification import ColorClassifier from .particleClassification.shapeClassification import ShapeClassifier -from segmentation import closeHolesOfSubImage -from errors import InvalidParticleError +from ..segmentation import closeHolesOfSubImage +from ..errors import InvalidParticleError class ParticleStats(object): longSize = None diff --git a/analysis/particleClassification/shapeClassification.py b/analysis/particleClassification/shapeClassification.py index 46190c5eff23db2be53213191296ceacc21e2766..c036bc85c00d3664f447a288fb6d714b41145ef9 100644 --- a/analysis/particleClassification/shapeClassification.py +++ b/analysis/particleClassification/shapeClassification.py @@ -20,7 +20,7 @@ along with this program, see COPYING. If not, see . """ import cv2 -from errors import InvalidParticleError +from ...errors import InvalidParticleError class ShapeClassifier(object): def __init__(self): diff --git a/analysis/particleContainer.py b/analysis/particleContainer.py index cb4dfadabd54b4bbe03a4b1873c388837743a2e9..4e6d0acccc07b959c03aa9d2cdd365d7db809bb1 100644 --- a/analysis/particleContainer.py +++ b/analysis/particleContainer.py @@ -23,8 +23,8 @@ import operator import os from PyQt5 import QtWidgets -from analysis import importSpectra -from analysis.particleAndMeasurement import Particle, Measurement +from . import importSpectra +from .particleAndMeasurement import Particle, Measurement class ParticleContainer(object): diff --git a/analysis/particleEditor.py b/analysis/particleEditor.py index 46ec15e9454aecd3a6d46e9830930ef41e234a4b..1148b8711da609a601197d91ef38a148289bbd30 100644 --- a/analysis/particleEditor.py +++ b/analysis/particleEditor.py @@ -23,8 +23,8 @@ import numpy as np from PyQt5 import QtWidgets, QtCore from .particlePainter import ParticlePainter -import analysis.particleCharacterization as pc -from errors import NotConnectedContoursError +from . import particleCharacterization as pc +from ..errors import NotConnectedContoursError class ParticleContextMenu(QtWidgets.QMenu): combineParticlesSignal = QtCore.pyqtSignal(list, str) diff --git a/dataset.py b/dataset.py index 12a9c34bc9aac55610c1378f2960786ead1e649e..81546be7b0564925cb80ba162567a44d73415035 100644 --- a/dataset.py +++ b/dataset.py @@ -22,10 +22,15 @@ import os import pickle import numpy as np import cv2 -from helperfunctions import cv2imread_fix, cv2imwrite_fix +import sys +from .helperfunctions import cv2imread_fix, cv2imwrite_fix from copy import copy -from analysis.particleContainer import ParticleContainer -from legacyConvert import legacyConversion, currentVersion +from .analysis.particleContainer import ParticleContainer +from .legacyConvert import legacyConversion, currentVersion +# for legacy pickle import the old module name dataset must be found +# (no relative import) +from . import dataset +sys.modules['dataset'] = dataset def loadData(fname): retds = None @@ -122,6 +127,9 @@ class DataSet(object): self.zpositions = [] # z-positions for optical scan self.heightmap = None self.zvalimg = None + self.coordinatetransform = None # if imported form extern source coordinate system may be rotated + self.signx = 1. + self.signy = -1. # parameters specifically for raman scan self.pshift = None # shift of raman scan position relative to image center @@ -181,8 +189,15 @@ class DataSet(object): def getZval(self, pixelpos): assert self.zvalimg is not None - zp = self.zvalimg[round(pixelpos[1]), round(pixelpos[0])] - z0, z1 = self.zpositions.min(), self.zpositions.max() + i, j = int(round(pixelpos[1])), int(round(pixelpos[0])) + if i>=self.zvalimg.shape[0]: + print('error in getZval:', self.zvalimg.shape, i, j) + i = self.zvalimg.shape[0]-1 + if j>=self.zvalimg.shape[1]: + print('error in getZval:', self.zvalimg.shape, i, j) + j = self.zvalimg.shape[1]-1 + zp = self.zvalimg[i,j] + z0, z1 = self.zpositions[0], self.zpositions[-1] return zp/255.*(z1-z0) + z0 def mapHeight(self, x, y): @@ -195,44 +210,59 @@ class DataSet(object): assert not self.readin p0 = copy(self.lastpos) + if self.coordinatetransform is not None: + z = 0. if len(p)<3 else p[2] + T, pc = self.coordinatetransform + p = (np.dot(np.array([p[0], p[1], z])-pc, T.T)) + if mode == 'df': - p0[0] -= self.imagedim_df[0]/2 - p0[1] += self.imagedim_df[1]/2 - return (p[0] - p0[0])/self.pixelscale_df, (p0[1] - p[1])/self.pixelscale_df + p0[0] -= self.signx*self.imagedim_df[0]/2 + p0[1] -= self.signy*self.imagedim_df[1]/2 + x, y = self.signx*(p[0] - p0[0])/self.pixelscale_df, self.signy*(p[1] - p0[1])/self.pixelscale_df elif mode == 'bf': - p0[0] -= self.imagedim_bf[0]/2 - p0[1] += self.imagedim_bf[1]/2 - return (p[0] - p0[0])/self.pixelscale_bf, (p0[1] - p[1])/self.pixelscale_bf + p0[0] -= self.signx*self.imagedim_bf[0]/2 + p0[1] -= self.signy*self.imagedim_bf[1]/2 + x, y = self.signx*(p[0] - p0[0])/self.pixelscale_bf, self.signy*(p[1] - p0[1])/self.pixelscale_bf + else: - print('mapToPixelMode not understood') - return - - def mapToLength(self, pixelpos, mode='df', force=False): + raise ValueError(f'mapToPixel mode: {mode} not understood') + + return x, y + + def mapToLength(self, pixelpos, mode='df', force=False, returnz=False): if not force: assert not self.readin p0 = copy(self.lastpos) p0[0] += self.coordOffset[0] p0[1] += self.coordOffset[1] - if mode == 'df': - p0[0] -= self.imagedim_df[0]/2 - p0[1] += self.imagedim_df[1]/2 - return (pixelpos[0]*self.pixelscale_df + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale_df) + p0[0] -= self.signx*self.imagedim_df[0]/2 + p0[1] -= self.signy*self.imagedim_df[1]/2 + x, y = (self.signx*pixelpos[0]*self.pixelscale_df + p0[0]), (p0[1] + self.signy*pixelpos[1]*self.pixelscale_df) elif mode == 'bf': - p0[0] -= self.imagedim_bf[0]/2 - p0[1] += self.imagedim_bf[1]/2 - return (pixelpos[0]*self.pixelscale_bf + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale_bf) + p0[0] -= self.signx*self.imagedim_bf[0]/2 + p0[1] -= self.signy*self.imagedim_bf[1]/2 + x, y = (self.signx*pixelpos[0]*self.pixelscale_bf + p0[0]), (p0[1] + self.signy*pixelpos[1]*self.pixelscale_bf) else: raise ValueError(f'mapToLength mode: {mode} not understood') - - def mapToLengthRaman(self, pixelpos, microscopeMode='df', noz=False): - p0x, p0y = self.mapToLength(pixelpos, mode = microscopeMode) - x, y = p0x + self.pshift[0], p0y + self.pshift[1] + z = None - if not noz: + if (returnz and self.zvalimg is not None) or self.coordinatetransform is not None: z = self.mapHeight(x, y) z += self.getZval(pixelpos) + + if self.coordinatetransform is not None: + T, pc = self.coordinatetransform + x, y, z = (np.dot(np.array([x,y,z]), T) + pc) + + if returnz: + return x, y, z + return x, y + + def mapToLengthRaman(self, pixelpos, microscopeMode='df', noz=False): + p0x, p0y, z = self.mapToLength(pixelpos, mode=microscopeMode, returnz=True) + x, y = p0x + self.pshift[0], p0y + self.pshift[1] return x, y, z def newProject(self, fname): diff --git a/detectionview.py b/detectionview.py index 14bf1f6738e5e1bfbe758f2ca6d4c11ec1e45d2a..cd564f1b8c096fa29f791f10cbdbe7ec772e1a3e 100644 --- a/detectionview.py +++ b/detectionview.py @@ -20,7 +20,7 @@ If not, see . """ import numpy as np from PyQt5 import QtCore, QtWidgets, QtGui -from segmentation import Segmentation +from .segmentation import Segmentation from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg import matplotlib.pyplot as plt from threading import Thread diff --git a/helperfunctions.py b/helperfunctions.py index 38bffa4c1578e3893a903f1a9934d289356d10e0..3447a5a842b6277359b3db1c894dc674c7c16fb0 100644 --- a/helperfunctions.py +++ b/helperfunctions.py @@ -23,7 +23,16 @@ import numpy as np import cv2 import os +try: + from skimage.io import imread as skimread + from skimage.io import imsave as skimsave +except ImportError: + skimread = None + skimsave = None + def cv2imread_fix(fname, flags=cv2.IMREAD_COLOR): + if skimread is not None: + return skimread(fname, as_gray=(flags==cv2.IMREAD_GRAYSCALE)) with open(fname, "rb") as fp: cont = fp.read() img = cv2.imdecode(np.fromstring(cont, dtype=np.uint8), flags) @@ -31,6 +40,8 @@ def cv2imread_fix(fname, flags=cv2.IMREAD_COLOR): return None def cv2imwrite_fix(fname, img, params=None): + if skimsave is not None: + skimsave(fname, img) pathname, ext = os.path.splitext(fname) if params is None: ret, data = cv2.imencode(ext, img) diff --git a/legacyConvert.py b/legacyConvert.py index f8f3c5142c551fd5176f0a6eae45298cf370afa8..b44dc1cb08df6401359465a7ec7516d362e1a687 100644 --- a/legacyConvert.py +++ b/legacyConvert.py @@ -23,10 +23,10 @@ import numpy as np import cv2 import os -from helperfunctions import cv2imread_fix, cv2imwrite_fix -from analysis.particleContainer import ParticleContainer -from analysis import particleCharacterization as pc -from errors import InvalidParticleError +from .helperfunctions import cv2imread_fix, cv2imwrite_fix +from .analysis.particleContainer import ParticleContainer +from .analysis import particleCharacterization as pc +from .errors import InvalidParticleError currentVersion = 4 diff --git a/opticalbackground.py b/opticalbackground.py index 75d8cafbed2a96ef1199b33feacae8308916374c..47635c0b6c9c952d8032a76a9e19bcd544f2d4db 100644 --- a/opticalbackground.py +++ b/opticalbackground.py @@ -24,7 +24,7 @@ import cv2 import numpy as np import os -from helperfunctions import cv2imread_fix +from .helperfunctions import cv2imread_fix class BackGroundManager(QtWidgets.QWidget): managerClosed = QtCore.pyqtSignal() diff --git a/opticalscan.py b/opticalscan.py index adca3dc5732315aa858cdc2f13a0a74b5aa72086..6ab990f4f28f08a773629847deb092fadd5171fb 100644 --- a/opticalscan.py +++ b/opticalscan.py @@ -23,14 +23,14 @@ from PyQt5 import QtCore, QtWidgets import numpy as np from multiprocessing import Process, Queue, Event import queue -from imagestitch import imageStacking +from .imagestitch import imageStacking import os import cv2 -from helperfunctions import cv2imread_fix, cv2imwrite_fix +from .helperfunctions import cv2imread_fix, cv2imwrite_fix from time import time import datetime import sys -from opticalbackground import BackGroundManager +from .opticalbackground import BackGroundManager def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, logpath='', ishdr=False): @@ -691,4 +691,4 @@ if __name__ == "__main__": ds = DataSet('Test') optscan = OpticalScan(SimulatedRaman(), ds) optscan.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/ramancom/ramancontrol.py b/ramancom/ramancontrol.py index 7d0174ec982a7b6c4384663b107fb99060e8ff6d..6aa0424ceb1929aba1d499139ee3935a4ca3afa0 100644 --- a/ramancom/ramancontrol.py +++ b/ramancom/ramancontrol.py @@ -43,19 +43,19 @@ except KeyError: pass if interface == "SIMULATED_RAMAN_CONTROL": - from ramancom.simulatedraman import SimulatedRaman + from .simulatedraman import SimulatedRaman RamanControl = SimulatedRaman print("WARNING: using only simulated raman control!") simulatedRaman = True elif interface == "WITEC_CONTROL": - from ramancom.WITecCOM import WITecCOM + from .WITecCOM import WITecCOM RamanControl = WITecCOM RamanControl.magn = int(config["General Microscope Setup"]["magnification"]) # not yet implemented in WITecCOM, but would probably be a good idea... simulatedRaman = False elif interface == "RENISHAW_CONTROL": - from ramancom.renishawcom import RenishawCOM + from .renishawcom import RenishawCOM RamanControl = RenishawCOM RamanControl.magn = int(config["General Microscope Setup"]["magnification"]) try: diff --git a/ramancom/simulatedraman.py b/ramancom/simulatedraman.py index 836a0853c8d2459bb5d95d7072df5b361ff7944e..e4f0210b78b0cc753b0d687c76760f9cb4e0bfc3 100644 --- a/ramancom/simulatedraman.py +++ b/ramancom/simulatedraman.py @@ -21,10 +21,12 @@ If not, see . Simualted Raman interface module for testing without actual raman system connected """ +import sys +stdout = sys.stdout from time import sleep import numpy as np from shutil import copyfile -from ramancom.ramanbase import RamanBase +from .ramanbase import RamanBase class SimulatedRaman(RamanBase): @@ -36,9 +38,9 @@ class SimulatedRaman(RamanBase): self.currentpos = None, 0., 0. self.currentZ = 0. # some plausible data to simulate consecutively changing positions - self.positionlist = np.array([[ 1526. , -1379.9, -131. ], - [ 3762.5, -1197.7, -138.1], - [ 2313.7, -2627.2, -138.1], + self.positionlist = np.array([[ -12012, 13716, -1290], + [ -11955, -9200, -1279], + [ 10978, -9254, -1297], [ 2704.1, -1788.2, -138.1], [ 3884. , -2650.8, -138.1]]) self.znum = 4 @@ -78,6 +80,7 @@ class SimulatedRaman(RamanBase): def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011, debugReturn=False, measurementRunning=False): assert self.connected + print('moving to:', x, y, z, file=stdout) if z is None: self.currentpos = x, y, self.currentpos[2] else: @@ -118,4 +121,4 @@ class SimulatedRaman(RamanBase): print("Scan number:", num) sleep(.1) if num==self.timeseries-1: - self.timeseries = False \ No newline at end of file + self.timeseries = False diff --git a/ramanscanui.py b/ramanscanui.py index 73337a00487c03c0a2a31e14cfcefc2f4cfbeffd..7b683f80ae95b51670a97169f6a315791103d9fc 100644 --- a/ramanscanui.py +++ b/ramanscanui.py @@ -24,7 +24,7 @@ import numpy as np from multiprocessing import Process, Queue, Event import queue from time import time -from external import tsp +from .external import tsp import datetime import sys import os @@ -286,4 +286,4 @@ class RamanScanUI(QtWidgets.QWidget): self.close() return self.timer.start(100.) - \ No newline at end of file + diff --git a/sampleview.py b/sampleview.py index bed70f29c3ac8f60aa4e441641933751d19912e5..ff73ff768e34e620cae674ff2f946a5bea67c85b 100644 --- a/sampleview.py +++ b/sampleview.py @@ -23,20 +23,18 @@ import numpy as np import os import cv2 import time - -from dataset import DataSet, loadData -from ramancom.ramancontrol import RamanControl, simulatedRaman -from opticalscan import OpticalScan -from ramanscanui import RamanScanUI -from detectionview import ParticleDetectionView -from analysis.analysisview import ParticleAnalysis -from zeissimporter import ZeissImporter -from viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo -from analysis.colorlegend import getColorFromNameWithSeed -from helperfunctions import polygoncovering, cv2imread_fix -from ramancom.configRaman import RamanConfigWin -from analysis.particleEditor import ParticleEditor - +from .dataset import DataSet, loadData +from .ramancom.ramancontrol import RamanControl, simulatedRaman +from .opticalscan import OpticalScan +from .ramanscanui import RamanScanUI +from .detectionview import ParticleDetectionView +from .analysis.analysisview import ParticleAnalysis +from .zeissimporter import ZeissImporter +from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo +from .helperfunctions import polygoncovering, cv2imread_fix +from .analysis.colorlegend import getColorFromNameWithSeed +from .analysis.particleEditor import ParticleEditor +from .ramancom.configRaman import RamanConfigWin class SampleView(QtWidgets.QGraphicsView): ScalingChanged = QtCore.pyqtSignal(float) @@ -178,7 +176,12 @@ class SampleView(QtWidgets.QGraphicsView): if mode is None: return assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] + print("switching to mode:", mode, flush=True) self.oscanwidget.setVisible(False) + if self.detectionwidget is not None: + self.detectionwidget.close() + self.detectionwidget.destroy() + self.detectionwidget = None self.ramanwidget.setVisible(False) self.mode = mode self.loadPixmap(self.microscopeMode) diff --git a/segmentation.py b/segmentation.py index 0028a93d796fc1d2bd4bea0a7eb8b717de4ba5ba..e003d933682c545e86cda273f7e24d31f53f6610 100644 --- a/segmentation.py +++ b/segmentation.py @@ -28,7 +28,7 @@ from skimage.feature import peak_local_max from skimage.morphology import watershed from random import random -from errors import InvalidParticleError +from .errors import InvalidParticleError def closeHolesOfSubImage(subimg): diff --git a/viewitems.py b/viewitems.py index 716d521ef4e2360c44b9d3ffcdb6caf516c089cd..0a56d92401c4d0ef437c62bd5b8783035db98b6d 100644 --- a/viewitems.py +++ b/viewitems.py @@ -18,10 +18,9 @@ You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ -#import numpy as np from PyQt5 import QtCore, QtWidgets, QtGui -from analysis.particleEditor import ParticleContextMenu -from analysis.particleCharacterization import getParticleCenterPoint +from .analysis.particleEditor import ParticleContextMenu +from .analysis.particleCharacterization import getParticleCenterPoint class SegmentationContour(QtWidgets.QGraphicsItem): def __init__(self, viewparent, contourData, pos=(0,0)): diff --git a/zeissimporter.py b/zeissimporter.py index 761e697960416c84cf89865457f48e3dc076e459..7b6e42523c255e1ab3293d4a5aed8c8a69d6b280 100644 --- a/zeissimporter.py +++ b/zeissimporter.py @@ -1,270 +1,272 @@ -# -*- coding: utf-8 -*- -""" -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 os -from PyQt5 import QtCore, QtWidgets -from zeissxml import ZeissHandler, make_parser -from opticalscan import PointCoordinates -from helperfunctions import cv2imread_fix, cv2imwrite_fix -from ramancom.ramancontrol import defaultPath -from dataset import DataSet -from scipy.optimize import least_squares -from itertools import permutations -import cv2 -import numpy as np - -class ZeissImporter(QtWidgets.QDialog): - def __init__(self, fname, ramanctrl, parent=None): - super().__init__(parent) - - if not ramanctrl.connect() or not self.readImportData(fname): - msg = QtWidgets.QMessageBox() - msg.setText('Connection failed! Please enable remote control.') - msg.exec() - self.validimport = False - return - else: - self.validimport = True - - self.ramanctrl = ramanctrl - - vbox = QtWidgets.QVBoxLayout() - pointgroup = QtWidgets.QGroupBox('Marker coordinates at Raman spot [µm]', self) - self.points = PointCoordinates(len(self.markers), self.ramanctrl, self, - names = [m.name for m in self.markers]) - self.points.pimageOnly.setVisible(False) - pointgroup.setLayout(self.points) - self.points.readPoint.connect(self.takePoint) - - self.pconvert = QtWidgets.QPushButton('Convert', self) - self.pexit = QtWidgets.QPushButton('Cancel', self) - self.pconvert.released.connect(self.convert) - self.pexit.released.connect(self.reject) - self.pconvert.setEnabled(False) - - btnLayout = QtWidgets.QHBoxLayout() - btnLayout.addStretch() - btnLayout.addWidget(self.pconvert) - btnLayout.addWidget(self.pexit) - - label = QtWidgets.QLabel("Z-Image blur radius", self) - self.blurspinbox = QtWidgets.QSpinBox(self) - self.blurspinbox.setMinimum(3) - self.blurspinbox.setMaximum(99) - self.blurspinbox.setSingleStep(2) - self.blurspinbox.setValue(5) - blurlayout = QtWidgets.QHBoxLayout() - blurlayout.addWidget(label) - blurlayout.addWidget(self.blurspinbox) - blurlayout.addStretch() - - vbox.addWidget(pointgroup) - vbox.addLayout(blurlayout) - vbox.addLayout(btnLayout) - self.setLayout(vbox) - - def readImportData(self, fname): - path = os.path.split(fname)[0] - self.zmapimgname = os.path.join(path, '3D.tif') - self.edfimgname = os.path.join(path, 'EDF.tif') - xmlname = os.path.join(path, '3D.tif_metadata.xml') - - errmsges = [] - if not os.path.exists(self.zmapimgname): - errmsges.append('Depth map image not found: 3D.tif') - if not os.path.exists(self.edfimgname): - errmsges.append('EDF image not found: EDF.tif') - if not os.path.exists(xmlname): - errmsges.append('XML metadata not found: 3D.tif_metadata.xml') - else: - parser = make_parser() - z = ZeissHandler() - parser.setContentHandler(z) - parser.parse(xmlname) - - if len(z.markers)<3: - errmsges.append('Fewer than 3 markers found to adjust coordinates!') - if None in [z.region.centerx, z.region.centery, - z.region.width, z.region.height]: - errmsges.append('Image dimensions incomplete or missing!') - if None in [z.zrange.z0, z.zrange.zn, z.zrange.dz]: - errmsges.append('ZStack information missing or incomplete!') - - if len(errmsges)>0: - QtWidgets.QMessageBox.error(self, 'Error!', - '\n'.join(errmsges), - QtWidgets.QMessageBox.Ok, - QtWidgets.QMessageBox.Ok) - return False - - self.region = z.region - self.zrange = z.zrange - self.markers = z.markers - return True - - @QtCore.pyqtSlot(float, float, float) - def takePoint(self, x, y, z): - points = self.points.getPoints() - if len(points)>=3: - self.pconvert.setEnabled(True) - - @QtCore.pyqtSlot() - def convert(self): - fname = QtWidgets.QFileDialog.getSaveFileName(self, - 'Create New GEPARD Project', defaultPath, '*.pkl')[0] - if fname=='': - return - dataset = DataSet(fname, newProject=True) - T, pc, zpc = self.getTransform() - imgshape, warp_mat = self.convertZimg(dataset, T, pc, zpc) - self.convertImage(dataset, warp_mat) - dataset.save() - self.gepardname = dataset.fname - self.accept() - - def convertImage(self, dataset, warp_mat): - img = cv2imread_fix(self.edfimgname) - img = cv2.warpAffine(img, warp_mat, img.shape[:2][::-1]) - cv2imwrite_fix(dataset.getImageName(), img) - - - def convertZimg(self, dataset, T, pc, zpc): - N = int(round((self.zrange.zn-self.zrange.z0)/self.zrange.dz)) - dataset.zpositions = np.linspace(self.zrange.z0, - self.zrange.zn, N)-zpc[2]+pc[2] - zimg = cv2imread_fix(self.zmapimgname, cv2.IMREAD_GRAYSCALE) - zmdist = zimg.mean() - zm = zmdist/255.*(self.zrange.zn-self.zrange.z0) + self.zrange.z0 - - radius = self.blurspinbox.value() - blur = cv2.GaussianBlur(zimg, (radius, radius), 0) - - pshift = self.ramanctrl.getRamanPositionShift() - dataset.pshift = pshift - pixelscale = self.region.width/zimg.shape[1] - - # use input image as single image aquired in one shot - dataset.imagedim_df = (self.region.width, self.region.height, 0.0) - dataset.pixelscale_df = pixelscale - - dataset.imagedim_bf = (self.region.width, self.region.height, 0.0) - dataset.pixelscale_bf = pixelscale - - # set image center as reference point in data set (transform from Zeiss) - p0 = np.dot((np.array([self.region.centerx, - self.region.centery,zm])-zpc),T)[:2] + pc[:2] - dataset.readin = False - dataset.lastpos = p0 - dataset.maxdim = p0 + p0 - - # pixel triangle for coordinate warping transformation - srcTri = np.array( [[0, 0], [zimg.shape[1] - 1, 0], - [0, zimg.shape[0] - 1]] ).astype(np.float32) - # upper left point (0,0) in Zeiss coordinates: - z0 = np.array([self.region.centerx - self.region.width/2, - self.region.centery + self.region.height/2]) - # transform pixel data to Zeiss coordinates - dstTri = np.array([[p[0]*pixelscale + z0[0], - z0[1] - p[1]*pixelscale, zm] for p in srcTri]).astype(np.double)-zpc - - # transform to Raman coordinates - dstTri = np.dot(dstTri,T) + pc[np.newaxis,:] - - # tilt blur image based on transformend z and adapt zpositions - x = np.linspace(0,1,blur.shape[1]) - y = np.linspace(0,1,blur.shape[0]) - x, y = np.meshgrid(x,y) - zmap = x*(dstTri[1,2]-dstTri[0,2]) + y*(dstTri[2,2]-dstTri[0,2]) + \ - (zimg * ((self.zrange.zn-self.zrange.z0)/255.) - \ - zmdist*((self.zrange.zn-self.zrange.z0)/255.)) - zmin, zmax = zmap.min(), zmap.max() - dataset.zpositions = np.array([zmap.min(), zmap.max()]) - blur = (zmap-zmin)*(255./(zmax-zmin)) - blur[blur>255.] = 255. - blur = np.uint8(blur) - # transform triangle back to pixel - dstTri = np.array([dataset.mapToPixel(p[:2]) for p in dstTri]).astype(np.float32) - - warp_mat = cv2.getAffineTransform(srcTri, dstTri) - - blur = cv2.warpAffine(blur, warp_mat, zimg.shape[::-1]) - zimgname = dataset.getZvalImageName() - cv2imwrite_fix(zimgname, blur) - return zimg.shape, warp_mat - - def getTransform(self): - points = self.points.getPoints() - pshift = self.ramanctrl.getRamanPositionShift() - points[:,0] -= pshift[0] - points[:,1] -= pshift[1] - zpoints = np.array([m.getPos() for m in self.markers], dtype=np.double) - pc = points.mean(axis=0) - zpc = zpoints.mean(axis=0) - - points -= pc[np.newaxis,:] - zpoints -= zpc[np.newaxis,:] - - def getRotMat(angles): - c1, s1 = np.cos(angles[0]), np.sin(angles[0]) - c2, s2 = np.cos(angles[1]), np.sin(angles[1]) - c3, s3 = np.cos(angles[2]), np.sin(angles[2]) - return np.mat([[c1*c3-s1*c2*s3, -c1*s3-s1*c2*c3, s1*s2], - [s1*c3+c1*c2*s3, -s1*s3+c1*c2*c3, -c1*s2], - [s1*s3, s2*c3, c2]]) - - # find the transformation matrix with best fit for small angles in - # [-45°,45°] for all permutation of markers - permbest = None - pointsbest = None - for perm in permutations(range(points.shape[0])): - ppoints = points[perm,:] - - def err(angles_shift): - T = getRotMat(angles_shift[:3]).T.A - return (np.dot(zpoints, T) - angles_shift[np.newaxis,3:] \ - - ppoints).ravel() - - angle = np.zeros(3) - opt = least_squares(err, np.concatenate((angle, np.zeros(3))), - bounds=(np.array([-np.pi/4]*3+[-np.inf]*3), - np.array([np.pi/4]*3+[np.inf]*3)), - method='dogbox') - - if permbest is None or \ - permbest.cost>opt.cost: - print("Current best permutation:", perm, flush=True) - permbest = opt - pointsbest = ppoints - - optangles = permbest.x[:3] - shift = permbest.x[3:] - T = getRotMat(optangles).T.A - e = (np.dot(zpoints, T)-shift[np.newaxis,:]-pointsbest) - print("Transformation angles:", optangles, flush=True) - print("Transformation shift:", shift, flush=True) - print("Transformation err:", e, flush=True) - d = np.linalg.norm(e, axis=1) - if np.any(d>1.): - QtWidgets.QMessageBox.warning(self, 'Warning!', - f'Transformation residuals are large:{d}', - QtWidgets.QMessageBox.Ok, - QtWidgets.QMessageBox.Ok) - return T, pc-shift, zpc - \ No newline at end of file +# -*- coding: utf-8 -*- +""" +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 os +from PyQt5 import QtCore, QtWidgets +from .zeissxml import ZeissHandler, make_parser +from .opticalscan import PointCoordinates +from .helperfunctions import cv2imread_fix, cv2imwrite_fix +from .ramancom.ramancontrol import defaultPath +from .dataset import DataSet +from scipy.optimize import least_squares +from itertools import permutations +import cv2 +import numpy as np + +class ZeissImporter(QtWidgets.QDialog): + def __init__(self, fname, ramanctrl, parent=None): + super().__init__(parent) + + if not ramanctrl.connect() or not self.readImportData(fname): + msg = QtWidgets.QMessageBox() + msg.setText('Connection failed! Please enable remote control.') + msg.exec() + self.validimport = False + return + else: + self.validimport = True + + self.ramanctrl = ramanctrl + + vbox = QtWidgets.QVBoxLayout() + pointgroup = QtWidgets.QGroupBox('Marker coordinates at Raman spot [µm]', self) + self.points = PointCoordinates(len(self.markers), self.ramanctrl, self, + names = [m.name for m in self.markers]) + self.points.pimageOnly.setVisible(False) + pointgroup.setLayout(self.points) + self.points.readPoint.connect(self.takePoint) + + self.pconvert = QtWidgets.QPushButton('Convert', self) + self.pexit = QtWidgets.QPushButton('Cancel', self) + self.pconvert.released.connect(self.convert) + self.pexit.released.connect(self.reject) + self.pconvert.setEnabled(False) + + self.xinvert = QtWidgets.QCheckBox('Invert x-axis') + self.yinvert = QtWidgets.QCheckBox('Invert y-axis') + self.zinvert = QtWidgets.QCheckBox('Invert z-axis') + + btnLayout = QtWidgets.QHBoxLayout() + btnLayout.addStretch() + btnLayout.addWidget(self.pconvert) + btnLayout.addWidget(self.pexit) + + label = QtWidgets.QLabel("Z-Image blur radius", self) + self.blurspinbox = QtWidgets.QSpinBox(self) + self.blurspinbox.setMinimum(3) + self.blurspinbox.setMaximum(99) + self.blurspinbox.setSingleStep(2) + self.blurspinbox.setValue(5) + blurlayout = QtWidgets.QHBoxLayout() + blurlayout.addWidget(label) + blurlayout.addWidget(self.blurspinbox) + blurlayout.addStretch() + + vbox.addWidget(pointgroup) + vbox.addLayout(blurlayout) + vbox.addWidget(self.xinvert) + vbox.addWidget(self.yinvert) + vbox.addWidget(self.zinvert) + vbox.addLayout(btnLayout) + self.setLayout(vbox) + + def readImportData(self, fname): + path = os.path.split(fname)[0] + self.edfimgname, self.zmapimgname, xmlname = '', '', '' + for name in os.listdir(path): + if name.lower().endswith('_meta.xml'): + xmlname = os.path.join(path, name) + elif name.lower().endswith('_c1.tif'): + self.edfimgname = os.path.join(path, name) + elif name.lower().endswith('_c2.tif'): + self.zmapimgname = os.path.join(path, name) + + errmsges = [] + if not os.path.exists(self.zmapimgname): + errmsges.append('Depth map image not found: NAME_c2.tif') + if not os.path.exists(self.edfimgname): + errmsges.append('EDF image not found: NAME_c1.tif') + if not os.path.exists(xmlname): + errmsges.append('XML metadata not found: NAME_meta.xml') + else: + parser = make_parser() + z = ZeissHandler() + parser.setContentHandler(z) + parser.parse(xmlname) + + if len(z.markers)<3: + errmsges.append('Fewer than 3 markers found to adjust coordinates!') + if None in [z.region.centerx, z.region.centery, + z.region.width, z.region.height]: + errmsges.append('Image dimensions incomplete or missing!') + if None in [z.zrange.z0, z.zrange.zn, z.zrange.dz]: + errmsges.append('ZStack information missing or incomplete!') + + if len(errmsges)>0: + QtWidgets.QMessageBox.critical(self, 'Error!', + '\n'.join(errmsges), + QtWidgets.QMessageBox.Ok, + QtWidgets.QMessageBox.Ok) + return False + + self.region = z.region + self.zrange = z.zrange + self.markers = z.markers + print(self.region) + print(self.zrange) + print(self.markers, flush=True) + return True + + @QtCore.pyqtSlot(float, float, float) + def takePoint(self, x, y, z): + points = self.points.getPoints() + if len(points)>=3: + self.pconvert.setEnabled(True) + + @QtCore.pyqtSlot() + def convert(self): + T, pc, zpc, accept = self.getTransform() + if accept: + fname = QtWidgets.QFileDialog.getSaveFileName(self, + 'Create New GEPARD Project', defaultPath, '*.pkl')[0] + if fname=='': + return + dataset = DataSet(fname, newProject=True) + self.convertZimg(dataset, T, pc, zpc) + self.convertImage(dataset) + dataset.save() + self.gepardname = dataset.fname + self.accept() + + def convertImage(self, dataset): + img = cv2imread_fix(self.edfimgname) + cv2imwrite_fix(dataset.getImageName(), img) + + def convertZimg(self, dataset, T, pc, zpc): + N = int(round(abs(self.zrange.zn-self.zrange.z0)/self.zrange.dz)) + z0, zn = self.zrange.z0, self.zrange.zn + if zn255.] = 255. + blur[np.isnan(blur)] = 0. + blur = np.uint8(blur) + + zimgname = dataset.getZvalImageName() + cv2imwrite_fix(zimgname, blur) + dataset.zvalimg = "saved" + + def getTransform(self): + points = self.points.getPoints() + pshift = self.ramanctrl.getRamanPositionShift() + points[:,0] -= pshift[0] + points[:,1] -= pshift[1] + Parity = np.mat(np.diag([-1. if self.xinvert.isChecked() else 1., + -1. if self.yinvert.isChecked() else 1., + -1. if self.zinvert.isChecked() else 1.])) + zpoints = np.array([m.getPos() for m in self.markers], dtype=np.double) + pc = points.mean(axis=0) + zpc = zpoints.mean(axis=0) + + points -= pc[np.newaxis,:] + zpoints -= zpc[np.newaxis,:] + + def getRotMat(angles): + c1, s1 = np.cos(angles[0]), np.sin(angles[0]) + c2, s2 = np.cos(angles[1]), np.sin(angles[1]) + c3, s3 = np.cos(angles[2]), np.sin(angles[2]) + return np.mat([[c1*c3-s1*c2*s3, -c1*s3-s1*c2*c3, s1*s2], + [s1*c3+c1*c2*s3, -s1*s3+c1*c2*c3, -c1*s2], + [s1*s3, s2*c3, c2]]) + + # find the transformation matrix with best fit for small angles in + # [-45°,45°] for all permutation of markers + permbest = None + pointsbest = None + ppoints = points[:,:].copy() + + def err(angles_shift): + T = (getRotMat(angles_shift[:3]).T*Parity).A + return (np.dot(zpoints, T) - angles_shift[np.newaxis,3:] \ + - ppoints).ravel() + + angle = np.zeros(3) + opt = least_squares(err, np.concatenate((angle, np.zeros(3))), + bounds=(np.array([-np.pi/4]*3+[-np.inf]*3), + np.array([np.pi/4]*3+[np.inf]*3)), + method='dogbox') + permbest = opt + pointsbest = ppoints + + optangles = permbest.x[:3] + shift = permbest.x[3:] + T = (getRotMat(optangles).T*Parity).A + e = (np.dot(zpoints, T)-shift[np.newaxis,:]-pointsbest) + print("Transformation angles:", optangles, flush=True) + print("Transformation shift:", shift, flush=True) + print("Transformation err:", e, flush=True) + d = np.linalg.norm(e, axis=1) + accept = True + if np.any(d>1.): + ret = QtWidgets.QMessageBox.warning(self, 'Warning!', + f'Transformation residuals are large:{d}', + QtWidgets.QMessageBox.Ok|QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.Ok) + if ret==QtWidgets.QMessageBox.Cancel: + accept = False + return T, pc-shift, zpc, accept + diff --git a/zeissxml.py b/zeissxml.py index 2abb80843b1d231fd2123ed44ec801a9d05e7482..6dbe249b855df7faf3529704564d2f13ac9412bd 100644 --- a/zeissxml.py +++ b/zeissxml.py @@ -39,13 +39,15 @@ class Region: def __init__(self): self.centerx, self.centery = None, None self.width, self.height = None, None + self.scalex, self.scaley = None, None def __repr__(self): return str(self) def __str__(self): return f'Region center: {self.centerx, self.centery} µm\n' + \ - f'Region size: {self.width, self.height} µm' + f'Region size: {self.width, self.height} µm\n' + \ + f'Scale: {self.scalex, self.scaley} µm/pixel' class ZRange: def __init__(self): @@ -65,6 +67,7 @@ class ZeissHandler(handler.ContentHandler): self.zrange = ZRange() self.intag = False self.subtag = '' + self.scaledim = '' def characters(self, content): if self.intag: @@ -75,20 +78,27 @@ class ZeissHandler(handler.ContentHandler): self.markers.append(Marker(attrs['Id'], attrs['StageXPosition'], attrs['StageYPosition'], attrs['FocusPosition'])) - elif name == 'TileRegion' or name == 'ZStackSetup': + elif name == 'TileRegion' or name == 'ZStackSetup' or name == 'Scaling': self.intag = True if self.intag: self.content = '' - if name in ['First','Last','Interval']: + if self.subtag == 'Items' and name == 'Distance': + self.scaledim = attrs['Id'] + if name in ['First','Last','Interval','Items']: self.subtag = name def endElement(self, name): - if name == 'TileRegion' or name == 'ZStackSetup': + if name == 'TileRegion' or name == 'ZStackSetup' or name == 'Scaling': self.intag = False if self.intag and name == 'CenterPosition': self.region.centerx, self.region.centery = map(float, self.content.split(',')) elif self.intag and name == 'ContourSize': self.region.width, self.region.height = map(float, self.content.split(',')) + elif self.intag and self.subtag == 'Items' and name == 'Value': + if self.scaledim == 'X': + self.region.scalex = float(self.content) + elif self.scaledim == 'Y': + self.region.scaley = float(self.content) elif self.intag and name == 'Value' and \ self.subtag in ['First','Last','Interval']: attrmap = {'First':'z0','Last':'zn','Interval':'dz'}