...
 
Commits (5)
......@@ -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 <https://www.gnu.org/licenses/>.
"""
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()
......@@ -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
......
......@@ -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
......
......@@ -20,7 +20,7 @@ along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import cv2
from errors import InvalidParticleError
from ...errors import InvalidParticleError
class ShapeClassifier(object):
def __init__(self):
......
......@@ -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):
......
......@@ -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)
......
......@@ -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):
......
......@@ -20,7 +20,7 @@ If not, see <https://www.gnu.org/licenses/>.
"""
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
......
......@@ -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)
......
......@@ -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
......
......@@ -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()
......
......@@ -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_())
......@@ -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:
......
......@@ -21,10 +21,12 @@ If not, see <https://www.gnu.org/licenses/>.
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
......@@ -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
......@@ -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)
......
......@@ -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):
......
......@@ -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 <https://www.gnu.org/licenses/>.
"""
#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)):
......
# -*- 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 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. <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 .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 zn<z0:
zn, z0 = z0, zn
# zeiss z axis has large values at the surface and smaller for large particles
# that is why we need to invert the zpositions: zimg black corresponds to zn
# zimg white corresponds to z0
dataset.zpositions = np.linspace(z0, zn, N)[::-1]-zpc[2]
dataset.heightmap = np.zeros(3)
dataset.signy = 1.
zimg = cv2imread_fix(self.zmapimgname, cv2.IMREAD_GRAYSCALE)
print("zimg shape:", zimg.shape, flush=True)
radius = self.blurspinbox.value()
blur = cv2.GaussianBlur(zimg, (radius, radius), 0)
pshift = self.ramanctrl.getRamanPositionShift()
dataset.pshift = pshift
pixelscale = self.region.scalex*1.e6
# use input image as single image aquired in one shot
dataset.imagedim_df = (zimg.shape[1]*pixelscale,
zimg.shape[0]*pixelscale, 0.0)
dataset.pixelscale_df = pixelscale
dataset.imagedim_bf = (zimg.shape[1]*pixelscale,
zimg.shape[0]*pixelscale, 0.0)
dataset.pixelscale_bf = pixelscale
print('scale:', pixelscale)
print('dim df:', dataset.imagedim_df)
print('dim bf:', dataset.imagedim_bf)
dataset.coordinatetransform = T, pc
# set image center as reference point (assume just one tile)
# in data set (use Zeiss coordinates)
p0center = np.array([self.region.centerx, self.region.centery]) - zpc[:2]
dataset.readin = False
dataset.lastpos = p0center
dataset.maxdim = p0center
zmin, zmax = dataset.zpositions.min(), dataset.zpositions.max()
blur = (blur)*(255.)
blur[blur>255.] = 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
......@@ -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'}
......