# -*- 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 . """ from PyQt5 import QtCore, QtWidgets import numpy as np from multiprocessing import Process, Queue, Event import queue from .imagestitch import imageStacking import os import cv2 from .helperfunctions import cv2imread_fix, cv2imwrite_fix from time import time import datetime import sys def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, logpath='', ishdr=False): if ishdr: merge_mertens = cv2.createMergeMertens() fp = None if logpath != '': try: fp = open(os.path.join(logpath, 'scanlog.txt'), 'a') sys.stderr = fp sys.stdout = fp except IOError: print('separate loging failed', flush=True) pass print('starting new optical scan', flush=True) ramanctrl = controlclass() ramanctrl.connect() zlist = list(enumerate(zpositions)) for i, p in enumerate(grid): x, y = p z = sol[0]*x + sol[1]*y + sol[2] for k, zk in (zlist if i%2==0 else zlist[::-1]): name = f"image_{i}_{k}.bmp" print("time:", time(), flush=True) zik = z+zk assert not np.isnan(zik) print("moving to:", x, y, zik, flush=True) ramanctrl.moveToAbsolutePosition(x, y, zik) if ishdr: img_list = [] fname = os.path.join(path,f"tmp.bmp") values = [5.,25.,100.] for j, val in enumerate(values if (i%2+k%2)%2==0 else reversed(values)): ramanctrl.setBrightness(val) ramanctrl.saveImage(fname) img_list.append(cv2imread_fix(fname)) res_mertens = merge_mertens.process(img_list) res_mertens_8bit = np.clip(res_mertens*255, 0, 255).astype('uint8') cv2imwrite_fix(os.path.join(path,name), res_mertens_8bit) else: ramanctrl.saveImage(os.path.join(path,name)) if stopevent.is_set(): ramanctrl.disconnect() return dataqueue.put(i) ramanctrl.disconnect() if fp is not None: fp.close() def loadAndPasteImage(srcnames, fullimage, fullzval, width, height, rotationvalue, p0, p1, p, halfResolution = False): colimgs = [] for name in srcnames: colimgs.append(cv2.cvtColor(cv2imread_fix(name), cv2.COLOR_BGR2RGB)) img, zval = imageStacking(colimgs) if halfResolution: #halve resolution, if fullimage would become too large otherwise img = cv2.resize(img, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) zval= cv2.resize(zval, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) x, y = p Nx, Ny = int((p1[0]-p0[0]+width)/width*img.shape[1]), int((p0[1]-p1[1]+height)/height*img.shape[0]) + 10 # + 10 because of rotation and hopefully it will be small c, s = np.cos(np.radians(rotationvalue)), np.sin(np.radians(rotationvalue)) dx, dy = (x-p0[0])/width*img.shape[1], (p0[1]-y)/height*img.shape[0] M = np.float32([[c,s,dx],[-s,c,dy]]) if fullimage is not None: cv2.warpAffine(img, M, (Nx, Ny), fullimage, borderMode=cv2.BORDER_TRANSPARENT) cv2.warpAffine(zval, M, (Nx, Ny), fullzval, borderMode=cv2.BORDER_TRANSPARENT) dst = fullimage zval = fullzval else: dst = cv2.warpAffine(img, M, (Nx, Ny)) zval = cv2.warpAffine(zval, M, (Nx, Ny)) return dst, zval class PointCoordinates(QtWidgets.QGridLayout): readPoint = QtCore.pyqtSignal(float, float, float, name='readPoint') def __init__(self, N, ramanctrl, parent=None, names=None): super().__init__(parent) self.dswidgets = [] self.N = 0 self.ramanctrl = ramanctrl self.names = names self.pimageOnly = QtWidgets.QPushButton("Image") self.addWidget(self.pimageOnly, 0, 6, QtCore.Qt.AlignRight) self.pimageOnly.released.connect(QtCore.pyqtSlot()(lambda : self.read(-1))) self.createWidgets(N) @QtCore.pyqtSlot() def createWidgets(self, N, pointsgiven=[]): self.validpoints = [False]*N points = np.zeros((N,3)) def connect(button, index): button.released.connect(QtCore.pyqtSlot()(lambda : self.read(index))) for i in range(self.N,min(N,len(self.dswidgets))): self.itemAtPosition(i+1,0).setVisible(True) self.itemAtPosition(i+1,1).setVisible(True) self.itemAtPosition(i+1,2).setVisible(True) self.itemAtPosition(i+1,3).setVisible(True) self.itemAtPosition(i+1,4).setVisible(True) self.itemAtPosition(i+1,5).setVisible(True) self.itemAtPosition(i+1,6).setVisible(True) for i in range(self.N, N): if self.names is not None: lx = QtWidgets.QLabel(f"{self.names[i]} -> x:") else: lx = QtWidgets.QLabel(f"{i+1} -> x:") ly = QtWidgets.QLabel("y:") lz = QtWidgets.QLabel("z:") wx = QtWidgets.QDoubleSpinBox() wy = QtWidgets.QDoubleSpinBox() wz = QtWidgets.QDoubleSpinBox() wx.setDecimals(1) wy.setDecimals(1) wz.setDecimals(1) wx.setRange(-500_000, 500_000) wy.setRange(-500_000, 500_000) wz.setRange(-500_000, 500_000) wx.setValue(points[i,0]) wy.setValue(points[i,1]) wz.setValue(points[i,2]) self.addWidget(lx, i+1, 0, QtCore.Qt.AlignLeft) self.addWidget(wx, i+1, 1, QtCore.Qt.AlignRight) self.addWidget(ly, i+1, 2, QtCore.Qt.AlignLeft) self.addWidget(wy, i+1, 3, QtCore.Qt.AlignRight) self.addWidget(lz, i+1, 4, QtCore.Qt.AlignLeft) self.addWidget(wz, i+1, 5, QtCore.Qt.AlignRight) pread = QtWidgets.QPushButton("read") connect(pread, i) self.addWidget(pread, i+1, 6, QtCore.Qt.AlignRight) self.dswidgets.append([wx,wy,wz]) for i in range(N, len(self.dswidgets)): self.itemAtPosition(i+1,0).setVisible(False) self.itemAtPosition(i+1,1).setVisible(False) self.itemAtPosition(i+1,2).setVisible(False) self.itemAtPosition(i+1,3).setVisible(False) self.itemAtPosition(i+1,4).setVisible(False) self.itemAtPosition(i+1,5).setVisible(False) self.itemAtPosition(i+1,6).setVisible(False) self.N = N for i, p in pointsgiven: wx, wy, wz = self.dswidgets[i] x, y, z = p wx.setValue(x) wy.setValue(y) wz.setValue(z) self.validpoints[i] = True for i in range(len(pointsgiven), N): wx, wy, wz = self.dswidgets[i] wx.setValue(0) wy.setValue(0) wz.setValue(0) self.update() def read(self, index): x, y, z = self.ramanctrl.getPosition() z = self.ramanctrl.getUserZ() if index>=0: wx, wy, wz = self.dswidgets[index] wx.setValue(x) wy.setValue(y) wz.setValue(z) self.validpoints[index] = True self.readPoint.emit(x,y,z) def getPoints(self): points = np.zeros((self.N, 3), dtype=np.double) for i in range(self.N): if self.validpoints[i]: wx, wy, wz = self.dswidgets[i] points[i,0] = wx.value() points[i,1] = wy.value() points[i,2] = wz.value() else: points[i,:] = np.nan return points class OpticalScan(QtWidgets.QWidget): imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field) boundaryUpdate = QtCore.pyqtSignal() def __init__(self, ramanctrl, dataset, logpath='', parent=None): super().__init__(parent, QtCore.Qt.Window) self.logpath = logpath self.view = parent vbox = QtWidgets.QVBoxLayout() pointgroup = QtWidgets.QGroupBox("Point coordinates [µm]", self) self.ramanctrl = ramanctrl self.dataset = dataset self.positions = [] self.process = None self.points = PointCoordinates(5, self.ramanctrl, self) pointgroup.setLayout(self.points) self.points.readPoint.connect(self.takePoint) self.pareaselect = QtWidgets.QPushButton("Area select", self) label = QtWidgets.QLabel("Size increase:", self) self.radiusincreaseedit = QtWidgets.QDoubleSpinBox(self) self.radiusincreaseedit.setMinimum(-1000) self.radiusincreaseedit.setMaximum(1000) self.radiusincreaseedit.setDecimals(0) self.radiusincreaseedit.setSingleStep(20) self.radiusincreaseedit.setMaximumWidth(100) self.radiusincreaseedit.valueChanged.connect(self.areaSelect) label2 = QtWidgets.QLabel("Maximal focus height [µm]:", self) self.zmaxedit = QtWidgets.QDoubleSpinBox(self) self.zmaxedit.setMinimum(1) self.zmaxedit.setMaximum(1000) self.zmaxedit.setDecimals(0) self.zmaxedit.setValue(50) self.zmaxedit.setMaximumWidth(100) label3 = QtWidgets.QLabel("Focus steps:", self) self.nzedit = QtWidgets.QSpinBox(self) self.nzedit.setRange(2,10) self.nzedit.setValue(3) self.nzedit.setMaximumWidth(100) self.hdrcheck = QtWidgets.QCheckBox("High dynamic range", self) self.hdrcheck.setChecked(False) self.prun = QtWidgets.QPushButton("Run", self) self.pexit = QtWidgets.QPushButton("Cancel", self) self.pareaselect.released.connect(self.areaSelect) self.prun.released.connect(self.run) self.pexit.released.connect(self.stopScan) self.prun.setEnabled(False) self.timelabeltext = "Estimated time to finish: " self.progressbar = QtWidgets.QProgressBar(self) self.progresstime = QtWidgets.QLabel(self.timelabeltext, self) self.progresstime.setEnabled(False) self.progressbar.setEnabled(False) radioGroup = QtWidgets.QGroupBox('Shape') radioLayout = QtWidgets.QHBoxLayout() self.circlerad = QtWidgets.QRadioButton("Circle") self.circlerad.clicked.connect(self.areaSelect) self.rectanglerad = QtWidgets.QRadioButton("Rectangle") self.rectanglerad.setChecked(True) self.rectanglerad.clicked.connect(self.areaSelect) radioLayout.addWidget(self.circlerad) radioLayout.addWidget(self.rectanglerad) radioGroup.setLayout(radioLayout) micModeGroup = QtWidgets.QGroupBox('Mode for Image Acquisition') micModeLayout = QtWidgets.QHBoxLayout() self.df_btn = QtWidgets.QRadioButton('Darkfield') self.df_btn.setChecked(True) self.df_btn.clicked.connect(self.areaSelect) self.bf_btn = QtWidgets.QRadioButton('Brightfield') self.bf_btn.clicked.connect(self.areaSelect) micModeLayout.addWidget(self.df_btn) micModeLayout.addWidget(self.bf_btn) micModeGroup.setLayout(micModeLayout) self.halfResChecker = QtWidgets.QCheckBox('Half resolution') self.halfResChecker.setChecked(False) self.halfResChecker.setToolTip('Enable for very high resolution images.\nFull resolution slows down the scan too much..') self.deleteImgChecker = QtWidgets.QCheckBox('Delete image files after run') self.deleteImgChecker.setChecked(True) self.areaOptionsGroup = QtWidgets.QGroupBox('Area Select Options') areaLayout = QtWidgets.QFormLayout() areaLayout.addRow(radioGroup) areaLayout.addRow(label, self.radiusincreaseedit) areaLayout.addRow(micModeGroup) if not self.view.ramanSwitchNeeded: micModeGroup.setDisabled(True) areaLayout.addRow(self.pareaselect) self.areaOptionsGroup.setLayout(areaLayout) self.areaOptionsGroup.setDisabled(True) furtherOptionsGroup = QtWidgets.QGroupBox('Further Options') furtherOptionsLayout = QtWidgets.QFormLayout() furtherOptionsLayout.addRow(label2, self.zmaxedit) furtherOptionsLayout.addRow(label3, self.nzedit) furtherOptionsLayout.addRow(self.hdrcheck) furtherOptionsLayout.addRow(self.deleteImgChecker) furtherOptionsLayout.addRow(self.halfResChecker) furtherOptionsGroup.setLayout(furtherOptionsLayout) btnLayout = QtWidgets.QHBoxLayout() btnLayout.addWidget(self.prun) btnLayout.addWidget(self.pexit) btnLayout.addStretch() vbox.addWidget(pointgroup) vbox.addWidget(self.areaOptionsGroup) vbox.addWidget(furtherOptionsGroup) vbox.addLayout(btnLayout) vbox.addWidget(self.progresstime) vbox.addWidget(self.progressbar) self.setLayout(vbox) #self.show() self.setVisible(False) @QtCore.pyqtSlot() def stopScan(self): if self.process is not None and self.process.is_alive(): reply = QtWidgets.QMessageBox.question(self, 'Stop optical scan?', "Do you want to terminate the running scan?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.timer.stop() self.processstopevent.set() self.process.join() self.dataqueue.close() self.dataqueue.join_thread() self.view.unblockUI() else: return self.close() @QtCore.pyqtSlot() def areaSelect(self): magn = self.ramanctrl.magn if self.circlerad.isChecked() == True: xym, r = cv2.minEnclosingCircle(np.array([p[:2] for p in self.dataset.fitpoints], dtype=np.float32)) r += self.radiusincreaseedit.value() phi = np.linspace(0, 2*np.pi, magn, endpoint=False) self.dataset.boundary = [[xym[0]+r*np.cos(phii), xym[1]+r*np.sin(phii)] for phii in phi] else: da = self.radiusincreaseedit.value() x0, x1 = self.dataset.fitpoints[:,0].min()-da, self.dataset.fitpoints[:,0].max()+da y0, y1 = self.dataset.fitpoints[:,1].min()-da, self.dataset.fitpoints[:,1].max()+da a = 2*(y1-y0 + x1-x0) nx, ny = max(int(np.round((x1-x0)/a*magn)),2), max(int(np.round((y1-y0)/a*magn)),2) x, dx = np.linspace(x0, x1, nx, endpoint=False, retstep=True) y, dy = np.linspace(y0, y1, ny, endpoint=False, retstep=True) self.dataset.boundary = [[xi, yi] for xi, yi in zip(x,y0*np.ones_like(x))] + \ [[xi, yi] for xi, yi in zip(x1*np.ones_like(y),y)] + \ [[xi, yi] for xi, yi in zip(x[::-1]+dx,y1*np.ones_like(x))] + \ [[xi, yi] for xi, yi in zip(x0*np.ones_like(y),y[::-1]+dy)] self.boundaryUpdate.emit() self.prun.setEnabled(True) def resetDataset(self, ds): self.dataset = ds self.points.createWidgets(5, list(zip(ds.fitindices,ds.fitpoints))) if len(self.dataset.fitindices)>1: # self.pareaselect.setEnabled(True) self.areaOptionsGroup.setEnabled(True) softwarez = self.ramanctrl.getSoftwareZ() if abs(softwarez) >0.1: reply = QtWidgets.QMessageBox.critical(self, 'Software z position nonzero', "The software z position needs to be set to zero."\ " Moving z for %4.0f µm relative to current position. Counteract manually before proceeding!"%(-softwarez), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Abort, QtWidgets.QMessageBox.Abort) if reply == QtWidgets.QMessageBox.Yes: self.ramanctrl.moveZto(0.0) else: QtWidgets.QMessageBox.information(self, "Information", 'Scan may stop if the software z position gets to high!', QtWidgets.QMessageBox.Ok) @QtCore.pyqtSlot(float, float, float) def takePoint(self, x, y, z): if self.dataset.heightmap is not None: reply = QtWidgets.QMessageBox.critical(self, 'Dataset already contains optical scan data', "Continuation will invalidate all previous results! Continue anyway?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply != QtWidgets.QMessageBox.Yes: return self.ramanctrl.saveImage(self.dataset.getTmpImageName()) width, height, rotationvalue = self.ramanctrl.getImageDimensions(self.view.microscopeMode) pshift = self.ramanctrl.getRamanPositionShift() self.dataset.pshift = pshift img = cv2.cvtColor(cv2imread_fix(self.dataset.getTmpImageName()), cv2.COLOR_BGR2RGB) if self.halfResChecker.isChecked(): img = cv2.resize(img, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) self.dataset.imagedim_bf = self.ramanctrl.getImageDimensions('bf') self.dataset.pixelscale_bf = self.dataset.imagedim_bf[0]/img.shape[1] #=imagedim_width/shape[1] self.dataset.imagedim_df = self.ramanctrl.getImageDimensions('df') self.dataset.pixelscale_df = self.dataset.imagedim_df[0]/img.shape[1] #=imagedim_width/shape[1] points = self.points.getPoints() ind = np.isfinite(points[:,0]) self.dataset.fitindices = np.arange(points.shape[0])[ind] points = points[ind,:].copy() self.dataset.fitpoints = points if len(points)>1: self.areaOptionsGroup.setEnabled(True) points = np.concatenate(([[x,y,z]], points), axis=0) p0 = [points[:,0].min(), points[:,1].max()] p1 = [points[:,0].max(), points[:,1].min()] if self.dataset.maxdim is not None: p0 = [min(p0[0], self.dataset.maxdim[0]), max(p0[1], self.dataset.maxdim[1])] p1 = [max(p1[0], self.dataset.maxdim[2]), min(p1[1], self.dataset.maxdim[3])] Nx, Ny = int((p1[0]-p0[0]+width)/width*img.shape[1]), int((p0[1]-p1[1]+height)/height*img.shape[0]) + 10 # + 10 because of rotation and hopefully it will be small c, s = np.cos(np.radians(rotationvalue)), np.sin(np.radians(rotationvalue)) dx, dy = (x-p0[0])/width*img.shape[1], (p0[1]-y)/height*img.shape[0] M = np.float32([[c,s,dx],[-s,c,dy]]) dst = cv2.warpAffine(img, M, (Nx, Ny)) if self.view.imgdata is not None and self.dataset.lastpos is not None: lp = self.dataset.lastpos dx, dy = (lp[0]-p0[0])/width*img.shape[1], (p0[1]-lp[1])/height*img.shape[0] full = self.view.imgdata M = np.float32([[1,0,dx],[0,1,dy]]) try: full = cv2.warpAffine(full, M, (Nx, Ny)) #fails, if image dimensions are >32767x32767px... dst = cv2.max(full, dst) except: QtWidgets.QMessageBox.critical(self, 'Error', 'Image is too large\nPlease repeat with "scale image" checked.') return self.view.imgdata = dst self.dataset.lastpos = p0 self.dataset.maxdim = p0 + p1 self.dataset.readin = False self.imageUpdate.emit(self.view.microscopeMode) @QtCore.pyqtSlot() def run(self): if self.dataset.ramanscansortindex is not None: reply = QtWidgets.QMessageBox.critical(self, 'Dataset already contains raman scan points', "Continuation will invalidate all previous results! Continue anyway?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply != QtWidgets.QMessageBox.Yes: return if self.dataset.readin: reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!', "Coordinate systems might have changed since. Do you want to continue with saved coordinates?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.dataset.readin = False else: return self.view.imparent.ramanSwitch.df_btn.setChecked(self.df_btn.isChecked()) self.view.imparent.ramanSwitch.setDisabled(True) self.view.setMicroscopeMode() if self.df_btn.isChecked(): self.view.dataset.imagescanMode = 'df' else: self.view.dataset.imagescanMode = 'bf' #TODO: #DISABLE OPTION GROUPS when scanning, reactivate upon cancelling points = np.float32(self.dataset.fitpoints) # convert z to software z, which is relative to current user z softwarez = self.ramanctrl.getSoftwareZ() # get current software z points[:,2] += softwarez-self.ramanctrl.getUserZ() Nz = self.nzedit.value() zmaxstack = self.zmaxedit.value() if Nz==1: zmaxstack = 0.0 self.dataset.zpositions = np.array([0.0]) else: self.dataset.zpositions = np.linspace(0, zmaxstack, Nz) width, height, rotationvalue = self.dataset.imagedim_df print("Width, height, rotation:", width, height, rotationvalue) print("Points x:", points[:,0].min(), points[:,0].max()) print("Points y:", points[:,1].min(), points[:,1].max()) print("Points z:", points[:,2].min(), points[:,2].max()) A = np.ones((points.shape[0],3)) A[:,:2] = points[:,:2] b = points[:,2] sol = np.linalg.lstsq(A, b, rcond=None)[0] self.dataset.heightmap = sol print("Fit deviation:", sol[0]*points[:,0]+sol[1]*points[:,1]+sol[2] -points[:,2] ) path = self.dataset.getScanPath() # get zmin and zmax in absolut software z coordinates zmin, zmax = None, None for i, p in enumerate(self.dataset.grid): x, y = p z = sol[0]*x + sol[1]*y + sol[2] if i==0: zmin, zmax = z, z else: if zmin>z: zmin = z if zmax=0: Ngrid = len(self.dataset.grid) names = [] for k in range(len(self.dataset.zpositions)): names.append(os.path.join(self.dataset.getScanPath(),f"image_{i}_{k}.bmp")) width, height, rotationvalue = (self.dataset.imagedim_df if self.view.imparent.ramanSwitch.df_btn.isChecked() else self.dataset.imagedim_bf) p = self.dataset.grid[i] p0, p1 = self.dataset.maxdim[:2], self.dataset.maxdim[2:] self.view.imgdata, self.dataset.zvalimg = loadAndPasteImage(names, self.view.imgdata, self.dataset.zvalimg, width, height, rotationvalue, p0, p1, p, halfResolution = self.halfResChecker.isChecked()) self.progressbar.setValue(i+1) if i>3: timerunning = time()-self.starttime ttot = timerunning*Ngrid/(i+1) time2go = ttot - timerunning self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) self.imageUpdate.emit(self.view.microscopeMode) if i==Ngrid-1: cv2imwrite_fix(self.dataset.getImageName(), cv2.cvtColor(self.view.imgdata, cv2.COLOR_RGB2BGR)) self.dataset.saveZvalImg() self.process.join() self.dataqueue.close() self.dataqueue.join_thread() if self.deleteImgChecker.isChecked(): path = self.dataset.getScanPath() files = os.listdir(path) for file in files: if file.startswith('image_') and (file.endswith('.bmp') or file.endswith('.tiff')): os.remove(os.path.join(path, file)) self.ramanctrl.connect() self.view.saveDataSet() self.view.unblockUI() self.view.switchMode("ParticleDetection") self.progressbar.setValue(0) self.progressbar.setEnabled(False) self.progresstime.setEnabled(False) self.close() return self.timer.start(100.) if __name__ == "__main__": from ramancom.simulatedraman import SimulatedRaman from dataset import DataSet app = QtWidgets.QApplication(sys.argv) ds = DataSet('Test') optscan = OpticalScan(SimulatedRaman(), ds) optscan.show() sys.exit(app.exec_())