From ac123cc1e4ac31dc0183cbb8b5c5b61791ddd0cb Mon Sep 17 00:00:00 2001 From: Lars Bittrich Date: Mon, 24 Sep 2018 20:35:42 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20of=20current=20project=20sta?= =?UTF-8?q?tus.=20Some=20modification=20will=20be=20needed=20before=20this?= =?UTF-8?q?=20will=20work=20without=20manual=20adaptations=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++ WITecCOM.py | 289 +++++++++++++++++ dataset.py | 251 +++++++++++++++ detectionview.py | 612 ++++++++++++++++++++++++++++++++++++ externalmodules/__init__.py | 0 externalmodules/setuptsp.py | 43 +++ externalmodules/tsp.pyx | 204 ++++++++++++ gepard.py | 275 ++++++++++++++++ helperfunctions.py | 84 +++++ imagestitch.py | 79 +++++ opticalscan.py | 535 +++++++++++++++++++++++++++++++ ramanbase.py | 68 ++++ ramancontrol.py | 45 +++ ramanscanui.py | 244 ++++++++++++++ sampleview.py | 394 +++++++++++++++++++++++ scalebar.py | 102 ++++++ segmentation.py | 413 ++++++++++++++++++++++++ simulatedraman.py | 117 +++++++ viewitems.py | 229 ++++++++++++++ witectesting.py | 73 +++++ 20 files changed, 4095 insertions(+) create mode 100644 WITecCOM.py create mode 100644 dataset.py create mode 100755 detectionview.py create mode 100644 externalmodules/__init__.py create mode 100755 externalmodules/setuptsp.py create mode 100755 externalmodules/tsp.pyx create mode 100755 gepard.py create mode 100644 helperfunctions.py create mode 100644 imagestitch.py create mode 100755 opticalscan.py create mode 100644 ramanbase.py create mode 100644 ramancontrol.py create mode 100644 ramanscanui.py create mode 100644 sampleview.py create mode 100755 scalebar.py create mode 100755 segmentation.py create mode 100644 simulatedraman.py create mode 100644 viewitems.py create mode 100644 witectesting.py diff --git a/README.md b/README.md index f1ae420..0184319 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ # GEPARD GEPARD - Gepard-Enabled PARticle Detection for Raman microscopes. + +Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für +Polymerforschung Dresden e. V. + +Requirements: + +* python 3.6, PyQt5, OpenCV 3.4.1, numpy 1.14, scikit-image 0.13.1, scipy 1.1.0, + win32com, pythoncom, cython 0.28 + +* we advise the use of Anaconda (python 3.6): https://www.anaconda.com/download + this package contains most of the python libraries + however, opencv and scikit-image are missing + start anaconda prompt and install opencv: + pip install opencv-python + conda install scikit-image + +* we recommend working with a 64bit OS and also a python interpreter compiled + for 64bit as many use cases require a lot of memory (16 GB better 32 GB + recommended) + +* the tsp module in externalmodules can be built with + python setuptsp.py + please note: for this step a valid compiler needs to be installed in the + system; Otherwise use the precompiled tsp-module + +If you plan on using the WITec Raman interface to control your device, please +note: You use this interface at your OWN RISK! Make sure, that no obstacles +block the objective and that you UNDERSTAND and VALIDATE the code, that controls +the microscope! Start with "witectesting.py", which should read and move within +small margins. + +At the moment the program is an executable python script. Copy the folder with +all its content to some place and run (e.g. using anaconda prompt): +python gepard.py + +It is possible to create a windows link file, that executes python with the +gepard script as an argument and the working directory pointing to the folder +containing gepard for convenience. diff --git a/WITecCOM.py b/WITecCOM.py new file mode 100644 index 0000000..9400671 --- /dev/null +++ b/WITecCOM.py @@ -0,0 +1,289 @@ +# -*- 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 +import sys +try: + import pythoncom + import win32com.client +except ImportError: + os.environ["NO_WITEC_CONTROL"] = "True" + +from time import sleep, time +from ramanbase import RamanBase +from socket import gethostname + + +class WITecCOM(RamanBase): + CLSID = "{C45E77CE-3D66-489A-B5E2-159F443BD1AA}" + hostname = "unknown" + def __init__(self, hostname=None): + super().__init__() + if hostname is None: + hostname = gethostname() + self.IBUCSAccess = win32com.client.DispatchEx(self.CLSID, machine=hostname, + clsctx=pythoncom.CLSCTX_REMOTE_SERVER) + + def connect(self): + if not self.IBUCSAccess.RequestWriteAccess(True): + self.connected = False + return False + + IBUCSCore = win32com.client.CastTo(self.IBUCSAccess, 'IBUCSCore') + self.PosXInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SamplePositioning|AbsolutePositionX") + self.PosYInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SamplePositioning|AbsolutePositionY") + self.PosZInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|ScanTable|PositionMicroscopeZ") + self.GoToInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SamplePositioning|GoToPosition") + self.PosXCurInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Software|SamplePositioner|CurrentPositionX") + self.PosYCurInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Software|SamplePositioner|CurrentPositionY") + self.PosZCurInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Software|SamplePositioner|CurrentPositionZ") + self.PosZCurUserInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Hardware|Controller|DataChannels|StepperMotorPosition") + # Get Float Manipulators + self.PosXFloatMan = win32com.client.CastTo(self.PosXInterface, 'IBUCSFloat') + self.PosYFloatMan = win32com.client.CastTo(self.PosYInterface, 'IBUCSFloat') + self.PosZFloatMan = win32com.client.CastTo(self.PosZInterface, 'IBUCSFloat') + self.PosXCurFloatMan = win32com.client.CastTo(self.PosXCurInterface, 'IBUCSStatusContainer') + self.PosYCurFloatMan = win32com.client.CastTo(self.PosYCurInterface, 'IBUCSStatusContainer') + self.PosZCurFloatMan = win32com.client.CastTo(self.PosZCurInterface, 'IBUCSStatusContainer') + self.PosZCurUserFloatMan = win32com.client.CastTo(self.PosZCurUserInterface, 'IBUCSStatusContainer') + self.GoToTrigger = win32com.client.CastTo(self.GoToInterface, 'IBUCSTrigger') + + self.ImageNameInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MultiCommVideoSystem|BitmapFileName") + self.ImageNameMan = win32com.client.CastTo(self.ImageNameInterface, 'IBUCSString') + self.ImageSaveInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MultiCommVideoSystem|SaveColorBitmapToFile") + self.ImageSaveMan = win32com.client.CastTo(self.ImageSaveInterface, 'IBUCSTrigger') + + self.ImageHeightInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|Video|Calibration|ImageHeightMicrons") + self.ImageHeightMan = win32com.client.CastTo(self.ImageHeightInterface, 'IBUCSFloat') + self.ImageWidthInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|Video|Calibration|ImageWidthMicrons") + self.ImageWidthMan = win32com.client.CastTo(self.ImageWidthInterface, 'IBUCSFloat') + self.ImageRotationInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|Video|Calibration|RotationDegrees") + self.ImageRotationMan = win32com.client.CastTo(self.ImageRotationInterface, 'IBUCSFloat') + + self.SequencerStartInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerSingleSpectrum|Start") + self.SequencerStartTrigger = win32com.client.CastTo(self.SequencerStartInterface, 'IBUCSTrigger') + + self.SequencerBusyInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Software|Sequencers|IsASequencerActive") + self.SequencerBusyStatus = win32com.client.CastTo(self.SequencerBusyInterface, 'IBUCSStatusContainer') + + self.TimeSeriesSlowNumMeasurementsInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|AmountOfMeasurements") + self.TimeSeriesSlowNumMeasurementsMan = win32com.client.CastTo(self.TimeSeriesSlowNumMeasurementsInterface, 'IBUCSInt') + self.TimeSeriesSlowNameInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|Naming|DataName") + self.TimeSeriesSlowNameMan = win32com.client.CastTo(self.TimeSeriesSlowNameInterface, 'IBUCSString') + self.TimeSeriesSlowNumAccumulationsInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|SpectrumAcquisition|Accumulations") + self.TimeSeriesSlowNumAccumulationsMan = win32com.client.CastTo(self.TimeSeriesSlowNumAccumulationsInterface, 'IBUCSInt') + self.TimeSeriesSlowIntTimeInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|SpectrumAcquisition|IntegrationTime") + self.TimeSeriesSlowIntTimeMan = win32com.client.CastTo(self.TimeSeriesSlowIntTimeInterface, 'IBUCSFloat') + self.TimeSeriesSlowNumInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|Naming|DataNumber") + self.TimeSeriesSlowNumMan = win32com.client.CastTo(self.TimeSeriesSlowNumInterface, 'IBUCSInt') + self.TimeSeriesSlowModeInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|MeasurementMode") + self.TimeSeriesSlowModeMan = win32com.client.CastTo(self.TimeSeriesSlowModeInterface, 'IBUCSEnum') + self.TimeSeriesSlowNextInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|NextMeasurement") + self.TimeSeriesSlowNextMan = win32com.client.CastTo(self.TimeSeriesSlowNextInterface, 'IBUCSTrigger') + + self.TimeSeriesSlowStartInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|Start") + self.TimeSeriesSlowStartMan = win32com.client.CastTo(self.TimeSeriesSlowStartInterface, 'IBUCSTrigger') + + self.TimeSeriesSlowStopInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|StopSequencer") + self.TimeSeriesSlowStopMan = win32com.client.CastTo(self.TimeSeriesSlowStopInterface, 'IBUCSTrigger') + + self.TimeSeriesSlowIndNextInterface = IBUCSCore.GetSubSystemDefaultInterface("UserParameters|SequencerTimeSeriesSlow|IndexOfNextMeasurement") + self.TimeSeriesSlowIndNextMan = win32com.client.CastTo(self.TimeSeriesSlowIndNextInterface, 'IBUCSInt') + + self.TimeSeriesSlowActivityInterface = IBUCSCore.GetSubSystemDefaultInterface("Status|Software|Sequencers|ActiveSequencer|CurrentActivity") + self.TimeSeriesSlowActivityMan = win32com.client.CastTo(self.TimeSeriesSlowActivityInterface, 'IBUCSStatusContainer') + + self.RamanRelativeXInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|Video|ProbePosition|RelativeX") + self.RamanRelativeXMan = win32com.client.CastTo(self.RamanRelativeXInterface, 'IBUCSFloat') + self.RamanRelativeYInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|Video|ProbePosition|RelativeY") + self.RamanRelativeYMan = win32com.client.CastTo(self.RamanRelativeYInterface, 'IBUCSFloat') + + self.BrightnessInterface = IBUCSCore.GetSubSystemDefaultInterface("MultiComm|MicroscopeControl|WhiteLight|Top|BrightnessPercentage") + self.BrightnessMan = win32com.client.CastTo(self.BrightnessInterface, 'IBUCSFloat') + + self.connected = True + return True + + def getBrightness(self): + assert self.connected + return self.BrightnessMan.GetValue() + + def setBrightness(self, newval): + if newval<0: + newval = 0.0 + elif newval>100.: + newval = 100.0 + self.BrightnessMan.SetValue(newval) + sleep(.1) + + def getRamanPositionShift(self): + rx = .5-self.RamanRelativeXMan.GetValue() # current assumption is, that image center is current position + ry = self.RamanRelativeYMan.GetValue()-.5 + width, height = self.ImageWidthMan.GetValue(), self.ImageHeightMan.GetValue() + return rx*width, ry*height + + + def disconnect(self): + if self.connected: + self.IBUCSAccess.RequestWriteAccess(False) + self.connected = False + + def getPosition(self): + assert self.connected + self.PosXCurFloatMan.Update() + x = self.PosXCurFloatMan.GetSingleValueAsDouble()[1] + self.PosYCurFloatMan.Update() + y = self.PosYCurFloatMan.GetSingleValueAsDouble()[1] + self.PosZCurFloatMan.Update() + z = self.PosZCurFloatMan.GetSingleValueAsDouble()[1] + return x, y, z + + def getSoftwareZ(self): + self.PosZCurFloatMan.Update() + z = self.PosZCurFloatMan.GetSingleValueAsDouble()[1] + return z + + def getUserZ(self): + assert self.connected + self.PosZCurUserFloatMan.Update() + z = self.PosZCurUserFloatMan.GetSingleValueAsDouble()[1] + return z + + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + assert self.connected + initpos = self.getPosition() + # move only if new position is really different; repeat if new position is ignored (happens some times) + while max(abs(initpos[0]-x), abs(initpos[1]-y))>epsxy: + t0 = time() + self.PosXFloatMan.SetValue(x) + self.PosYFloatMan.SetValue(y) + self.GoToTrigger.OperateTrigger() + + # wait till position is found within accuracy of epsxy; check if position changes at all + distance = 2*epsxy + while distance > epsxy:# and (lastpos is None or lastpos!=curpos): + curpos = self.getPosition() + distance = max(abs(curpos[0]-x), abs(curpos[1]-y)) + if ((time()-t0>0.5) and max(abs(curpos[0]-initpos[0]), abs(curpos[1]-initpos[1]))10.): + print("WARNING: signal ignored:", time()-t0, x, y, curpos, initpos) + sys.stdout.flush() + break + sleep(.01) + sleep(.1) + initpos = self.getPosition() + if z is not None: + self.moveZto(z, epsz) + + def moveZto(self, z, epsz=0.011): + assert self.connected + #z = round(z,2) + while abs(self.PosZCurFloatMan.GetSingleValueAsDouble()[1]-z)>epsz: + initpos = self.PosZCurFloatMan.GetSingleValueAsDouble()[1] + t0 = time() + self.PosZFloatMan.SetValue(z) + distance = 2*epsz + while distance > epsz: + self.PosZCurFloatMan.Update() + curpos = self.PosZCurFloatMan.GetSingleValueAsDouble()[1] + distance = abs(curpos-z) + if ((time()-t0>0.5) and abs(curpos-initpos)10.): + print("WARNING: signal z ignored:", time()-t0) + sys.stdout.flush() + break + sleep(.01) + sleep(.1) + + def saveImage(self, fname): + assert self.connected + self.ImageNameMan.SetValue(fname) + self.ImageSaveMan.OperateTrigger() + sleep(.1) + + def getImageDimensions(self): + """ Get the image width and height in um and the orientation angle in degrees. + """ + assert self.connected + width, height = self.ImageWidthMan.GetValue(), self.ImageHeightMan.GetValue() + angle = self.ImageRotationMan.GetValue() + return width, height, angle + + def startSinglePointScan(self): + assert self.connected + self.SequencerStartTrigger.OperateTrigger() + # Wait until sequencer has finished + while True: + self.SequencerBusyStatus.Update() + Busy = self.SequencerBusyStatus.GetSingleValueAsInt()[1] + if not Busy: + break + + def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + assert self.connected + self.timeseries = numberofscans + self.TimeSeriesSlowNameMan.SetValue(label) + self.TimeSeriesSlowNumMeasurementsMan.SetValue(numberofscans) + self.TimeSeriesSlowNumAccumulationsMan.SetValue(accumulations) + self.TimeSeriesSlowIntTimeMan.SetValue(integrtime) + self.TimeSeriesSlowModeMan.SetValueNumeric(0) + self.TimeSeriesSlowNumMan.SetValue(0) + self.TimeSeriesSlowStartMan.OperateTrigger() + sleep(0.1) + t1 = time() + while True: + self.TimeSeriesSlowActivityMan.Update() + valid, act = self.TimeSeriesSlowActivityMan.GetSingleValueAsString() + if act=="Waiting for next Measurement": + break + else: + sleep(0.02) + if time()-t1>3.: + print("Waiting for measurement ready...") + t1 = time() + + + def nextTimeSeriesScan(self, num): + assert self.timeseries + self.TimeSeriesSlowNextMan.OperateTrigger() + # Wait until sequencer has finished + sleep(0.1) + t1 = time() + while True: + ind = self.TimeSeriesSlowIndNextMan.GetValue() + if ind>num: + break + else: + sleep(0.02) + if time()-t1>3.: + print("Waiting for next index...") + t1 = time() + while True: + valid, act = self.TimeSeriesSlowActivityMan.GetSingleValueAsString() + if act=="Waiting for next Measurement": + break + else: + sleep(0.02) + if time()-t1>3.: + print("Waiting for measurement ready...") + t1 = time() + if num==self.timeseries-1: + self.timeseries = False + + \ No newline at end of file diff --git a/dataset.py b/dataset.py new file mode 100644 index 0000000..941a8c0 --- /dev/null +++ b/dataset.py @@ -0,0 +1,251 @@ +# -*- 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 +import pickle +import numpy as np +import cv2 +from helperfunctions import cv2imread_fix, cv2imwrite_fix +from copy import copy + +currentversion = 1 + +def loadData(fname): + retds = None + with open(fname, "rb") as fp: + ds = pickle.load(fp) + ds.fname = fname + ds.readin = True + ds.updatePath() + retds = DataSet(fname) + retds.version = 0 + retds.__dict__.update(ds.__dict__) + if retds.version < currentversion: + retds.legacyConversion() + elif retds.zvalimg=="saved": + retds.loadZvalImg() + return retds + +def saveData(dataset, fname): + with open(fname, "wb") as fp: + # zvalimg is rather large and thus it is saved separately in a tif file + # only onces after its creation + zvalimg = dataset.zvalimg + if zvalimg is not None: + dataset.zvalimg = "saved" + pickle.dump(dataset, fp, protocol=-1) + dataset.zvalimg = zvalimg + +class DataSet(object): + def __init__(self, fname, newProject=False): + self.fname = fname + # parameters specifically for optical scan + self.version = currentversion + self.lastpos = None + self.maxdim = None + self.pixelscale = None # µm / pixel + self.imagedim = None # width, height, angle + self.fitpoints = [] # manually adjusted positions aquired to define the specimen geometry + self.fitindices = [] # which of the five positions in the ui are already known + self.boundary = [] # scan boundary computed by a circle around the fitpoints + manual adjustments + self.grid = [] # scan grid positions for optical scan + self.zpositions = [] # z-positions for optical scan + self.heightmap = None + self.zvalimg = None + + # parameters specifically for raman scan + self.pshift = None # shift of raman scan position relative to image center + self.seedpoints = np.array([]) + self.seeddeletepoints = np.array([]) + self.ramanpoints = [] + self.particlecontours = [] + self.particlestats = [] + self.ramanscansortindex = None + self.ramanscandone = False + + self.readin = True # a value that is always set to True at loadData + # and mark that the coordinate system might be changed in the meantime + self.mode = "prepare" + if newProject: + self.fname = self.newProject(fname) + self.updatePath() + + def saveZvalImg(self): + if self.zvalimg is not None: + cv2imwrite_fix(self.getZvalImageName(), self.zvalimg) + + def loadZvalImg(self): + if os.path.exists(self.getZvalImageName()): + self.zvalimg = cv2imread_fix(self.getZvalImageName(), cv2.IMREAD_GRAYSCALE) + + def legacyConversion(self, recreatefullimage=False): + if self.version==0: + print("Converting legacy version 0 to 1") + print("This may take some time") + + # local imports as these functions are only needed for the rare occasion of legacy conversion + from opticalscan import loadAndPasteImage + + # try to load png and check for detection contours + buggyimage = recreatefullimage + if not buggyimage and os.path.exists(self.getLegacyImageName()): + img = cv2imread_fix(self.getLegacyImageName()) + Nc = len(self.particlecontours) + if Nc>0: + contour = self.particlecontours[Nc//2] + contpixels = img[contour[:,0,1],contour[:,0,0]] + if np.all(contpixels[:,1]==255) and np.all(contpixels[:,2]==0) \ + and np.all(contpixels[:,0]==0): + buggyimage = True + if not buggyimage: + cv2imwrite_fix(self.getImageName(), img) + del img + + if buggyimage: + print("recreating fullimage from grid data") + imgdata = None + zvalimg = None + Ngrid = len(self.grid) + + width, height, rotationvalue = self.imagedim + p0, p1 = self.maxdim[:2], self.maxdim[2:] + for i in range(Ngrid): + print(f"Processing image {i+1} of {Ngrid}") + names = [] + for k in range(len(self.zpositions)): + names.append(os.path.join(self.getScanPath(), f"image_{i}_{k}.bmp")) + p = self.grid[i] + imgdata, zvalimg = loadAndPasteImage(names, imgdata, zvalimg, width, + height, rotationvalue, p0, p1, p) + self.zvalimg = zvalimg + cv2imwrite_fix(self.getImageName(), cv2.cvtColor(imgdata, cv2.COLOR_RGB2BGR)) + del imgdata + self.saveZvalImg() + if "particleimgs" in self.__dict__: + del self.particleimgs + + self.version = 1 + #os.remove(self.getLegacyImageName()) + #os.remove(self.getLegacyDetectImageName()) + #self.save() + # add later conversion for higher version numbers here + + def getSubImage(self, img, index, draw=True): + contour = self.particlecontours[index] + x0, x1 = contour[:,0,0].min(), contour[:,0,0].max() + y0, y1 = contour[:,0,1].min(), contour[:,0,1].max() + subimg = img[y0:y1+1,x0:x1+1].copy() + if draw: + cv2.drawContours(subimg, [contour], -1, (0,255,0), 1) + return subimg + + 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() + return zp/255.*(z1-z0) + z0 + + def mapHeight(self, x, y): + assert not self.readin + assert self.heightmap is not None + return self.heightmap[0]*x + self.heightmap[1]*y + self.heightmap[2] + + def mapToPixel(self, p, force=False): + if not force: + assert not self.readin + p0 = copy(self.lastpos) + p0[0] -= self.imagedim[0]/2 + p0[1] += self.imagedim[1]/2 + return (p[0] - p0[0])/self.pixelscale, (p0[1] - p[1])/self.pixelscale + + def mapToLength(self, pixelpos, force=False): + if not force: + assert not self.readin + p0 = copy(self.lastpos) + p0[0] -= self.imagedim[0]/2 + p0[1] += self.imagedim[1]/2 + return (pixelpos[0]*self.pixelscale + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale) + + def mapToLengthRaman(self, pixelpos, noz=False): + p0x, p0y = self.mapToLength(pixelpos) + x, y = p0x + self.pshift[0], p0y + self.pshift[1] + z = None + if not noz: + z = self.mapHeight(x, y) + z += self.getZval(pixelpos) + return x, y, z + + def newProject(self, fname): + path = os.path.split(fname)[0] + name = os.path.splitext(os.path.basename(fname))[0] + newpath = os.path.join(path, name) + fname = os.path.join(newpath, name + ".pkl") + if not os.path.exists(newpath): + os.mkdir(newpath) # for new projects a directory will be created + elif os.path.exists(fname): # if this project is already there, load it instead + self.__dict__.update(loadData(fname).__dict__) + return fname + + def getScanPath(self): + scandir = os.path.join(self.path, "scanimages") + if not os.path.exists(scandir): + os.mkdir(scandir) + return scandir + + def updatePath(self): + self.path = os.path.split(self.fname)[0] + self.name = os.path.splitext(os.path.basename(self.fname))[0] + + def getImageName(self): + return os.path.join(self.path, "fullimage.tif") + + def getZvalImageName(self): + return os.path.join(self.path, "zvalues.tif") + + def getLegacyImageName(self): + return os.path.join(self.path, "fullimage.png") + + def getLegacyDetectImageName(self): + return os.path.join(self.path, "detectimage.png") + + def getDetectImageName(self): + raise NotImplementedError("No longer implemented due to change in API") + + def getTmpImageName(self): + return os.path.join(self.path, "tmp.bmp") + + def saveParticleData(self): + if len(self.ramanscansortindex)>0: + data = [] + for i in self.ramanscansortindex: + data.append(list(self.ramanpoints[i])+list(self.particlestats[i])) + data = np.array(data) + data[:,0], data[:,1], z = self.mapToLengthRaman((data[:,0], data[:,1]), noz=True) + data[:,2:7] *= self.pixelscale + header = "x [µm], y [µm], length [µm], height [µm], length_ellipse [µm], height_ellipse [µm]" + if data.shape[1]>6: + header = header + ", area [µm^2]" + data[:,6] *= self.pixelscale + np.savetxt(os.path.join(self.path, "particledata.txt"), data, + header=header) + + def save(self): + saveData(self, self.fname) + \ No newline at end of file diff --git a/detectionview.py b/detectionview.py new file mode 100755 index 0000000..f78ae50 --- /dev/null +++ b/detectionview.py @@ -0,0 +1,612 @@ +# -*- 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 numpy as np +from PyQt5 import QtCore, QtWidgets, QtGui +from segmentation import Segmentation +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +import matplotlib.pyplot as plt +from threading import Thread + +Nscreen = 1000 + +class HistWidget(QtWidgets.QWidget): + def __init__(self, histcallback, curvecallback, parent=None): + super().__init__(parent) + + self.updateCallbacks(histcallback, curvecallback) + self.points = np.array([[50,0],[100,150],[200,255]]) + + self.fig = plt.Figure() + self.canvas = FigureCanvasQTAgg(self.fig) + self.canvas.setParent(self) + + self.ax = self.fig.add_subplot(111, autoscale_on=False) + self.ax.axis("off") + self.fig.subplots_adjust(left=0.05, top=0.98, bottom=0.05, right=0.995) + self.canvas.mpl_connect('pick_event', self.moveControlPoint) + self.canvas.mpl_connect('button_release_event', self.releaseAxes) + self.canvas.mpl_connect('motion_notify_event', self.moveAxes) + + self.moveind = None + + self.plot() + self.setMaximumSize(200,200) + + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(self.canvas) + self.setLayout(vbox) + + def value(self): + return self.points + + def moveAxes(self, event): + if (event.inaxes==self.ax) and self.moveind is not None: + x = event.xdata + y = event.ydata + if self.moveind == 0: + self.points[0,0] = max(0,min(x, self.points[1,0])) + elif self.moveind == 1: + self.points[1,0] = max(self.points[0,0]+1, min(x, self.points[2,0]-1)) + self.points[1,1] = max(0, min(y, 255)) + else: + self.points[2,0] = max(self.points[1,0]+1, min(x, 255)) + self.plot() + + def releaseAxes(self, event): + self.moveind = None + + def plot(self): + self.ax.lines = [] + self.ax.collections = [] + self.ax.artists = [] + + hist = self.histcallback()[:,0] + hist /= hist.max() + hist *= 255 + hist[0] = 0 + hist[-1] = 0 + xarr, arr = self.curvecallback(self.points) + + self.ax.fill(xarr, hist, color=(.7,.7,.7)) + self.ax.plot([0,0], [255,255], "k-") + self.ax.plot(xarr, arr, "b-") + self.ax.plot(self.points[:,0], self.points[:,1], "go", picker=5) + self.ax.set_xlim(0,255) + self.ax.set_ylim(0,255) + + self.canvas.draw() + self.repaint() + + def updateCallbacks(self, histcallback, curvecallback): + self.histcallback = histcallback + self.curvecallback = curvecallback + + def moveControlPoint(self, event): + if self.moveind is None: + self.moveind = event.ind + + +class ImageView(QtWidgets.QLabel): + seedChanged = QtCore.pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.overlay = None + self.drag = False + self.alpha = .8 + self.contours = [] + self.seedradius = 3 + self.showseedpoints = True + self.measpoints = [] + self.seedpoints = [] + self.seeddeletepoints = [] + + @QtCore.pyqtSlot(int) + def resetAlpha(self, num): + self.alpha = num/100. + self.update() + + @QtCore.pyqtSlot(int) + def setSeedRadius(self, num): + self.seedradius = num + self.update() + + @QtCore.pyqtSlot(int) + def changeSeedDisplay(self, state): + self.showseedpoints = (state==QtCore.Qt.Checked) + self.update() + + def showStep(self, img, imgtype): + self.overlay = None + if imgtype==0: + img = img.repeat(3).reshape(img.shape[0],img.shape[1],3) + self.setOverlay(img) + elif imgtype==1: + levels = img + img = np.zeros(tuple(levels.shape)+(3,), dtype=np.uint8) + img[:,:,0][np.nonzero(levels&1)] = 255. + img[:,:,1][np.nonzero(levels&2)] = 255. + self.setOverlay(img) + self.update() + + def removeSeeds(self,p0): + p0 = np.array([p0]) + if len(self.seedpoints)>0: + arr = np.array(self.seedpoints) + d = np.linalg.norm(arr-p0, axis=1) + ind = d>self.seedradius + self.seedpoints = arr[ind,:].tolist() + if len(self.seeddeletepoints)>0: + arr = np.array(self.seeddeletepoints) + d = np.linalg.norm(arr-p0, axis=1) + ind = d>self.seedradius + self.seeddeletepoints = arr[ind,:].tolist() + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.LeftButton and event.modifiers() & QtCore.Qt.ControlModifier: + if event.modifiers()&QtCore.Qt.ShiftModifier: + self.drag = "delete" + elif event.modifiers()&QtCore.Qt.AltModifier: + self.drag = "remove" + else: + self.drag = "add" + p0 = event.pos() + if self.drag=="add": + self.seedpoints.append([p0.x(),p0.y()]) + elif self.drag=="delete": + self.seeddeletepoints.append([p0.x(),p0.y()]) + else: + self.removeSeeds([p0.x(),p0.y()]) + + self.update() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self.drag: + p0 = event.pos() + if self.drag=="add": + self.seedpoints.append([p0.x(),p0.y()]) + elif self.drag=="delete": + self.seeddeletepoints.append([p0.x(),p0.y()]) + else: + self.removeSeeds([p0.x(),p0.y()]) + self.update() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if self.drag: + self.seedChanged.emit() + self.drag = False + super().mouseReleaseEvent(event) + + def clearData(self): + self.contours = [] + self.measpoints = [] + self.seedpoints = [] + self.seeddeletepoints = [] + self.overlay = None + + def updateSeedPoints(self, seedpoints=[], seeddeletepoints=[]): + self.seedpoints = seedpoints + self.seeddeletepoints = seeddeletepoints + + def updateDetectionResults(self, contours, measpoints): + self.overlay = None + cp = [] + for c in contours: + polygon = QtGui.QPolygonF() + for ci in c: + polygon.append(QtCore.QPointF(ci[0,0],ci[0,1])) + cp.append(polygon) + + self.contours = cp + self.measpoints = measpoints + self.update() + + def setOverlay(self, img): + height, width, channel = img.shape + assert channel==3 + bytesPerLine = 3 * width + pix = QtGui.QPixmap() + pix.convertFromImage(QtGui.QImage(img.data, width, height, + bytesPerLine, QtGui.QImage.Format_RGB888)) + self.overlay = pix + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.drawPixmap(0, 0, self.pixmap()) + painter.setOpacity(self.alpha) + if self.overlay is not None: + painter.drawPixmap(0, 0, self.overlay) + else: + painter.setPen(QtCore.Qt.blue) + painter.setBrush(QtCore.Qt.green) + for c in self.contours: + painter.drawPolygon(c) + painter.setPen(QtCore.Qt.red) + painter.setBrush(QtCore.Qt.red) + for p in self.measpoints: + painter.drawEllipse(p[0]-2, p[1]-2, 5, 5) + + if self.showseedpoints: + r = self.seedradius + painter.setPen(QtCore.Qt.magenta) + painter.setBrush(QtCore.Qt.magenta) + for p in self.seeddeletepoints: + painter.drawEllipse(p[0]-r, p[1]-r, 2*r, 2*r) + + painter.setPen(QtCore.Qt.white) + painter.setBrush(QtCore.Qt.white) + for p in self.seedpoints: + painter.drawEllipse(p[0]-r, p[1]-r, 2*r, 2*r) + +class ParticleDetectionView(QtWidgets.QWidget): + imageUpdate = QtCore.pyqtSignal(name='imageUpdate') + detectionFinished = QtCore.pyqtSignal(name='detectionFinished') + + def __init__(self, img, dataset, parent=None): + super().__init__(parent, QtCore.Qt.Window) + self.dataset = dataset + self.img = img + self.imgclip = 0,0,0,0 + self.seg = Segmentation() + self.thread = None + + vbox = QtWidgets.QVBoxLayout() + hbox = QtWidgets.QHBoxLayout() + + def makeShowLambda(name): + def f(): return self.detectShow(name) + return QtCore.pyqtSlot()(f) + def makeValueLambda(objmethod): + return lambda : objmethod() + + self.imglabel = ImageView(self) + self.imglabel.seedChanged.connect(self.seedChanged) + self.drag = False + self.seedradiusedit = QtWidgets.QSpinBox(self) + self.seedradiusedit.setMinimum(1) + self.seedradiusedit.valueChanged.connect(self.imglabel.setSeedRadius) + self.seedradiusedit.valueChanged.connect(self.updateImageSeeds) + self.seedradiusedit.setValue(3) + self.showseedpoints = QtWidgets.QCheckBox("Show manual seed points", self) + self.showseedpoints.stateChanged.connect(self.imglabel.changeSeedDisplay) + self.showseedpoints.setChecked(True) + self.setImageCenter() + group = QtWidgets.QGroupBox("Detection settings", self) + grid = QtWidgets.QGridLayout() + self.parameters = [] + for i, p in enumerate(self.seg.parlist): + label, colstretch = None, 1 + if p.name == "points": + paramui = HistWidget(lambda : self.seg.calculateHist(self.seg.convert2Gray(self.subimg)), + self.seg.calculateHistFunction, self) + valuefunc = makeValueLambda(paramui.value) + colstretch = 2 + if p.dtype == np.bool: + paramui = QtWidgets.QCheckBox(p.helptext, self) + paramui.setChecked(p.value) + valuefunc = makeValueLambda(paramui.isChecked) + colstretch = 2 + elif p.dtype == int or p.dtype == float: + label = QtWidgets.QLabel(p.helptext, self) + if p.dtype == int: + paramui = QtWidgets.QSpinBox(self) + else: + paramui = QtWidgets.QDoubleSpinBox(self) + paramui.setDecimals(p.decimals) + if p.valrange[0] is not None: + paramui.setMinimum(p.valrange[0]) + if p.valrange[1] is not None: + paramui.setMaximum(p.valrange[1]) + paramui.setSingleStep(p.stepsize) + paramui.setValue(p.value) + paramui.setMinimumWidth(70) + valuefunc = makeValueLambda(paramui.value) + elif p.dtype is None: + label = QtWidgets.QLabel(p.helptext, self) + paramui = None + if paramui is not None: + self.parameters.append([paramui, p.name, valuefunc]) + if colstretch == 1: + grid.addWidget(paramui, i, 1, QtCore.Qt.AlignLeft) + else: + grid.addWidget(paramui, i, 0, 1, 2, QtCore.Qt.AlignLeft) + if label is not None: + grid.addWidget(label, i, 0, QtCore.Qt.AlignLeft) + if p.show is True: + pshow = QtWidgets.QPushButton(">", self) + pshow.released.connect(makeShowLambda(p.name)) + grid.addWidget(pshow, i, 2, QtCore.Qt.AlignRight) + + label = QtWidgets.QLabel("Seed radius", self) + grid.addWidget(label, i+1, 0, QtCore.Qt.AlignLeft) + grid.addWidget(self.seedradiusedit, i+1, 1, QtCore.Qt.AlignLeft) + grid.addWidget(self.showseedpoints, i+2, 0, 1, 2, QtCore.Qt.AlignLeft) + grid.addWidget(QtWidgets.QLabel("Use Ctrl for seeds and Ctrl+Shift for delete points"), i+3, 0, 1, 2, QtCore.Qt.AlignLeft) + grid.addWidget(QtWidgets.QLabel("Ctrl+Alt removes seeds near cursor"), i+4, 0, 1, 2, QtCore.Qt.AlignLeft) + group.setLayout(grid) + vbox.addWidget(group) + + self.slider = QtWidgets.QSlider(self) + self.slider.setRange(0,100) + self.slider.setValue(80) + self.slider.setOrientation(QtCore.Qt.Horizontal) + self.slider.sliderMoved.connect(self.imglabel.resetAlpha) + vbox.addWidget(self.slider) + hbox2 = QtWidgets.QHBoxLayout() + self.pdetectsub = QtWidgets.QPushButton("Detect", self) + self.pdetectall = QtWidgets.QPushButton("Detect all", self) + self.pclear = QtWidgets.QPushButton("Clear detection", self) + self.pdetectsub.released.connect(makeShowLambda(None)) + self.pdetectall.released.connect(self.detectParticles) + self.pclear.released.connect(self.clearDetection) + hbox2.addWidget(self.pdetectsub) + hbox2.addWidget(self.pdetectall) + hbox2.addWidget(self.pclear) + vbox.addLayout(hbox2) + + vbox.addStretch() + hbox.addLayout(vbox,0) + hbox.addWidget(self.imglabel,1) + + self.setLayout(hbox) + self.setWindowTitle("Particle Detection") + + @QtCore.pyqtSlot() + def seedChanged(self): + seedradius = self.seedradiusedit.value() + def clipdata(arr0, arr1, p0, n1, n2, m1, m2): + if arr0.shape[0]>0: + ind = (arr0[:,0]>m1-seedradius)&(arr0[:,0]<=m2+seedradius) + ind &= (arr0[:,1]>n1-seedradius)&(arr0[:,1]<=n2+seedradius) + arr0 = arr0[~ind,:] + else: + arr0 = arr0.reshape(0,2) + if arr1.shape[0]>0: + arr1 += p0 + else: + arr1 = arr1.reshape(0,2) + return np.concatenate((arr0, arr1), axis=0) + + if self.dataset is not None: + n1,n2,m1,m2 = self.imgclip + p0 = np.array([[m1,n1]], dtype=np.int32) + + self.dataset.seedpoints = clipdata(self.dataset.seedpoints, + np.array(self.imglabel.seedpoints, dtype=np.int32), + p0, n1, n2, m1, m2) + self.dataset.seeddeletepoints = clipdata(self.dataset.seeddeletepoints, + np.array(self.imglabel.seeddeletepoints, dtype=np.int32), + p0, n1, n2, m1, m2) + self.dataset.save() + + @QtCore.pyqtSlot() + def updateImageSeeds(self): + if self.dataset is not None: + seedradius = self.seedradiusedit.value() + n1,n2,m1,m2 = self.imgclip + p0 = np.array([[m1,n1]], dtype=np.int32) + seedpoints = [] + seeddeletepoints = [] + arr1 = self.dataset.seedpoints + if arr1.shape[0]>0: + ind = (arr1[:,0]>m1-seedradius)&(arr1[:,0]<=m2+seedradius) + ind &= (arr1[:,1]>n1-seedradius)&(arr1[:,1]<=n2+seedradius) + if np.any(ind): + seedpoints = (arr1[ind,:]-p0).tolist() + arr2 = self.dataset.seeddeletepoints + if arr2.shape[0]>0: + ind = (arr2[:,0]>m1-seedradius)&(arr2[:,0]<=m2+seedradius) + ind &= (arr2[:,1]>n1-seedradius)&(arr2[:,1]<=n2+seedradius) + if np.any(ind): + seeddeletepoints = (arr2[ind,:]-p0).tolist() + self.imglabel.updateSeedPoints(seedpoints, seeddeletepoints) + + def closeEvent(self, event): + self.detectionFinished.emit() + self.destroy() + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.RightButton: + self.drag = True + self.p0 = event.pos() + + def mouseMoveEvent(self, event): + if self.drag: + dp = self.p0-event.pos() + self.lastmove = (self.lastcenter[0]+dp.x(),self.lastcenter[1]+dp.y()) + self.setImageCenter(self.lastmove) + + def mouseReleaseEvent(self, event): + if self.drag: + self.lastcenter = self.lastmove + self.drag = False + + def setDataSet(self, ds): + self.dataset = ds + self.updateImageSeeds() + + def setImageCenter(self, center=None): + if center is None: + centerx = self.img.shape[1]//2 + centery = self.img.shape[0]//2 + else: + centerx, centery = center + if self.img.shape[0]self.img.shape[0]: + n1 -= n2-self.img.shape[0] + if n1<0: n1 = 0 + n2 = self.img.shape[0] + if m1<0: + m2 -= m1 + m1 = 0 + if m2>self.img.shape[1]: + m1 -= m2-self.img.shape[1] + if m1<0: m1 = 0 + m2 = self.img.shape[1] + sub = self.img[n1:n2,m1:m2,:].copy() + self.imgclip = n1,n2,m1,m2 + self.subimg = sub + height, width, channel = sub.shape + bytesPerLine = 3 * width + pix = QtGui.QPixmap() + pix.convertFromImage(QtGui.QImage(sub.data, width, height, + bytesPerLine, QtGui.QImage.Format_RGB888)) + self.imglabel.clearData() + self.imglabel.setPixmap(pix) + self.updateImageSeeds() + + def detectShow(self, showname): + img = self.subimg.copy() + kwargs = {} + for ui, name, valuefunc in self.parameters: + kwargs[name] = valuefunc() + self.seg.setParameters(**kwargs) + seedradius = self.seedradiusedit.value() + if showname is not None: + stepImg, imgtype = self.seg.apply2Image(img, self.imglabel.seedpoints, + self.imglabel.seeddeletepoints, + seedradius, + return_step=showname) + self.imglabel.showStep(stepImg, imgtype) + else: + measurementpoints, contours, particlestats = self.seg.apply2Image(img, + self.imglabel.seedpoints, + self.imglabel.seeddeletepoints, + seedradius) + self.imglabel.updateDetectionResults(contours, measurementpoints) + + @QtCore.pyqtSlot() + def clearDetection(self): + if self.dataset is not None: + self.dataset.ramanpoints = [] + self.dataset.particlecontours = [] + self.dataset.particlestats = [] + self.dataset.ramanscansortindex = [] + self.dataset.ramanscandone = False + self.dataset.mode = "opticalscan" + self.dataset.save() + self.imageUpdate.emit() + + @QtCore.pyqtSlot() + def cancelThread(self): + self.seg.cancelcomputation = True + self.thread.join() + self.seg.cancelcomputation = False + self.threadrunning = False + + def blockUI(self): + self.pdetectsub.setEnabled(False) + self.pclear.setEnabled(False) + + def unBlockUI(self): + self.pdetectsub.setEnabled(True) + self.pclear.setEnabled(True) + + @QtCore.pyqtSlot() + def detectParticles(self): + if self.thread is not None and self.thread.is_alive(): + self.cancelThread() + return + self.blockUI() + self.pdetectall.setText("Cancel") + self.threadrunning = True + self.thread = Thread(target=self._worker) + self.thread.start() + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.checkOnComputation) + self.timer.setSingleShot(True) + self.timer.start(100.) + + @QtCore.pyqtSlot() + def checkOnComputation(self): + if self.thread is not None: + if not self.threadrunning: + #self.thread.join() + self.thread = None + self.unBlockUI() + self.pdetectall.setText("Detect all") + self.imageUpdate.emit() + if self.dataset is not None: + self.setWindowTitle(str(len(self.dataset.ramanpoints)) + " Particles") + else: + self.timer.start(100.) + + def _worker(self): + kwargs = {} + seedpoints, deletepoints = [], [] + if self.dataset is not None: + seedpoints = self.dataset.seedpoints + deletepoints = self.dataset.seeddeletepoints + for ui, name, valuefunc in self.parameters: + kwargs[name] = valuefunc() + seedradius = self.seedradiusedit.value() + self.seg.setParameters(**kwargs) + measurementpoints, contours, particlestats = self.seg.apply2Image(self.img, + seedpoints, + deletepoints, + seedradius) + if measurementpoints is None: # computation was canceled + return + if self.dataset is not None: + self.dataset.ramanscandone = False + self.dataset.ramanpoints = measurementpoints + self.dataset.particlecontours = contours + self.dataset.particlestats = particlestats + self.dataset.ramanscansortindex = [] + self.dataset.mode = "prepareraman" + self.dataset.save() + + self.threadrunning = False + + +if __name__ == "__main__": + import cv2 + import sys + from helperfunctions import cv2imread_fix + from dataset import DataSet + from time import time + + fname = r"D:\Projekte\Mikroplastik\Waferreinigung_Kerzenfilter\WaferMilliQH118\fullimage.tif" + app = QtWidgets.QApplication(sys.argv) + + t2 = time() + img = cv2imread_fix(fname) + t3 = time() + print("OpenCV read:", t3-t2) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + view = ParticleDetectionView(img, None) + ds = DataSet("dummy") + view.setDataSet(ds) + view.show() + app.exec_() \ No newline at end of file diff --git a/externalmodules/__init__.py b/externalmodules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/externalmodules/setuptsp.py b/externalmodules/setuptsp.py new file mode 100755 index 0000000..0ea7cee --- /dev/null +++ b/externalmodules/setuptsp.py @@ -0,0 +1,43 @@ +# -*- 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 . +""" + +try: + from setuptools import setup + from setuptools import Extension +except ImportError: + from distutils.core import setup + from distutils.extension import Extension +from Cython.Build import cythonize +import numpy as np +import sys + +if len(sys.argv)==1: + sys.argv.append("build_ext") + sys.argv.append("--inplace") + +ext = Extension("tsp", ["tsp.pyx"], + extra_compile_args=['-O3'],) + +setup( + name = "optimized tsp-module", + ext_modules = cythonize([ext]), # accepts a glob pattern + include_dirs=[np.get_include()] +) \ No newline at end of file diff --git a/externalmodules/tsp.pyx b/externalmodules/tsp.pyx new file mode 100755 index 0000000..06b9747 --- /dev/null +++ b/externalmodules/tsp.pyx @@ -0,0 +1,204 @@ +# -*- 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 numpy as np +from numpy.linalg import norm +from libc.math cimport exp, sqrt +from libc.stdlib cimport rand, RAND_MAX + +cimport numpy as np +cimport cython + +DTYPE = np.float +ctypedef np.float_t DTYPE_t +ctypedef np.int32_t INT32_t +ctypedef np.int64_t INT64_t + +@cython.cdivision(True) +cdef double pyrand(): + return rand()/float(RAND_MAX) + +@cython.cdivision(True) +cdef int randint(int N): + return rand()%N + +@cython.boundscheck(False) # assume: no index larger than N-1 +@cython.wraparound(False) # assume: no neg. index +cdef double getdist(int n1, int n2, np.ndarray[DTYPE_t, ndim=2] points): + cdef double dx, dy + cdef int N=points.shape[0] + if n1==N or n2==N: + return 0.0 + dx = points[n1,0]-points[n2,0] + dy = points[n1,1]-points[n2,1] + return sqrt(dx*dx + dy*dy) + +cpdef TotalDistance(city, R): + dist = np.sum(np.sqrt(np.sum(np.diff(R[city[:-1],:],axis=0)**2,axis=1))) + return dist + +# use this only after debugging! +@cython.boundscheck(False) # assume: no index larger than N-1 +@cython.wraparound(False) # assume: no neg. index +cdef reverse(np.ndarray[INT32_t, ndim=1] city, int n0, int n1): + cdef int nct, nn, j, k, l + nct = city.shape[0] + nn = cython.cdiv((1+ ((n1-n0) % nct)),2) # half the lenght of the segment to be reversed + # the segment is reversed in the following way n[0]<->n[1], n[0]+1<->n[1]-1, n[0]+2<->n[1]-2,... + # Start at the ends of the segment and swap pairs of cities, moving towards the center. + for j in range(nn): + k = (n0+j) % nct + l = (n1-j) % nct + city[k], city[l] = city[l], city[k] # swap + +# use this only after debugging! +@cython.boundscheck(False) # assume: no index larger than N-1 +@cython.wraparound(False) # assume: no neg. index +cdef np.ndarray[INT32_t, ndim=1] transpt(np.ndarray[INT32_t, ndim=1] city, int n0, int n1, int n2, int n3, int n4, int n5): + cdef int nct, j, i + cdef np.ndarray[INT32_t, ndim=1] newcity = np.empty_like(city) + nct = city.shape[0] + i = 0 + # Segment in the range n[0]...n[1] + for j in range( (n1-n0)%nct + 1): + newcity[i] = city[ (j+n0)%nct ] + i += 1 + # is followed by segment n[5]...n[2] + for j in range( (n2-n5)%nct + 1): + newcity[i] = city[ (j+n5)%nct ] + i += 1 + # is followed by segment n[3]...n[4] + for j in range( (n4-n3)%nct + 1): + newcity[i] = city[ (j+n3)%nct ] + i += 1 + return newcity + +# use this only after debugging! +@cython.boundscheck(False) # assume: no index larger than N-1 +@cython.wraparound(False) # assume: no neg. index +def tspcomp(np.ndarray[DTYPE_t, ndim=2] points, np.ndarray[INT32_t, ndim=1] city=None, int maxTsteps=100, double Tstart=0.2, double fCool=0.9, int usemat=1): + """ + maxTsteps = 50 # Temperature is lowered not more than maxTsteps + Tstart = 0.2 # Starting temperature - has to be high enough + fCool = 0.9 # Factor to multiply temperature at each cooling step + """ + cdef int ncity, maxSteps, maxAccepted, nct, t, i, accepted, n0, n1, n2, n3, n4, n5, nn, nc + cdef double Preverse, T, de, dist + cdef np.ndarray[INT32_t, ndim=1] ind + cdef np.ndarray[DTYPE_t, ndim=1] d + cdef np.ndarray[DTYPE_t, ndim=2] distmat + ncity = points.shape[0] + maxSteps = 100*ncity # Number of steps at constant temperature + maxAccepted = 10*ncity # Number of accepted steps at constant temperature + Preverse = 0.5 # How often to choose reverse/transpose trial move + + # Choosing city coordinates + if usemat: + distmat = np.zeros((ncity+1,ncity+1)) + for i in range(ncity): + ind = np.arange(i+1,ncity, dtype=np.int32) + d = norm(points[ind,:]-points[i,:][np.newaxis,:],axis=1) + distmat[i,i+1:-1] = d + distmat[i+1:-1,i] = d + + # The index table -- the order the cities are visited. + if city is None: + city = np.arange(ncity+1, dtype=np.int32) + else: + assert city.shape[0]==ncity + city = np.concatenate((city,np.int32([ncity]))) + # Distance of the travel at the beginning + dist = TotalDistance(city, points) + + # Stores points of a move + nct = ncity+1 # number of cities + + T = Tstart # temperature + + for t in range(maxTsteps): # Over temperature + + accepted = 0 + for i in range(maxSteps): # At each temperature, many Monte Carlo steps + + while True: # Will find two random cities sufficiently close by + # Two cities n[0] and n[1] are choosen at random + n0 = randint((nct)) # select one city + n1 = randint((nct-1)) # select another city, but not the same + if (n1 >= n0): n1 += 1 # + elif (n1 < n0): n0, n1 = n1, n0 # swap, because it must be: n[0]=3: break + + # We want to have one index before and one after the two cities + # The order hence is [n2,n0,n1,n3] + n2 = (n0-1) % nct # index before n0 -- see figure in the lecture notes + n3 = (n1+1) % nct # index after n2 -- see figure in the lecture notes + if Preverse > pyrand(): + # Here we reverse a segment + # What would be the cost to reverse the path between city[n[0]]-city[n[1]]? + if usemat: + de = distmat[city[n2],city[n1]] + distmat[city[n3], city[n0]] - \ + distmat[city[n2],city[n0]] - distmat[city[n3], city[n1]] + else: + de = getdist(city[n2],city[n1], points) + getdist(city[n3],city[n0], points) - \ + getdist(city[n2],city[n0], points) - getdist(city[n3],city[n1], points) + + if de<0 or exp(-cython.cdiv(de,T))>pyrand(): # Metropolis + accepted += 1 + dist += de + reverse(city, n0, n1) + else: + # Here we transpose a segment + nc = (n1+1+ randint((nn-1)))%nct # Another point outside n[0],n[1] segment. See picture in lecture nodes! + n4 = nc + n5 = (nc+1) % nct + + # Cost to transpose a segment + if usemat: + de = -distmat[city[n1],city[n3]] - \ + distmat[city[n0],city[n2]] - \ + distmat[city[n4],city[n5]] + de += distmat[city[n0],city[n4]] + \ + distmat[city[n1],city[n5]] + \ + distmat[city[n2],city[n3]] + else: + de = -getdist(city[n1],city[n3], points) - \ + getdist(city[n0],city[n2], points) - \ + getdist(city[n4],city[n5], points) + \ + getdist(city[n0],city[n4], points) + \ + getdist(city[n1],city[n5], points) + \ + getdist(city[n2],city[n3], points) + + if de<0 or exp(-cython.cdiv(de,T))>pyrand(): # Metropolis + accepted += 1 + dist += de + city = transpt(city, n0, n1, n2, n3, n4, n5) + + if accepted > maxAccepted: break + + print("T=%10.5f , distance= %10.5f , accepted steps= %d" %(T, dist, accepted)) + T *= fCool # The system is cooled down + if accepted == 0: break # If the path does not want to change any more, we can stop + + for i in range(nct): + if city[i]==ncity: + break + c = np.concatenate((city[i+1:], city[:i])) + return c, T \ No newline at end of file diff --git a/gepard.py b/gepard.py new file mode 100755 index 0000000..2fdaf38 --- /dev/null +++ b/gepard.py @@ -0,0 +1,275 @@ +# -*- 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, QtGui +from sampleview import SampleView +from scalebar import ScaleBar +from ramancontrol import defaultPath +import os + +class MeasureParticleWindow(QtWidgets.QMainWindow): + + def __init__(self): + super(MeasureParticleWindow, self).__init__() + + self.setWindowTitle("GEPARD") + self.resize(900, 700) + + self.view = SampleView() + self.view.imparent = self + self.view.ScalingChanged.connect(self.scalingChanged) + self.scalebar = ScaleBar(self) + self.view.ScalingChanged.connect(self.scalebar.updateScale) + + mdiarea = QtWidgets.QMdiArea(self) + mdiarea.addSubWindow(self.scalebar) + subview = mdiarea.addSubWindow(self.view) + subview.showMaximized() + subview.setWindowFlags(QtCore.Qt.FramelessWindowHint) + mdiarea.setOption(QtWidgets.QMdiArea.DontMaximizeSubWindowOnActivation) + + self.setCentralWidget(mdiarea) + + self.createActions() + self.createMenus() + self.createToolBar() + self.updateModes() + + def resizeEvent(self, event): + self.scalebar.move(0,self.height()-self.scalebar.height()) + + def closeEvent(self, event): + self.view.closeEvent(event) + + @QtCore.pyqtSlot(float) + def scalingChanged(self, scale): + self.zoomInAct.setEnabled(self.view.scaleFactor < 20.0) + self.zoomOutAct.setEnabled(self.view.scaleFactor > .01) + self.normalSizeAct.setEnabled(self.view.scaleFactor != 1.) + + @QtCore.pyqtSlot() + def open(self, fileName=False): + if fileName is False: + fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Open Project", + defaultPath, "*.pkl")[0] + if fileName: + self.fname = str(fileName) + self.view.open(self.fname) + self.scalingChanged(1.) + + @QtCore.pyqtSlot() + def new(self, fileName=False): + if fileName is False: + fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project", + defaultPath, "*.pkl")[0] + if fileName: + self.fname = str(fileName) + self.view.new(self.fname) + self.scalingChanged(1.) + + @QtCore.pyqtSlot() + def about(self): + QtWidgets.QMessageBox.about(self, 'GEPARD', + "Developed by Complex Fiber Structures GmbH on behalf of Leibniz-IPF Dresden") + + + def createActions(self): + fname = os.path.join(os.path.split(__file__)[0], + os.path.join("data","brand.png")) + self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname), + "About Particle Measurment", self) + self.aboutAct.triggered.connect(self.about) + + self.openAct = QtWidgets.QAction("&Open Project...", self) + self.openAct.setShortcut("Ctrl+O") + self.openAct.triggered.connect(self.open) + + self.newAct = QtWidgets.QAction("&New Measurement...", self) + self.newAct.setShortcut("Ctrl+N") + self.newAct.triggered.connect(self.new) + + self.exitAct = QtWidgets.QAction("E&xit", self) + self.exitAct.setShortcut("Ctrl+Q") + self.exitAct.triggered.connect(self.close) + + self.zoomInAct = QtWidgets.QAction("Zoom &In (25%)", self) + self.zoomInAct.setShortcut("Ctrl++") + self.zoomInAct.setEnabled(False) + self.zoomInAct.triggered.connect(self.view.zoomIn) + + self.zoomOutAct = QtWidgets.QAction("Zoom &Out (25%)", self) + self.zoomOutAct.setShortcut("Ctrl+-") + self.zoomOutAct.setEnabled(False) + self.zoomOutAct.triggered.connect(self.view.zoomOut) + + self.normalSizeAct = QtWidgets.QAction("&Normal Size", self) + self.normalSizeAct.setShortcut("Ctrl+S") + self.normalSizeAct.setEnabled(False) + self.normalSizeAct.triggered.connect(self.view.normalSize) + + self.fitToWindowAct = QtWidgets.QAction("&Fit to Window", self) + self.fitToWindowAct.setShortcut("Ctrl+E") + self.fitToWindowAct.setEnabled(True) + self.fitToWindowAct.triggered.connect(self.view.fitToWindow) + + self.connectRamanAct = QtWidgets.QAction("Connect to Microscope", self) + self.connectRamanAct.setEnabled(True) + self.connectRamanAct.triggered.connect(self.view.connectRaman) + + self.disconnectRamanAct = QtWidgets.QAction("Release Microscope", self) + self.disconnectRamanAct.setEnabled(False) + self.disconnectRamanAct.triggered.connect(self.view.disconnectRaman) + + self.opticalScanAct = QtWidgets.QAction("Optical Scan", self) + self.opticalScanAct.setEnabled(False) + self.opticalScanAct.setCheckable(True) + self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("OpticalScan"))) + + self.detectParticleAct = QtWidgets.QAction("Detect Particles", self) + self.detectParticleAct.setEnabled(False) + self.detectParticleAct.setCheckable(True) + self.detectParticleAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("ParticleDetection"))) + + self.ramanScanAct = QtWidgets.QAction("Raman Scan", self) + self.ramanScanAct.setEnabled(False) + self.ramanScanAct.setCheckable(True) + self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("RamanScan"))) + + + self.particelAnalysisAct = QtWidgets.QAction("Particle analysis", self) + self.particelAnalysisAct.setEnabled(False) + self.particelAnalysisAct.setCheckable(True) + self.particelAnalysisAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("ParticleAnalysis"))) + + + def updateModes(self, active=None, maxenabled=None): + ose, osc, pde, pdc, rse, rsc, pae, pac = [False]*8 + if maxenabled=="OpticalScan": + ose = True + elif maxenabled=="ParticleDetection": + ose, pde = True, True + elif maxenabled=="RamanScan": + ose, pde, rse = True, True, True + elif maxenabled=="ParticleAnalysis": + ose, pde, rse, pae = True, True, True, True + + if active=="OpticalScan" and ose: + osc = True + elif active=="ParticleDetection" and pde: + pdc = True + elif active=="RamanScan" and rse: + rsc = True + elif active=="ParticleAnalysis" and pae: + pac = True + + self.opticalScanAct.setEnabled(ose) + self.opticalScanAct.setChecked(osc) + self.detectParticleAct.setEnabled(pde) + self.detectParticleAct.setChecked(pdc) + self.ramanScanAct.setEnabled(rse) + self.ramanScanAct.setChecked(rsc) + self.particelAnalysisAct.setEnabled(pae) + self.particelAnalysisAct.setChecked(pac) + + def unblockUI(self, connected): + self.openAct.setEnabled(True) + self.newAct.setEnabled(True) + self.updateConnected(connected) + self.exitAct.setEnabled(True) + + def blockUI(self): + self.openAct.setEnabled(False) + self.newAct.setEnabled(False) + self.connectRamanAct.setEnabled(False) + self.disconnectRamanAct.setEnabled(False) + self.exitAct.setEnabled(False) + self.opticalScanAct.setEnabled(False) + self.detectParticleAct.setEnabled(False) + self.ramanScanAct.setEnabled(False) + self.particelAnalysisAct.setEnabled(False) + + def updateConnected(self, connected): + if connected: + self.connectRamanAct.setEnabled(False) + self.disconnectRamanAct.setEnabled(True) + else: + self.connectRamanAct.setEnabled(True) + self.disconnectRamanAct.setEnabled(False) + + def createMenus(self): + self.fileMenu = QtWidgets.QMenu("&File", self) + self.fileMenu.addAction(self.newAct) + self.fileMenu.addAction(self.openAct) + self.fileMenu.addSeparator() + self.fileMenu.addAction(self.exitAct) + + self.viewMenu = QtWidgets.QMenu("&View", self) + self.viewMenu.addAction(self.zoomInAct) + self.viewMenu.addAction(self.zoomOutAct) + self.viewMenu.addAction(self.normalSizeAct) + self.viewMenu.addSeparator() + self.viewMenu.addAction(self.fitToWindowAct) + + self.modeMenu = QtWidgets.QMenu("&Mode", self) + self.modeMenu.addAction(self.opticalScanAct) + self.modeMenu.addAction(self.detectParticleAct) + self.modeMenu.addAction(self.particelAnalysisAct) + + self.helpMenu = QtWidgets.QMenu("&Help", self) + self.helpMenu.addAction(self.aboutAct) + + self.menuBar().addMenu(self.fileMenu) + self.menuBar().addMenu(self.viewMenu) + self.menuBar().addMenu(self.modeMenu) + self.menuBar().addMenu(self.helpMenu) + + def createToolBar(self): + self.toolbar = QtWidgets.QToolBar("Tools") + self.toolbar.setIconSize(QtCore.QSize(100,50)) + self.toolbar.addAction(self.aboutAct) + self.toolbar.addAction(self.newAct) + self.toolbar.addAction(self.openAct) + self.toolbar.addSeparator() + self.toolbar.addAction(self.connectRamanAct) + self.toolbar.addAction(self.disconnectRamanAct) + self.toolbar.addSeparator() + self.toolbar.addAction(self.opticalScanAct) + self.toolbar.addAction(self.detectParticleAct) + self.toolbar.addAction(self.ramanScanAct) + self.toolbar.addAction(self.particelAnalysisAct) + self.toolbar.addSeparator() + self.toolbar.addAction(self.exitAct) + self.toolbar.setOrientation(QtCore.Qt.Vertical) + self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar) + +if __name__ == '__main__': + + import sys + from time import localtime, strftime + logname = os.path.join(os.path.split(__file__)[0], os.path.join("logfile.txt")) + fp = open(logname, "a") + sys.stderr = fp + sys.stdout = fp + + print("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime())) + app = QtWidgets.QApplication(sys.argv) + meas = MeasureParticleWindow() + meas.showMaximized() + ret = app.exec_() \ No newline at end of file diff --git a/helperfunctions.py b/helperfunctions.py new file mode 100644 index 0000000..38bffa4 --- /dev/null +++ b/helperfunctions.py @@ -0,0 +1,84 @@ +# -*- 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 numpy as np +import cv2 +import os + +def cv2imread_fix(fname, flags=cv2.IMREAD_COLOR): + with open(fname, "rb") as fp: + cont = fp.read() + img = cv2.imdecode(np.fromstring(cont, dtype=np.uint8), flags) + return img + return None + +def cv2imwrite_fix(fname, img, params=None): + pathname, ext = os.path.splitext(fname) + if params is None: + ret, data = cv2.imencode(ext, img) + else: + ret, data = cv2.imencode(ext, img, params) + with open(fname, "wb") as fp: + fp.write(data.tobytes()) + return ret + +def polygoncovering(boundary, wx, wy): + poslist = [] + ymin, ymax = boundary[:,1].min(), boundary[:,1].max() + Ny = max(int(np.ceil((ymax-ymin)/wy)),1) + dyi = wy*Ny - (ymax-ymin) + y = ymin - .5*dyi + wy*np.arange(Ny) + dx = np.roll(boundary[:,0],-1)-boundary[:,0] + dy = np.roll(boundary[:,1],-1)-boundary[:,1] + x0c, x1c = boundary[:,0].min(), boundary[:,0].max() + x0clast, x1clast = x0c, x1c + for i, yi in enumerate(y): + if i==0: + if Ny>1: + ind = boundary[:,1]yi + else: + ind = (boundary[:,1]>yi)&(boundary[:,1] results are ok + indc = (ti>=0.)&(ti<1) + xi = boundary[indc,0] + ti[indc]*dx[indc] + x0c, x1c = xi.min(), xi.max() + if i==0: + x0clast, x1clast = x0c, x1c + if np.any(ind): + x0n, x1n = boundary[ind,0].min(), boundary[ind,0].max() + else: + x0n, x1n = x0c, x1c + x0 = min([x0n,x0c,x0clast]) + x1 = max([x1n,x1c,x1clast]) + Nx = int(np.ceil((x1-x0)/wx)) + dxi = wx*Nx - (x1-x0) + x = x0 - .5*dxi + .5*wx + wx*np.arange(Nx) + poslist.extend([[xi,yi+.5*wy] for xi in (x if i%2==0 else x[::-1])]) + x0clast, x1clast = x0c, x1c + return poslist + \ No newline at end of file diff --git a/imagestitch.py b/imagestitch.py new file mode 100644 index 0000000..89d8da5 --- /dev/null +++ b/imagestitch.py @@ -0,0 +1,79 @@ +# -*- 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 cv2 +import numpy as np + +def imageStacking(colimgs): + full = [] + images = [] + laplacians = [] + for img in colimgs: + gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) + + lap = cv2.Laplacian(gray, cv2.CV_64F) + full.append(img) + images.append(gray) + blurlap = (cv2.GaussianBlur((lap)**2,(25,25),0)) + laplacians.append(blurlap) + images = np.array(images) + laplacians = np.array(laplacians) + full = np.array(full) + full = np.uint8(full) + + lap = laplacians+.1 + zval = lap.argmax(axis=0)*(255./(lap.shape[0] - (1 if lap.shape[0]>1 else 0))) + im = np.sum(full * lap[:,:,:,np.newaxis], axis=0)/(lap[:,:,:,np.newaxis].sum(axis=0)) + + zval = np.uint8(zval) + im = np.uint8(im) + return im, zval + +def combineImages(path, nx, ny, nk, width, height, angle): + imgs = [] + full = None + for i in range(nx): + for j in range(ny): + colimgs = [] + for k in range(nk): + colimgs.append(cv2.imread(path + f'test_{i}_{j}_{k}.bmp')) + img = imageStacking(colimgs) + imgs.append(img) + dx = i*.9*img.shape[1] + dy = j*.8*img.shape[0] + c, s = np.cos(np.radians(angle)), np.sin(np.radians(angle)) + M = np.float32([[c,s,dx],[-s,c,dy]]) + dst = cv2.warpAffine(img, M, (int(img.shape[1]*((nx-1)*.9 +1)), int(img.shape[0]*((ny-1)*.8 +1)))) + + if full is None: + full = dst + else: + full = cv2.max(full,dst) + cv2.imwrite("full_dunkel.png", full) + + +if __name__ == "__main__": + path = "../Bildserie-Scan/dunkelfeld/" + + Nx, Ny, Nk = 10, 10, 4 + width, height, angle = 463.78607177734375, 296.0336608886719, -0.04330849274992943 + + combineImages(path, Nx, Ny, Nk, width, height, angle) \ No newline at end of file diff --git a/opticalscan.py b/opticalscan.py new file mode 100755 index 0000000..29ada09 --- /dev/null +++ b/opticalscan.py @@ -0,0 +1,535 @@ +# -*- 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, Pipe +from imagestitch import imageStacking +import os +import cv2 +from helperfunctions import cv2imread_fix, cv2imwrite_fix +from time import sleep, time +import datetime +import sys + +def scan(path, sol, zpositions, grid, controlclass, connection, ishdr=False): + if ishdr: + merge_mertens = cv2.createMergeMertens() + with open("scanlog.txt", "a") as fp: + sys.stderr = fp + sys.stdout = fp + + 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()) + zik = z+zk + assert not np.isnan(zik) + print("moving to:", x, y, zik) + 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 connection.poll(): + instruction = connection.recv() + if instruction=="stop": + ramanctrl.disconnect() + return + connection.send(i) + ramanctrl.disconnect() + while not connection.poll(): + sleep(.1) + connection.recv() + +def loadAndPasteImage(srcnames, fullimage, fullzval, width, height, + rotationvalue, p0, p1, p): + colimgs = [] + for name in srcnames: + colimgs.append(cv2.cvtColor(cv2imread_fix(name), cv2.COLOR_BGR2RGB)) + img, zval = imageStacking(colimgs) + 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): + super().__init__(parent) + self.dswidgets = [] + self.N = 0 + self.ramanctrl = ramanctrl + 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): + 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(name='imageUpdate') + boundaryUpdate = QtCore.pyqtSignal() + + def __init__(self, ramanctrl, dataset, parent=None): + super().__init__(parent, QtCore.Qt.Window) + self.view = parent + vbox = QtWidgets.QVBoxLayout() + group = QtWidgets.QGroupBox("Point coordinates [µm]", self) + self.ramanctrl = ramanctrl + self.dataset = dataset + self.positions = [] + self.process = None + self.points = PointCoordinates(5, self.ramanctrl, self) + group.setLayout(self.points) + self.points.readPoint.connect(self.takePoint) + + hbox = QtWidgets.QHBoxLayout() + 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) + 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) + label3 = QtWidgets.QLabel("Focus steps:", self) + self.nzedit = QtWidgets.QSpinBox(self) + self.nzedit.setRange(2,10) + self.nzedit.setValue(3) + 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.pareaselect.setEnabled(False) + 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) + + vboxradio = QtWidgets.QVBoxLayout() + self.circlerad = QtWidgets.QRadioButton("Circle") + self.rectanglerad = QtWidgets.QRadioButton("Rectangle") + self.circlerad.setChecked(True) + vboxradio.addWidget(self.circlerad) + vboxradio.addWidget(self.rectanglerad) + + grid = QtWidgets.QGridLayout() + + grid.addLayout(vboxradio, 0, 0, QtCore.Qt.AlignLeft) + grid.addWidget(label, 0, 1, QtCore.Qt.AlignLeft) + grid.addWidget(self.radiusincreaseedit, 0, 2, QtCore.Qt.AlignRight) + grid.addWidget(self.pareaselect, 0, 3, QtCore.Qt.AlignRight) + + grid.addWidget(self.hdrcheck, 1, 0, QtCore.Qt.AlignLeft) + grid.addWidget(label2, 1, 1, QtCore.Qt.AlignLeft) + grid.addWidget(self.zmaxedit, 1, 2, QtCore.Qt.AlignRight) + grid.addWidget(label3, 1, 3, QtCore.Qt.AlignLeft) + grid.addWidget(self.nzedit, 1, 4, QtCore.Qt.AlignRight) + + grid.addWidget(self.prun, 2, 3, QtCore.Qt.AlignLeft) + grid.addWidget(self.pexit, 2, 4, QtCore.Qt.AlignRight) + + hbox.addStretch() + hbox.addLayout(grid) + + vbox.addWidget(group) + vbox.addLayout(hbox) + 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.connection.send("stop") + self.process.join() + self.view.unblockUI() + else: + return + self.close() + + + + @QtCore.pyqtSlot() + def areaSelect(self): + 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, 20, 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*20)),2), max(int(np.round((y1-y0)/a*20)),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) + 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() + pshift = self.ramanctrl.getRamanPositionShift() + self.dataset.pshift = pshift + img = cv2.cvtColor(cv2imread_fix(self.dataset.getTmpImageName()), cv2.COLOR_BGR2RGB) + self.dataset.pixelscale = width/img.shape[1] + self.dataset.imagedim = width, height, rotationvalue + + 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.pareaselect.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]]) + full = cv2.warpAffine(full, M, (Nx, Ny)) + dst = cv2.max(full, dst) + + self.view.imgdata = dst + self.dataset.lastpos = p0 + self.dataset.maxdim = p0 + p1 + self.dataset.readin = False + self.imageUpdate.emit() + + @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 + 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 + 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)[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 zmax3: + 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() + if i==Ngrid-1: + self.connection.send("stop") + cv2imwrite_fix(self.dataset.getImageName(), cv2.cvtColor(self.view.imgdata, cv2.COLOR_RGB2BGR)) + self.dataset.saveZvalImg() + self.process.join() + self.connection.close() + 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__": + import sys + from WITecCOM import WITecCOM + + app = QtWidgets.QApplication(sys.argv) + optscan = OpticalScan(WITecCOM()) + optscan.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ramanbase.py b/ramanbase.py new file mode 100644 index 0000000..f0d474f --- /dev/null +++ b/ramanbase.py @@ -0,0 +1,68 @@ +# -*- 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 . +""" + +class RamanBase(object): + def __init__(self): + self.connected = False + self.timeseries = False + + def getRamanPositionShift(self): + """ Compute the shift between laser spot and image center""" + raise NotImplementedError + + def connect(self): + self.connected = True + return True + + def disconnect(self): + self.connected = False + + def getPosition(self): + raise NotImplementedError + + def getSoftwareZ(self): + raise NotImplementedError + + def getUserZ(self): + raise NotImplementedError + + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + raise NotImplementedError + + def moveZto(self, z, epsz=0.011): + raise NotImplementedError + + def saveImage(self, fname): + raise NotImplementedError + + def getImageDimensions(self): + """ Get the image width and height in um and the orientation angle in degrees. + """ + raise NotImplementedError + + def startSinglePointScan(self): + raise NotImplementedError + + def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + raise NotImplementedError + + def nextTimeSeriesScan(self, num): + raise NotImplementedError \ No newline at end of file diff --git a/ramancontrol.py b/ramancontrol.py new file mode 100644 index 0000000..9b8306b --- /dev/null +++ b/ramancontrol.py @@ -0,0 +1,45 @@ +# -*- 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 socket import gethostname +from WITecCOM import WITecCOM +#from renishawcom import RenishawCOM +from simulatedraman import SimulatedRaman + +__all__ = ["RamanControl", "simulatedRaman", "defaultPath"] + +defaultPath = os.path.split(__file__)[0] + +RamanControl = SimulatedRaman +hostname = gethostname() +if "SIMULATED_RAMAN_CONTROL" not in os.environ: + if "NO_WITEC_CONTROL" not in os.environ: + WITecCOM.hostname = hostname + #defaultPath = r"D:\Projekte\Mikroplastik" + RamanControl = WITecCOM +# elif hostname == RenishawCOM.hostname and "NO_RENISHAW_CONTROL" not in os.environ: +# RamanControl = RenishawCOM + +if RamanControl == SimulatedRaman: + print("WARNING: using only simulated raman control!") + simulatedRaman = True +else: + simulatedRaman = False \ No newline at end of file diff --git a/ramanscanui.py b/ramanscanui.py new file mode 100644 index 0000000..db05b10 --- /dev/null +++ b/ramanscanui.py @@ -0,0 +1,244 @@ +# -*- 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, Pipe +from time import sleep, time +from externalmodules import tsp +import datetime +import sys + +def reorder(points, N=20): + y0, y1 = points[:,1].min(), points[:,1].max() + y = np.linspace(y0,y1+.1,N+1) + allind = np.arange(points.shape[0]) + newind = [] + for i, yi in enumerate(y[:-1]): + yi1 = y[i+1] + indy = allind[(points[:,1]>=yi)&(points[:,1]0: + self.prun.setEnabled(True) + self.setWindowTitle(str(len(ds.ramanpoints)) + " Particles") + + @QtCore.pyqtSlot() + def stopScan(self): + if self.process is not None and self.process.is_alive(): + reply = QtWidgets.QMessageBox.question(self, 'Stop raman 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.connection.send("stop") + self.process.join() + self.view.unblockUI() + else: + return + self.close() + + @QtCore.pyqtSlot() + def run(self): + 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 + accu = self.accumulationsedit.value() + inttime = self.inttimeedit.value() + points = np.asarray(self.dataset.ramanpoints) + lmin = None + for i in range(20,41): + c = reorder(points, i) + l = np.sum(np.sqrt(np.sum(np.diff(points[c,:],axis=0)**2,axis=1))) + if lmin is None or l3: + timerunning = time()-self.starttime + ttot = timerunning*Npoints/(i+1) + time2go = ttot - timerunning + self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) + if i==Npoints-1: + self.connection.send("stop") + self.process.join() + self.connection.close() + self.dataset.ramanscandone = True + self.view.saveDataSet() + self.view.unblockUI() + self.view.switchMode("ParticleAnalysis") + self.progressbar.setValue(0) + self.progressbar.setEnabled(False) + self.progresstime.setEnabled(False) + self.close() + return + self.timer.start(100.) + \ No newline at end of file diff --git a/sampleview.py b/sampleview.py new file mode 100644 index 0000000..9e56b3e --- /dev/null +++ b/sampleview.py @@ -0,0 +1,394 @@ +# -*- 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, QtGui, QtWidgets +import numpy as np +import os +from dataset import DataSet, loadData +from ramancontrol import RamanControl, simulatedRaman +from opticalscan import OpticalScan +from ramanscanui import RamanScanUI +from detectionview import ParticleDetectionView +from viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegementationContours +from helperfunctions import polygoncovering, cv2imread_fix +import cv2 + +class SampleView(QtWidgets.QGraphicsView): + ScalingChanged = QtCore.pyqtSignal(float) + + def __init__(self): + super(SampleView, self).__init__() + + self.item = QtWidgets.QGraphicsPixmapItem() + self.item.setPos(0, 0) + self.item.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.scaleFactor = 1.0 + scene = QtWidgets.QGraphicsScene(self) + scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) + scene.addItem(self.item) + scene.setBackgroundBrush(QtCore.Qt.darkGray) + self.setScene(scene) + self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) + self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) + self.setRenderHint(QtGui.QPainter.Antialiasing) + self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) + self.ramanctrl = RamanControl() + self.drag = None + self.mode = None + self.dataset = None + self.fititems = [] + self.boundaryitems = [[],[]] + self.scanitems = [] + self.ramanscanitems = [] + self.imgdata = None + self.isblocked = False + self.contouritem = SegementationContours() + scene.addItem(self.contouritem) + self.detectionwidget = None + self.ramanwidget = RamanScanUI(self.ramanctrl, None, self) + self.ramanwidget.imageUpdate.connect(self.loadPixmap) + self.oscanwidget = OpticalScan(self.ramanctrl, None, self) + self.oscanwidget.imageUpdate.connect(self.loadPixmap) + self.oscanwidget.boundaryUpdate.connect(self.resetBoundary) + self.setMinimumSize(600,600) + + def closeEvent(self, event): + reply = QtWidgets.QMessageBox.question(self, 'Message', + "Are you sure to quit?", QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + + if reply == QtWidgets.QMessageBox.Yes: + self.disconnectRaman() + self.saveDataSet() + event.accept() + self.oscanwidget.close() + if self.detectionwidget is not None: + self.detectionwidget.close() + self.ramanwidget.close() + else: + event.ignore() + + def saveDataSet(self): + if self.dataset is not None: + self.dataset.save() + + @QtCore.pyqtSlot() + def zoomIn(self): + self.scaleImage(1.25) + + @QtCore.pyqtSlot() + def zoomOut(self): + self.scaleImage(0.8) + + @QtCore.pyqtSlot() + def normalSize(self): + self.scaleFactor = 1.0 + self.setTransform(QtGui.QTransform.fromScale(1., 1.)) + self.announceScaling() + + @QtCore.pyqtSlot() + def fitToWindow(self): + print("fitting") + brect = self.item.sceneBoundingRect() + self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) + self.scaleFactor = self.transform().m11() + self.announceScaling() + + def switchMode(self, mode, loadnew=False): + if mode is None: + return + assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] + # TODO add code for mode switching here + self.oscanwidget.setVisible(False) + if self.detectionwidget is not None: + self.detectionwidget.close() + self.detectionwidget.destroy() + self.ramanwidget.setVisible(False) + self.contouritem.resetContours([]) + self.mode = mode + self.loadPixmap() + if mode == "OpticalScan": + self.oscanwidget.setVisible(True) + self.oscanwidget.resetDataset(self.dataset) + elif mode == "ParticleDetection": + self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) + self.detectionwidget.show() + self.detectionwidget.imageUpdate.connect(self.detectionUpdate) + self.detectionwidget.detectionFinished.connect(self.activateMaxMode) + elif mode == "RamanScan": + self.ramanwidget.resetDataset(self.dataset) + self.ramanwidget.setVisible(True) + if loadnew: + self.fitToWindow() + self.imparent.updateModes(mode, self.getMaxMode()) + + def open(self, fname): + self.saveDataSet() + if self.dataset is not None: + self.dataset.save() + self.dataset = loadData(fname) + self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) + self.imgdata = None + self.activateMaxMode(loadnew=True) + + def new(self, fname): + self.saveDataSet() + if self.dataset is not None: + self.dataset.save() + self.dataset = DataSet(fname, newProject=True) + self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) + self.imgdata = None + self.activateMaxMode(loadnew=True) + + @QtCore.pyqtSlot() + def activateMaxMode(self, loadnew=False): + mode = self.getMaxMode() + self.imparent.updateModes(self.mode, self.getMaxMode()) + self.switchMode(mode, loadnew=loadnew) + + def blockUI(self): + self.isblocked = True + self.imparent.blockUI() + + def unblockUI(self): + self.isblocked = False + self.imparent.unblockUI(self.ramanctrl.connected) + self.imparent.updateModes(self.mode, self.getMaxMode()) + + def getMaxMode(self): + if self.dataset is None: + return None + if not self.ramanctrl.connected: + self.connectRaman() + if not self.ramanctrl.connected: + return None + maxmode = "OpticalScan" + if os.path.exists(self.dataset.getImageName()): + maxmode = "ParticleDetection" + if len(self.dataset.ramanpoints)>0: + maxmode = "RamanScan" + if self.dataset.ramanscandone: + maxmode = "ParticleAnalysis" + return maxmode + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.RightButton: + self.drag = event.pos() + elif event.button()==QtCore.Qt.LeftButton and self.mode in ["OpticalScan", "RamanScan"] \ + and event.modifiers()==QtCore.Qt.ControlModifier: + p0 = self.mapToScene(event.pos()) + if self.dataset is not None and self.dataset.pshift is not None: + 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 + x, y, z = self.dataset.mapToLengthRaman([p0.x(), p0.y()], noz=(False if self.mode=="RamanScan" else True)) + if z is not None: + assert z>-100. + self.ramanctrl.moveToAbsolutePosition(x, y, z) + elif event.button()==QtCore.Qt.LeftButton and self.mode=="ParticleDetection": + p0 = self.mapToScene(event.pos()) + self.detectionwidget.setImageCenter([p0.x(), p0.y()]) + else: + p0 = self.mapToScene(event.pos()) + super(SampleView, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self.drag is not None: + p0 = event.pos() + move = self.drag-p0 + self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) + self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) + self.drag = p0 + else: + super(SampleView, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self.drag = None + super(SampleView, self).mouseReleaseEvent(event) + + def wheelEvent(self, event): + factor = 1.01**(event.angleDelta().y()/8) + self.scaleImage(factor) + + def scaleImage(self, factor): + if factor<1 and not self.imparent.zoomOutAct.isEnabled(): + return + if factor>1 and not self.imparent.zoomInAct.isEnabled(): + return + self.scaleFactor *= factor + self.scale(factor, factor) + self.announceScaling() + + def announceScaling(self): + if self.dataset is None or self.dataset.pixelscale is None: + self.ScalingChanged.emit(-1.0) + else: + self.ScalingChanged.emit(self.dataset.pixelscale/self.scaleFactor) + + def connectRaman(self): + if not self.ramanctrl.connect(): + msg = QtWidgets.QMessageBox() + msg.setText("Connection failed! Please enable remote control.") + msg.exec() + else: + mode = self.getMaxMode() + self.switchMode(mode) + self.imparent.updateConnected(self.ramanctrl.connected) + + def disconnectRaman(self): + self.ramanctrl.disconnect() + self.imparent.updateConnected(self.ramanctrl.connected) + + @QtCore.pyqtSlot() + def detectionUpdate(self): + self.contouritem.resetContours(self.dataset.particlecontours) + self.prepareAnalysis() + self.update() + + @QtCore.pyqtSlot() + def loadPixmap(self): + self.clearItems() + if self.dataset is None: + self.item.setPixmap(QtGui.QPixmap()) + else: + data = self.imgdata + fname = self.dataset.getImageName() + if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": + self.contouritem.resetContours(self.dataset.particlecontours) + if data is None and os.path.exists(fname): + data = cv2.cvtColor(cv2imread_fix(fname), cv2.COLOR_BGR2RGB) + self.imgdata = data + if data is not None: + height, width, channel = data.shape + bytesPerLine = 3 * width + pix = QtGui.QPixmap() + pix.convertFromImage(QtGui.QImage(data.data, + width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) + self.item.setPixmap(pix) + else: + self.item.setPixmap(QtGui.QPixmap()) + if self.mode == "OpticalScan": + for i, p in zip(self.dataset.fitindices, self.dataset.fitpoints): + p = self.dataset.mapToPixel(p, force=True) + fititem = FitPosIndicator(i+1, pos=p) + self.scene().addItem(fititem) + self.fititems.append(fititem) + if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": + self.prepareAnalysis() + else: + self.fitToWindow() + + @QtCore.pyqtSlot() + def resetScanPositions(self): + for item in self.scanitems: + self.scene().removeItem(item) + edges, nodes = self.boundaryitems + boundary = [] + for n in nodes: + p = n.pos().x(), n.pos().y() + boundary.append(self.dataset.mapToLength(p, force=True)) + boundary = np.array(boundary) + print(boundary) + self.dataset.boundary = boundary + width, height, angle = self.dataset.imagedim + margin = min(width, height)*0.02 + wx, wy = width-margin, height-margin + print(wx,wy) + p1 = polygoncovering(boundary, wx, wy) + b2 = boundary.copy() + b2 = b2[:,[1,0]] + p2 = polygoncovering(b2, wy, wx) + if len(p2)0: + data = [] + for i in self.dataset.ramanscansortindex: + data.append(list(self.dataset.ramanpoints[i])+list(self.dataset.particlestats[i])) + for i in range(len(data)): + item = RamanScanIndicator(self, i+1, 20, (data[i][0],data[i][1])) + self.scene().addItem(item) + self.ramanscanitems.append(item) + + def highLightRamanIndex(self, index): + if index < len(self.ramanscanitems): + for item in self.ramanscanitems: + item.setHighLight(False) + self.ramanscanitems[index].setHighLight(True) + self.ensureVisible(self.ramanscanitems[index]) + #self.centerOn(self.ramanscanitems[index]) + + def clearItems(self): + for item in self.fititems: + self.scene().removeItem(item) + self.fititems = [] + for item in self.scanitems: + self.scene().removeItem(item) + edges, nodes = self.boundaryitems + for item in edges: + self.scene().removeItem(item) + for item in nodes: + self.scene().removeItem(item) + self.scanitems = [] + self.boundaryitems = [], [] + for item in self.ramanscanitems: + self.scene().removeItem(item) + self.ramanscanitems = [] \ No newline at end of file diff --git a/scalebar.py b/scalebar.py new file mode 100755 index 0000000..ff4dda5 --- /dev/null +++ b/scalebar.py @@ -0,0 +1,102 @@ +# -*- 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, QtGui +import numpy as np + +WX, WY = 400, 80 + +class ScaleBar(QtWidgets.QMdiSubWindow): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.FramelessWindowHint) + + self.setFixedSize(WX, WY) + self.divisor = None + self.drag = None + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.LeftButton: + self.drag = event.pos() + + + def mouseMoveEvent(self, event): + if self.drag is not None: + p0 = event.pos() + self.move(self.mapToParent(p0-self.drag)) + self.parentWidget().update() + else: + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self.drag = None + super().mouseReleaseEvent(event) + + @QtCore.pyqtSlot(float) + def updateScale(self, pixelscale): + if pixelscale<0: + self.divisor = None + else: + tmp = np.log10(pixelscale*WX*.95*.83); + exponent = np.floor(tmp) + divisor = 10**(tmp-exponent) + if divisor>3: + divisor = np.round(divisor) + else: + divisor = np.round(divisor*2)/2 + + divisor *= 10**exponent + + self.divisor = divisor + self.wscale = int(np.round(divisor/pixelscale)) + self.update() + + def paintEvent(self, e): + qp = QtGui.QPainter() + qp.begin(self) + qp.setPen(QtGui.QColor(0,0,0)) + backgroundColor = self.palette().light().color() + #backgroundColor.setAlpha(200) + qp.setBrush(backgroundColor) + qp.drawRect(0,0,WX,WY) + qp.setPen(QtGui.QColor(0,0,0)) + qp.setBrush(QtGui.QColor(0,0,100)) + y0, dy = 40, 35 + if self.divisor is None: + qp.drawRect(10,y0,WX-20,dy) + else: + font = qp.font() + font.setPointSize(30) + qp.setFont(font) + qp.drawText((WX-self.wscale)//2,5,self.wscale,dy, + QtCore.Qt.AlignCenter, str(int(self.divisor))+" µm") + qp.drawRect((WX-self.wscale)//2,y0,self.wscale,dy) + qp.end() + + +if __name__ == '__main__': + + import sys + + app = QtWidgets.QApplication(sys.argv) + meas = ScaleBar(None) + meas.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/segmentation.py b/segmentation.py new file mode 100755 index 0000000..9fdd756 --- /dev/null +++ b/segmentation.py @@ -0,0 +1,413 @@ +# -*- 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 numpy as np +import cv2 +cv2.useOptimized() +from time import time +from scipy.interpolate import InterpolatedUnivariateSpline +from scipy import ndimage as ndi +from skimage.feature import peak_local_max +from skimage.morphology import watershed +from random import random + +class Parameter(object): + def __init__(self, name, dtype, value=None, minval=None, maxval=None, + decimals=0, stepsize=1, helptext=None, show=False): + self.name = name + self.dtype = dtype + self.value = value + self.valrange = (minval, maxval) + self.decimals = decimals + self.stepsize = stepsize + self.helptext = helptext + self.show = show + +class Segmentation(object): + def __init__(self): + self.cancelcomputation = False + self.initialParameters() + + def initialParameters(self): + parlist = [Parameter("points", np.ndarray, np.array([[20,0],[50,100],[200,255]]), helptext="Curve contrast"), + Parameter("contrastcurve", np.bool, True, helptext="Contrast curve", show=True), + Parameter("blurRadius", int, 9, 3, 99, 1, 2, helptext="Blur radius", show=True), + Parameter("threshold", float, .2, .01, .9, 2, .02, helptext="Basic threshold", show=True), + Parameter("maxholebrightness", float, 0.5, 0, 1, 2, 0.02, helptext="Close holes brighter than..", show = True), + Parameter("erodeconvexdefects", int, 0, 0, 20, helptext="Erode convex defects", show=True), + Parameter("minparticlearea", int, 20, 10, 1000, 0, 50, helptext="Min. particle pixel area", show=False), + Parameter("minparticledistance", int, 20, 10, 1000, 0, 5, helptext="Min. distance between particles", show=False), + Parameter("measurefrac", float, 1, 0, 1, 2, stepsize = 0.05, helptext="measure fraction of particles", show=False), + Parameter("sure_fg", None, helptext="Show sure foreground", show=True), + Parameter("compactness", float, 0.1, 0, 1, 2, 0.05, helptext="watershed compactness", show=False), + Parameter("watershed", None, helptext="Show watershed markers", show=True), + ] + # make each parameter accessible via self.name + # the variables are defined as properties and because of how the local context + # in for loops works the actural setter and getter functions are defined inside + # a separate contex in a local function + def makeGetter(p): + return lambda : p.value + def makeSetter(p): + def setter(value): + p.value = value + return setter + for p in parlist: + # variabels in self are writen directly to the name dictionary + self.__dict__[p.name] = property(makeGetter(p), makeSetter(p)) + self.parlist = parlist + + def setParameters(self, **kwargs): + for key in kwargs: + self.__dict__[key] = kwargs[key] + + def convert2Gray(self, img): + gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) + return gray + + def calculateHist(self, gray): + hist = cv2.calcHist([gray],[0],None,[256],[0,256]) + return hist + + def calculateHistFunction(self, points): + t = np.linspace(0,1,800) + x0 = np.concatenate(([-1.],points[:,0],[256.])) + y0 = np.concatenate(([0.],points[:,1],[255.])) + t0 = np.concatenate(([0.],np.cumsum(np.sqrt(np.diff(x0)**2+np.diff(y0)**2)))) + t0 /= t0[-1] + fx = InterpolatedUnivariateSpline(t0, x0, k=3) + fy = InterpolatedUnivariateSpline(t0, y0, k=3) + x = fx(t) + y = fy(t) + arr = np.zeros(256, dtype=np.uint8) + xi = np.arange(256) + ind = np.searchsorted(xi, x) + arr[ind[ind<256]] = y[ind<256] + arr[xi>points[:,0].max()] = 255 + arr[xi255] = 255. + arr[arr<0] = 0. + + return xi, arr + + + def closeHoles(self, thresh): + n, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) + newthresh = np.zeros_like(thresh) + + for label in range(1, n): + up = stats[label, cv2.CC_STAT_TOP] + left = stats[label, cv2.CC_STAT_LEFT] + width = stats[label, cv2.CC_STAT_WIDTH] + height = stats[label, cv2.CC_STAT_HEIGHT] + subimg = np.uint8(255 * (labels[up:(up+height), left:(left+width)] == label)) + + # Add padding to TrehsholdImage + subimg = cv2.copyMakeBorder(subimg, 1, 1, 1, 1, 0) + # Copy the thresholded image. + im_floodfill = subimg.copy() + # Mask used to flood filling. + # Notice the size needs to be 2 pixels than the image. + h, w = subimg.shape[:2] + mask = np.zeros((h+2, w+2), np.uint8) + # Floodfill from point (0, 0) + cv2.floodFill(im_floodfill, mask, (0,0), 255); + # Invert floodfilled image + im_floodfill_inv = cv2.bitwise_not(im_floodfill) + # Combine the two images to get the foreground. + im_out = subimg | im_floodfill_inv + + newthresh[up:(up+height), left:(left+width)] += im_out[1:-1, 1:-1] + + return newthresh + + def closeBrightHoles(self, thresh, grayimage, maxbrightness): + n, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) + maxbrightness = np.uint8(maxbrightness * 255) + + for label in range(1, n): + up = stats[label, cv2.CC_STAT_TOP] + left = stats[label, cv2.CC_STAT_LEFT] + width = stats[label, cv2.CC_STAT_WIDTH] + height = stats[label, cv2.CC_STAT_HEIGHT] + subimg = np.uint8(255 * (labels[up:(up+height), left:(left+width)] == label)) + + # Add padding to TrehsholdImage + subimg = cv2.copyMakeBorder(subimg, 1, 1, 1, 1, 0) + # Copy the thresholded image. + im_floodfill = subimg.copy() + # Mask used to flood filling. + # Notice the size needs to be 2 pixels than the image. + h, w = subimg.shape[:2] + mask = np.zeros((h+2, w+2), np.uint8) + # Floodfill from point (0, 0) + cv2.floodFill(im_floodfill, mask, (0,0), 255); + + indices = np.where(im_floodfill == 0)[0] + if len(indices) > 0: + if np.mean(grayimage[indices[0]]) > maxbrightness: + + #close hole and add closed image to thresh: + im_floodfill_inv = cv2.bitwise_not(im_floodfill) + # Combine the two images to get the foreground. + im_out = subimg | im_floodfill_inv + + thresh[up:(up+height), left:(left+width)] += im_out[1:-1, 1:-1] + + return thresh + + def getEdgeBorders(self, image): + edges = abs(cv2.Laplacian(image, cv2.CV_64F)) + edges = cv2.blur(edges, (5, 5)) + edges = edges**0.6 + edges = edges/edges.max() + + return edges + + def erodeConvexDefects(self, thresh, numiter): + thresh = cv2.copyMakeBorder(thresh, 1, 1, 1, 1, 0) + for iterations in range(numiter): + thresh2, contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + for cnt in contours: + hull = cv2.convexHull(cnt, returnPoints = False) + defects = cv2.convexityDefects(cnt, hull) + if defects is not None: + sqarea = np.sqrt(cv2.contourArea(cnt)) + blobsize = int(sqarea/15 * 1/(iterations+1)) + for i in range(defects.shape[0]): + s, e, f, d = defects[i,0] + if d > sqarea*5: + cv2.circle(thresh,tuple(cnt[f][0]),blobsize,0,-1) + + return thresh[1:-1, 1:-1] + + def getSureForeground(self, thresh, mindistance, minarea): + sure_fg = np.zeros_like(thresh) + n, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) + + for label in range(1, n): + up = stats[label, cv2.CC_STAT_TOP] + left = stats[label, cv2.CC_STAT_LEFT] + width = stats[label, cv2.CC_STAT_WIDTH] + height = stats[label, cv2.CC_STAT_HEIGHT] + area = stats[label, cv2.CC_STAT_AREA] + subimg = np.uint8(255 * (labels[up:(up+height), left:(left+width)] == label)) + subdist = cv2.distanceTransform(subimg, cv2.DIST_L2,3) + subfg = np.uint8(peak_local_max(subdist, mindistance, indices = False)) + + if subfg.max() > 0 and random() < self.measurefrac: #i.e., at least one maximum value was added + sure_fg[up:(up+height), left:(left+width)] += subfg + + elif area > minarea and random() < self.measurefrac: + #simply get maximum of subdist + submax = np.where(subdist == subdist.max()) + sure_fg[up+submax[0][0], left+submax[1][0]] = 1 + + sure_fg = cv2.dilate(sure_fg, np.ones((3, 3))) + return sure_fg + + def characterizeParticle(self, contours): + longellipse, shortellipse = np.nan, np.nan + + cnt = contours + + if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... + ellipse = cv2.fitEllipse(cnt) + shortellipse, longellipse = ellipse[1] + # double Sizes, as the ellipse returns half-axes + # - > THIS is WRONG! fitEllipse returns the FULL width and height of the rotated ellipse + rect = cv2.minAreaRect(cnt) + long, short = rect[1] + if short>long: + long, short = short, long + + return long, short, longellipse, shortellipse, cv2.contourArea(cnt) + + def measurementPoints(self, binParticle, numPoints=1): + binParticle = cv2.copyMakeBorder(binParticle, 1, 1, 1, 1, 0) + dist = cv2.distanceTransform(np.uint8(binParticle), cv2.DIST_L2,3) + ind = np.argmax(dist) + x = [ind//dist.shape[1]-1] + y = [ind%dist.shape[1]-1] + for i in range(numPoints-1): + binParticle.flat[ind] = 0 + dist = cv2.distanceTransform(np.uint8(binParticle), cv2.DIST_L2,3) + ind = np.argmax(dist) + x.append(ind//dist.shape[1]-1) + y.append(ind%dist.shape[1]-1) + return x, y + + def getSubLabelMap(self, labelMap, label): + oneLabel = labelMap==label + i, j = np.arange(labelMap.shape[0]), np.arange(labelMap.shape[1]) + i1, i2 = i[np.any(oneLabel, axis=1)][[0,-1]] + j1, j2 = j[np.any(oneLabel, axis=0)][[0,-1]] + sub = labelMap[i1:i2+1, j1:j2+1] + sub = (sub == label)*label + return sub, [i1, i2], [j1, j2] + + def apply2Image(self, img, seedpoints, deletepoints, seedradius, return_step=None): + t0 = time() + # convert to gray image and do histrogram normalization + gray = self.convert2Gray(img) + print("gray") + if self.contrastcurve: + xi, arr = self.calculateHistFunction(self.points) + gray = arr[gray] + print("contrast curve") + if self.cancelcomputation: + return None, None, None + + # return even if inactive! + if return_step=="contrastcurve": return gray, 0 + + # image blur for noise-reduction + blur = cv2.medianBlur(gray, self.blurRadius) + blur = np.uint8(blur*(255/blur.max())) + if return_step=="blurRadius": return blur, 0 + print("blur") + if self.cancelcomputation: + return None, None, None + + # thresholding + thresh = cv2.threshold(blur, int(255*self.threshold), 255, cv2.THRESH_BINARY)[1] + if return_step=="threshold": return thresh, 0 + print("threshold") + if self.cancelcomputation: + return None, None, None + + #close holes darkter than self.max_brightness +# if self.maxholebrightness > 0.01: <-- NO! Always do it, 0 closes ALL holes + self.closeBrightHoles(thresh, blur, self.maxholebrightness) + print("closed holes") + + if return_step=='maxholebrightness': return thresh, 0 + if self.cancelcomputation: + return None, None, None + + if self.erodeconvexdefects>0: + erthresh = self.erodeConvexDefects(thresh, self.erodeconvexdefects) ##ist erthresh hier eigentlich notwendig? Können wir bei Bedarf nicht einfach "thresh" überschreiben, anstatt noch ein großes Bild in den Speicher zu laden? + else: + erthresh = thresh + print("erodeconvexdefects") + if self.cancelcomputation: + return None, None, None + # return even if inactive! + + if return_step=="erodeconvexdefects": + if self.erodeconvexdefects > 0: return erthresh, 0 + else: return thresh, 0 + + dist_transform = cv2.distanceTransform(erthresh, cv2.DIST_L2,5) + print("distanceTransform") + if self.cancelcomputation: + return None, None, None + + ####get sure_fg + '''the peak_local_max function takes the min distance between peaks. Unfortunately, that means that individual + particles smaller than that distance are consequently disregarded. Hence, we need a connectec_components approach''' + + sure_fg = self.getSureForeground(erthresh, self.minparticledistance, self.minparticlearea) + + # modify sure_fg with seedpoints and deletepoints + if len(deletepoints)>0: + h, w = sure_fg.shape[:2] + mask = np.zeros((h+2, w+2), np.uint8) + for p in np.int32(deletepoints): + cv2.floodFill(sure_fg, mask, tuple(p), 0) + for p in np.int32(deletepoints): + cv2.circle(sure_fg, tuple(p), int(seedradius), 0, -1) + + for p in np.int32(seedpoints): + cv2.circle(sure_fg, tuple(p), int(seedradius), 1, -1) + + sure_bg = cv2.dilate(erthresh, np.ones((5, 5)), iterations = 1) + sure_bg = self.closeHoles(sure_bg) + print("sure_fg, sure_bg") + + sure_bg = cv2.dilate(thresh, np.ones((5, 5)), iterations = 1) + sure_bg = self.closeHoles(sure_bg) + print("sure_fg, sure_bg") + if self.cancelcomputation: + return None, None, None + + unknown = cv2.subtract(sure_bg, sure_fg) + + ret, markers = cv2.connectedComponents(sure_fg) + markers = markers+1 + markers[unknown==255] = 0 + print("connectedComponents") + if self.cancelcomputation: + return None, None, None + + if return_step=="sure_fg": + img = np.zeros_like(sure_fg) + img[np.nonzero(sure_fg)] |= 1 #dilation of sure_fg is included in self.getSureForeground + img[np.nonzero(sure_bg)] |= 2 + return img, 1 + + #ich habe jetzt nur noch den Skimage Watershed integriert. Oben auskommentiert der opencv watershed, falls wir ihn doch nochmal für irgendwas brauchen... + markers = ndi.label(sure_fg)[0] + markers = watershed(-dist_transform, markers, mask=sure_bg, compactness = self.compactness, watershed_line = True) #labels = 0 for background, 1... for particles + + print("watershed") + if self.cancelcomputation: + return None, None, None + if return_step=="watershed": + return np.uint8(255*(markers!=0)), 0 + + temp, contours, hierarchy = cv2.findContours(markers, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) + print("contours") + if self.cancelcomputation: + return None, None, None + + particlestats = [] + measurementpoints = [] + + tmpcontours = [contours[i] for i in range(len(contours)) if hierarchy[0,i,3]<0] + contours = [] + + for i, cnt in enumerate(tmpcontours): + #print(cnt.shape, hierarchy[0,i,3]) + label = markers[cnt[0,0,1],cnt[0,0,0]] + if label==0: + continue + particlestats.append(self.characterizeParticle(cnt)) + x0, x1 = cnt[:,0,0].min(), cnt[:,0,0].max() + y0, y1 = cnt[:,0,1].min(), cnt[:,0,1].max() + subimg = (markers[y0:y1+1,x0:x1+1]).copy() + subimg[subimg!=label] = 0 + y, x = self.measurementPoints(subimg) + contours.append(cnt) + for index in range(0, len(x)): + measurementpoints.append([x[index] + x0, y[index] + y0]) + + print(len(np.unique(markers))-1, len(contours)) + print("stats") + + #img = np.zeros_like(thresh) + #img[markers!=0] = 255. + if return_step is not None: + raise NotImplementedError(f"this particular return_step: {return_step} is not implemented yet") + print("contours") + + tf = time() + print("particle detection took:", tf-t0, "seconds") + return measurementpoints, contours, particlestats diff --git a/simulatedraman.py b/simulatedraman.py new file mode 100644 index 0000000..0cba7ad --- /dev/null +++ b/simulatedraman.py @@ -0,0 +1,117 @@ +# -*- 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 . + +Simualted Raman interface module for testing without actual raman system connected +""" + +from time import sleep +from dataset import loadData +from shutil import copyfile + +from ramanbase import RamanBase + + +class SimulatedRaman(RamanBase): + def __init__(self): + super().__init__() + fname = "fakeData/test1.pkl" + ds = loadData(fname) + self.currentpos = None, 0., 0. + self.currentZ = 0. + self.positionlist = ds.fitpoints + self.znum = len(ds.zpositions) + self.gridnum = len(ds.grid) + self.positionindex = 0 + self.imageindex = 0 + + def getRamanPositionShift(self): + return 0., 0. + + def connect(self): + self.connected = True + self.imageindex = 0 + return True + + def disconnect(self): + self.connected = False + + def getPosition(self): + if self.currentpos[0] is None: + pos = self.positionlist[self.positionindex] + self.positionindex = (self.positionindex+1)%len(self.positionlist) + else: + pos = self.currentpos + return pos + + def getSoftwareZ(self): + return self.currentpos[2] + + def getUserZ(self): + assert self.connected + if self.currentpos[0] is None: + return self.positionlist[self.positionindex][2] + else: + return self.currentZ + + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + assert self.connected + if z is None: + self.currentpos = x, y, self.currentpos[2] + else: + self.currentpos = x, y, z + sleep(1.) + + def moveZto(self, z, epsz=0.011): + assert self.connected + self.currentpos = self.currentpos[0], self.currentpos[1], z + + def saveImage(self, fname): + assert self.connected + i, k = self.imageindex//self.znum, self.imageindex%self.znum + #print("Fake saving:", fname) + copyfile(f"fakeData/scanimages/image_{i}_{k}.bmp", fname) + self.imageindex = (self.imageindex+1)%(self.znum*self.gridnum) + sleep(.01) + + def getImageDimensions(self): + """ Get the image width and height in um and the orientation angle in degrees. + """ + assert self.connected + width, height, angle = 463.78607177734375, 296.0336608886719, -0.04330849274992943 + return width, height, angle + + def startSinglePointScan(self): + assert self.connected + print("Fake scan") + sleep(.3) + + def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + assert self.connected + print("Scanning ",numberofscans, "particle positions") + self.timeseries = numberofscans + sleep(.3) + + + def nextTimeSeriesScan(self, num): + #assert self.timeseries # will not work since initiateTimeSeries is done by a different class + print("Scan number:", num) + sleep(.3) + if num==self.timeseries-1: + self.timeseries = False \ No newline at end of file diff --git a/viewitems.py b/viewitems.py new file mode 100644 index 0000000..f14936a --- /dev/null +++ b/viewitems.py @@ -0,0 +1,229 @@ +# -*- 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 numpy as np +from PyQt5 import QtCore, QtWidgets, QtGui + +class SegementationContours(QtWidgets.QGraphicsItem): + def __init__(self, contours=[], pos=(0,0)): + super().__init__() + self.setPos(pos[0], pos[1]) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.brect = QtCore.QRectF(0,0,1,1) + self.resetContours(contours) + + def boundingRect(self): + return self.brect + + def resetContours(self, contours): + cp = [] + x0 = None + for c in contours: + polygon = QtGui.QPolygonF() + if x0 is None: + x0 = c[:,0,0].min() + x1 = c[:,0,0].max() + y0 = c[:,0,1].min() + y1 = c[:,0,1].max() + else: + x0 = min(x0, c[:,0,0].min()) + x1 = max(x1, c[:,0,0].max()) + y0 = min(y0, c[:,0,1].min()) + y1 = max(y1, c[:,0,1].max()) + for ci in c: + polygon.append(QtCore.QPointF(ci[0,0],ci[0,1])) + cp.append(polygon) + if x0 is None: + self.brect = QtCore.QRectF(0,0,1,1) + else: + self.brect.setCoords(x0,y0,x1,y1) + self.contours = cp + self.update() + + def paint(self, painter, option, widget): + painter.setPen(QtCore.Qt.green) + for c in self.contours: + painter.drawPolygon(c) + + +class FitPosIndicator(QtWidgets.QGraphicsItem): + indicatorSize = 80 + def __init__(self, number, pos=(0,0)): + super().__init__() + self.setPos(pos[0], pos[1]) + self.number = number + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + + def boundingRect(self): + return QtCore.QRectF(-self.indicatorSize-1,-self.indicatorSize-1, + 2*self.indicatorSize+2,2*self.indicatorSize+2) + + def paint(self, painter, option, widget): + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtGui.QColor(250,250,0,150)) + rect = QtCore.QRectF(-self.indicatorSize,-self.indicatorSize, + 2*self.indicatorSize,2*self.indicatorSize) + font = painter.font() + font.setPointSize(40) + painter.setFont(font) + painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) + painter.drawEllipse(rect) + + def shape(self): + path = QtGui.QPainterPath() + path.addEllipse(self.boundingRect()) + return path + +class RamanScanIndicator(QtWidgets.QGraphicsItem): + def __init__(self, view, number, radius, pos=(0,0)): + super().__init__() + #self.setAcceptedMouseButtons(QtCore.Qt.LeftButton) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) + self.view = view + self.number = number + self.radius = radius + self.highlight = False + self.setPos(pos[0], pos[1]) + + def setHighLight(self, highlight): + if highlight!=self.highlight: + self.highlight = highlight + self.update() + + def boundingRect(self): + return QtCore.QRectF(-self.radius-1,-self.radius-1, + 2*self.radius+2,2*self.radius+2) + + def shape(self): + path = QtGui.QPainterPath() + path.addEllipse(self.boundingRect()) + return path + + def paint(self, painter, option, widget): + if self.highlight: + painter.setPen(QtCore.Qt.red) + painter.setBrush(QtGui.QColor(100,250,100,150)) + else: + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtGui.QColor(50,50,250,150)) + rect = QtCore.QRectF(-self.radius, -self.radius, 2*self.radius, 2*self.radius) + painter.drawEllipse(rect) + font = painter.font() + font.setPointSize(10) + painter.setFont(font) + painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) + + def mousePressEvent(self, event): + p = event.pos() + x, y = p.x(), p.y() + r = np.sqrt(x**2+y**2) + print(self.number, r) + if r + +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 socket import gethostname +from WITecCOM import WITecCOM + +wc = WITecCOM(gethostname()) + +# try connection +print("Connecting to WITec Raman. Please make sure, that Remote Access is enabled.") +success = wc.connect() +if not success: + print("Connection to WITec Raman failed!") + exit(1) + +# read position +print("Reading position") +posx, posy, posz = wc.getPosition() +print(f"Current position is: x={posx}µm, y={posy}µm, z={posz}µm") +softwarez = wc.getSoftwareZ() +print(f"Current software z position is also z={softwarez}µm") +userz = wc.getUserZ() +print(f"Current user z position is z={userz}µm") + +# image read +print("Taking image and saving to tmp.bmp") +wc.saveImage("tmp.bmp") + +# moving x and y +print("Test to move 100µm in x and after that 100µm in y direction followed by 10µm up.") +print("Make sure no obstacles block the objective (better turn it away)!") +answer = input("continue ([no]/yes)?") +if answer!="yes": + wc.disconnect() + exit(0) + +print("moving in x-direction:") +wc.moveToAbsolutePosition(posx+100., posy) +posx, posy, posz = wc.getPosition() +print(f"Current position is: x={posx}µm, y={posy}µm, z={posz}µm") + +print("moving in y-direction:") +wc.moveToAbsolutePosition(posx, posy+100.) +posx, posy, posz = wc.getPosition() +print(f"Current position is: x={posx}µm, y={posy}µm, z={posz}µm") + +print("moving 10µm up") +wc.moveToAbsolutePosition(posx, posy, posz+10.) +posx, posy, posz = wc.getPosition() +print(f"Current position is: x={posx}µm, y={posy}µm, z={posz}µm") + +print("Test completed") + +wc.disconnect() + + -- GitLab