diff --git a/.gitignore b/.gitignore index 192a19dedbf286bc967298724e35ea0f87074ffc..358b8330af18b262022db5f1597a44768dde09d3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ ramancom/renishawtesting.py *.lib *.obj + +*.pyc diff --git a/analysis/advancedWITec.py b/analysis/advancedWITec.py new file mode 100644 index 0000000000000000000000000000000000000000..f2ac2e3b92b1c38436805d7eb5516023a719dd4b --- /dev/null +++ b/analysis/advancedWITec.py @@ -0,0 +1,213 @@ +""" +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 numpy as np + +class AdvancedWITecSpectra(object): + """ + Handles Spectra formatting and storage when using the advanced "silent spectrum" option in the WITec COM interface + :return: + """ + def __init__(self): + super(AdvancedWITecSpectra, self).__init__() + self.dsetpath = None + self.tmpspecpath = None + self.curSpecIndex = None + self.excitWavel = None + self.spectraBatchSize = None + + def setDatasetPath(self, path): + self.dsetpath = path + + def setSpectraBatchSize(self, batchSize): + self.spectraBatchSize = batchSize + + def createTmpSpecFolder(self): + assert self.dsetpath is not None + self.tmpspecpath = os.path.join(self.dsetpath, 'spectra') + if not os.path.exists(self.tmpspecpath): + os.mkdir(self.tmpspecpath) + + def registerNewSpectrum(self, specString, specIndex): + wavenumbers, averaged_counts = self.deassembleSpecString(specString) + if specIndex == 0: + fname = os.path.join(self.tmpspecpath, 'Wavenumbers.npy') + np.save(fname, wavenumbers) + + fname = os.path.join(self.tmpspecpath, f'Spectrum ({specIndex}).npy') + np.save(fname, averaged_counts) + self.curSpecIndex = specIndex + + def createSummarizedSpecFiles(self): + allSpectra = self.getAllSpectra() + allspecfname = os.path.join(self.dsetpath, 'spectra.npy') + np.save(allspecfname, allSpectra) + self.createTrueMatchTxt(allSpectra, self.excitWavel) + + def deassembleSpecString(self, specString): + keywordLines = self.getKeyWordLines(specString) + + try: + specSize = self.getSpecSize(specString, keywordLines['SpectrumSize'][0]) + except: + print(keywordLines) + raise + wavenumbers = self.getWavenumbers(specString, keywordLines['[XData]'][0], specSize) + xDataKind = self.getXDataKind(specString, keywordLines['XDataKind'][0]) + self.excitWavel = self.getExcitationWavelength(specString, keywordLines['ExcitationWavelength'][0]) + + if xDataKind == 'nm': + wavenumbers = self.convertWavenumbersFrom_nm_to_Percm(wavenumbers, self.excitWavel) + else: + print('warning, unexpected xDataKind:', xDataKind) + print('please check how to deal with it!!!') + assert False + + averaged_counts = self.getAveragedSpectra(specString, keywordLines['SpectrumData'], specSize) + return wavenumbers, averaged_counts + + def getKeyWordLines(self, specString): + keywordLines = {'[WITEC_TRUEMATCH_ASCII_HEADER]': [], + '[XData]': [], + 'ExcitationWavelength': [], + 'SpectrumSize': [], + 'XDataKind': [], + 'SpectrumHeader': [], + 'SampleMetaData': [], + 'SpectrumData': []} + + for index, line in enumerate(specString): + for key in keywordLines.keys(): + if line.find(key) != -1: + keywordLines[key].append(index) + return keywordLines + + def getSpecSize(self, specString, specSizeIndex): + line = specString[specSizeIndex] + specSize = [int(s) for s in line.split() if self.isNumber(s)] + assert len(specSize) == 1 + return specSize[0] + + def getExcitationWavelength(self, specString, excitWavenumIndex): + line = specString[excitWavenumIndex] + excitWavel = [float(s) for s in line.split() if self.isNumber(s)] + assert len(excitWavel) == 1 + return excitWavel[0] + + def getXDataKind(self, specString, xDataKindIndex): + line = specString[xDataKindIndex] + return line.split()[-1] + + def getWavenumbers(self, specString, startXDataIndex, specSize): + wavenumbers = [] + curIndex = startXDataIndex+1 + curLine = specString[curIndex] + + while self.isNumber(curLine): + wavenumbers.append(float(curLine)) + curIndex += 1 + curLine = specString[curIndex] + + assert len(wavenumbers) == specSize + return wavenumbers + + def convertWavenumbersFrom_nm_to_Percm(self, wavenumbers, excit_nm): + newWavenumbers = [] + for abs_nm in wavenumbers: + raman_shift = 1E7/excit_nm - 1E7/abs_nm + newWavenumbers.append(raman_shift) + return newWavenumbers + + def getAveragedSpectra(self, specString, startIndices, specSize): + startIndices = [i+1 for i in startIndices] #the spectrum starts one line AFTER the SpectrumData-Tag + spectrum = [] + for index in range(specSize): + curSlice = [float(specString[index + startIndex]) for startIndex in startIndices] + spectrum.append(np.mean(curSlice)) + return spectrum + + def getAllSpectra(self): + numSpectra = self.curSpecIndex + 1 + wavenumbers = np.load(os.path.join(self.tmpspecpath, 'Wavenumbers.npy')) + allSpectra = np.zeros((wavenumbers.shape[0], numSpectra+1)) + allSpectra[:, 0] = wavenumbers + for i in range(numSpectra): + curSpecPath = os.path.join(self.tmpspecpath, f'Spectrum ({i}).npy') + allSpectra[:, i+1] = np.load(curSpecPath ) + os.remove(curSpecPath) + return allSpectra + + def createTrueMatchTxt(self, allSpectra, wavelength): + def writeHeader(fp): + fp.write('[WITEC_TRUEMATCH_ASCII_HEADER]\n\r') + fp.write('Version = 2.0\n\r\n\r') + + def writeWavenumbers(fp, wavenumbers): + fp.write('[XData]\n\r') + for line in wavenumbers: + fp.write(str(line) + '\n\r') + + def writeSpectrum(fp, intensities): + fp.write('\n\r') + fp.write('[SpectrumHeader]\n\r') + fp.write(f'Title = Spectrum {specIndex} \n\r') + fp.write(f'ExcitationWavelength = {wavelength}\n\r') + fp.write(f'SpectrumSize = {specSize}\n\r') + fp.write('XDataKind = 1/cm\n\r\n\r') + fp.write('[SampleMetaData]\n\r') + fp.write(f'int Spectrum_Number = {specIndex}\n\r\n\r') + fp.write('[SpectrumData]\n\r') + for line in intensities: + fp.write(str(line) + '\n\r') + + wavenumbers = allSpectra[:, 0] + spectra = allSpectra[:, 1:] + specSize = allSpectra.shape[0] + del allSpectra + numSpectra = spectra.shape[1] + numBatches = int(np.ceil(numSpectra/self.spectraBatchSize)) + + for batchIndex in range(numBatches): + outName = os.path.join(self.dsetpath, f'SpectraForTrueMatch {batchIndex}.txt') + if os.path.exists(outName): + os.remove(outName) + + if batchIndex < numBatches-1: + specIndicesInBatch = np.arange(batchIndex*self.spectraBatchSize, (batchIndex+1)*self.spectraBatchSize) + else: + specIndicesInBatch = np.arange(batchIndex*self.spectraBatchSize, numSpectra) + + with open(outName, 'w') as fp: + writeHeader(fp) + writeWavenumbers(fp, wavenumbers) + + for specIndex in specIndicesInBatch: + spec = spectra[:, specIndex] + writeSpectrum(fp, spec) + + def isNumber(self, string): + isNumber = False + try: + float(string) + isNumber = True + except ValueError: + pass + return isNumber diff --git a/analysis/ftirAperture.py b/analysis/ftirAperture.py new file mode 100644 index 0000000000000000000000000000000000000000..581bb768f66628d9fe7bfc97a592f0133c27d832 --- /dev/null +++ b/analysis/ftirAperture.py @@ -0,0 +1,142 @@ +# -*- 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 scipy import optimize +import cv2 +from ..cythonModules.getMaxRect import findMaxRect + + +#################################################### +'''Code taken from https://github.com/pogam/ExtractRect/blob/master/extractRect.py and modified for our use''' +#################################################### + +def residual(angle, img): + img_rot, rotationMatrix = rotateImageAroundAngle(img, angle) + point1, point2, width, height, max_area = findMaxRect(img_rot) + return 1/max_area + +def getFinalRectangle(angle, img, shiftScaleParam): + ny = img.shape[1] + img_rot, rotationMatrix = rotateImageAroundAngle(img, angle) + point1, point2, width, height, max_area = findMaxRect(img_rot) + width *= shiftScaleParam.scale_factor + height *= shiftScaleParam.scale_factor + + #invert rectangle + M_invert = cv2.invertAffineTransform(rotationMatrix) + rect_coord = [point1, [point1[0],point2[1]] , + point2, [point2[0],point1[1]] ] + + rect_coord_ori = [] + for coord in rect_coord: + rect_coord_ori.append(np.dot(M_invert,[coord[0],(ny-1)-coord[1],1])) + + #transform to numpy coord of input image + coord_out = [] + for coord in rect_coord_ori: + coord_out.append([shiftScaleParam.scale_factor*round( coord[0],0)-shiftScaleParam.shift_x,\ + shiftScaleParam.scale_factor*round((ny-1)-coord[1],0)-shiftScaleParam.shift_y]) + + #transpose back the original coords: + coord_out = transposeRectCoords(coord_out) + + return coord_out, round(width), round(height) + +def transposeRectCoords(rectCoords): + #mirror x and y: + newCoords = [] + for i in range(len(rectCoords)): + newCoords.append([rectCoords[i][1], rectCoords[i][0]]) + return newCoords + +def addBorderToMakeImageSquare(img): + nx, ny = img.shape + if nx != ny: + n = max([nx,ny]) + img_square = np.ones([n,n]) + xshift = (n-nx)/2 + yshift = (n-ny)/2 + if yshift == 0: + img_square[int(xshift):int(xshift+nx),:] = img + else: + img_square[:,int(yshift):int(yshift+ny)] = img + else: + xshift = 0 + yshift = 0 + img_square = img + + return img_square, xshift, yshift + +def limitImageSize(img, limitSize): + if img.shape[0] > limitSize: + img_small = cv2.resize(img,(limitSize, limitSize),interpolation=0) + scale_factor = 1.*img.shape[0]/img_small.shape[0] + else: + img_small = img + scale_factor = 1 + + return img_small, scale_factor + +def makeImageOddSized(img): + # set the input data with an odd number of point in each dimension to make rotation easier + nx,ny = img.shape + nx_extra = -nx + ny_extra = -ny + if nx%2==0: + nx+=1 + nx_extra = 1 + if ny%2==0: + ny+=1 + ny_extra = 1 + img_odd = np.ones([img.shape[0]+max([0,nx_extra]),img.shape[1]+max([0,ny_extra])], dtype=np.uint8) + img_odd[:-nx_extra, :-ny_extra] = img + return img_odd + +def rotateImageAroundAngle(img, angle): + nx,ny = img.shape + rotationMatrix = cv2.getRotationMatrix2D(((nx-1)/2,(ny-1)/2),angle,1) + img_rot = np.array(cv2.warpAffine(img, rotationMatrix, img.shape, flags=cv2.INTER_NEAREST, borderValue=1)) + img_rot = img_rot.astype(np.uint8) + return img_rot, rotationMatrix + +def findRotatedMaximalRectangle(img, nbre_angle=4, limit_image_size=300): + img_square, shift_x, shift_y = addBorderToMakeImageSquare(img) + img_small, scale_factor = limitImageSize(img_square, limit_image_size) + img_odd = makeImageOddSized(img_small) + nx,ny = nx_odd, ny_odd = img_odd.shape + shiftScaleParam = ShiftAndScaleParam(shift_x, shift_y, scale_factor) + + # angle_range = ([(90.,180.),]) + angle_range = ([(0., 90.), ]) + coeff1 = optimize.brute(residual, angle_range, args=(img_odd,), Ns=nbre_angle, finish=None) + popt = optimize.fmin(residual, coeff1, args=(img_odd,), xtol=5, ftol=1.e-5, disp=False) + opt_angle = popt[0] + + rectangleCoords, width, height = getFinalRectangle(opt_angle, img_odd, shiftScaleParam) + + return rectangleCoords, opt_angle, width, height + +class ShiftAndScaleParam(object): + def __init__(self, shift_x=0, shift_y=0, scale_factor=1): + self.shift_x = shift_x + self.shift_y = shift_y + self.scale_factor = scale_factor \ No newline at end of file diff --git a/analysis/particleAndMeasurement.py b/analysis/particleAndMeasurement.py index de6ce7947d380fc69b38a7fa8a0a8b4b7ee145ca..c8e1bbeb4d071f2641cbd8a522855ebcb2906e9e 100644 --- a/analysis/particleAndMeasurement.py +++ b/analysis/particleAndMeasurement.py @@ -29,6 +29,7 @@ class Particle(object): self.height = None self.area = None self.contour = None + self.aperture = None self.measurements = [] self.color = None self.shape = None diff --git a/analysis/particleCharacterization.py b/analysis/particleCharacterization.py index 3b52afa96ba6ea6faea7095fad0a9b9233db6af1..a07853feb0997c707e008428f5aac3ffd57bf6a2 100644 --- a/analysis/particleCharacterization.py +++ b/analysis/particleCharacterization.py @@ -26,17 +26,34 @@ from copy import deepcopy from .particleClassification.colorClassification import ColorClassifier from .particleClassification.shapeClassification import ShapeClassifier +from .ftirAperture import findRotatedMaximalRectangle from ..segmentation import closeHolesOfSubImage from ..errors import InvalidParticleError from ..helperfunctions import cv2imread_fix + +class FTIRAperture(object): + """ + Configuration for an FTIR aperture. CenterCoords, width and height in Pixels + """ + centerX: float = None + centerY: float = None + centerZ: float = None + width: float = None + height: float = None + angle: float = None + rectCoords: list = None + + class ParticleStats(object): - longSize = None - shortSize = None - height = None - area = None - shape = None - color = None + longSize: float = None + shortSize: float = None + height: float = None + area: float = None + shape: str = None + color: str = None + aperture: FTIRAperture = None + def particleIsValid(particle): if particle.longSize == 0 or particle.shortSize == 0: @@ -45,7 +62,8 @@ def particleIsValid(particle): if cv2.contourArea(particle.contour) == 0: return False return True - + + def updateStatsOfParticlesIfNotManuallyEdited(particleContainer): dataset = particleContainer.datasetParent fullimage = loadFullimageFromDataset(dataset) @@ -59,6 +77,23 @@ def updateStatsOfParticlesIfNotManuallyEdited(particleContainer): newStats = getParticleStatsWithPixelScale(particle.contour, dataset, fullimage, zimg) particle.__dict__.update(newStats.__dict__) + +def getFTIRAperture(partImg: np.ndarray) -> FTIRAperture: + rectPoints, angle, width, heigth = findRotatedMaximalRectangle(partImg, nbre_angle=4, limit_image_size=300) + xvalues: list = [i[0] for i in rectPoints] + yvalues: list = [i[1] for i in rectPoints] + + aperture = FTIRAperture() + aperture.height = heigth + aperture.width = width + aperture.angle = angle + aperture.centerX = int(round(min(xvalues) + (max(xvalues) - min(xvalues)) / 2)) + aperture.centerY = int(round(min(yvalues) + (max(yvalues) - min(yvalues)) / 2)) + aperture.rectCoords = rectPoints + + return aperture + + def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None): if fullimage is None: fullimage = loadFullimageFromDataset(dataset) @@ -84,17 +119,31 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None): newStats.longSize *= pixelscale newStats.shortSize *= pixelscale - partImg = getParticleImageFromFullimage(cnt, fullimage) + partImg, extrema = getParticleImageFromFullimage(cnt, fullimage) newStats.color = getParticleColor(partImg) + + padding: int = int(2) + imgforAperture = np.zeros((partImg.shape[0]+2*padding, partImg.shape[1]+2*padding)) + imgforAperture[padding:partImg.shape[0]+padding, padding:partImg.shape[1]+padding] = np.mean(partImg, axis=2) + imgforAperture[imgforAperture > 0] = 1 # convert to binary image + imgforAperture = np.uint8(1 - imgforAperture) # background has to be 1, particle has to be 0 + aperture: FTIRAperture = getFTIRAperture(imgforAperture) + aperture.centerX = aperture.centerX + extrema[0] - padding + aperture.centerY = aperture.centerY + extrema[2] - padding + aperture.centerZ = newStats.height + newStats.aperture = aperture + return newStats + def getFibreDimension(contour): longSize = cv2.arcLength(contour, True)/2 img = contoursToImg([contour])[0] dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) maxThickness = np.max(dist)*2 return longSize, maxThickness - + + def getParticleColor(imgRGB, colorClassifier=None): img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL) meanHSV = cv2.mean(img) @@ -103,14 +152,16 @@ def getParticleColor(imgRGB, colorClassifier=None): color = colorClassifier.classifyColor(meanHSV) return color + def getParticleShape(contour, particleHeight, shapeClassifier=None): if shapeClassifier is None: shapeClassifier = ShapeClassifier() shape = shapeClassifier.classifyShape(contour, particleHeight) return shape + def getParticleHeight(contour, fullZimg, dataset): - zimg = getParticleImageFromFullimage(contour, fullZimg) + zimg, _extrema = getParticleImageFromFullimage(contour, fullZimg) if zimg.shape[0] == 0 or zimg.shape[1] == 0: raise InvalidParticleError @@ -122,6 +173,7 @@ def getParticleHeight(contour, fullZimg, dataset): height = avg_ZValue/255.*(z1-z0) + z0 return height + def getContourStats(cnt): ##characterize particle if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... @@ -138,10 +190,12 @@ def getContourStats(cnt): return long, short, area + def mergeContours(contours): img, xmin, ymin, padding = contoursToImg(contours) return imgToCnt(img, xmin, ymin, padding) + def getParticleImageFromFullimage(contour, fullimage): contourCopy = deepcopy(contour) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) @@ -159,8 +213,9 @@ def getParticleImageFromFullimage(contour, fullimage): img[mask == 0] = 0 img = np.array(img, dtype = np.uint8) - return img - + return img, (xmin, xmax, ymin, ymax) + + def contoursToImg(contours, padding=0): contourCopy = deepcopy(contours) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) @@ -181,6 +236,7 @@ def contoursToImg(contours, padding=0): img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3)))) return img, xmin, ymin, padding + def imgToCnt(img, xmin, ymin, padding=0): def getContour(img, contourMode): if cv2.__version__ > '3.5': @@ -214,6 +270,7 @@ def imgToCnt(img, xmin, ymin, padding=0): return contour + def getContourExtrema(contours): try: cnt = np.vstack(tuple(contours)) @@ -228,6 +285,7 @@ def getContourExtrema(contours): return xmin, xmax, ymin, ymax + def getParticleCenterPoint(contour): img, xmin, ymin, padding = contoursToImg(contour) dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) @@ -238,8 +296,10 @@ def getParticleCenterPoint(contour): y += ymin return x, y + def loadFullimageFromDataset(dataset): return cv2imread_fix(dataset.getImageName()) + def loadZValImageFromDataset(dataset): return cv2imread_fix(dataset.getZvalImageName(), cv2.IMREAD_GRAYSCALE) \ No newline at end of file diff --git a/analysis/particleContainer.py b/analysis/particleContainer.py index 67e060aaed1be5b1e569bf8972dd7b2a410ad2c4..bfd312289c347fc479c460bdb97bed439c53ad3b 100644 --- a/analysis/particleContainer.py +++ b/analysis/particleContainer.py @@ -185,12 +185,20 @@ class ParticleContainer(object): particle = self.getParticleOfIndex(partIndex) return particle.getParticleAssignment() - def getMeasurementPixelCoords(self): - coords = [] + def getMeasurementPixelCoords(self) -> list: + coords: list = [] for meas in self.measurements: coords.append([meas.pixelcoord_x, meas.pixelcoord_y]) return coords - + + def getApertureCenterCoords(self) -> list: + coords: list = [] + for particle in self.particles: + assert particle.aperture is not None, 'Aperture for particle was not set!' + coords.append([particle.aperture.centerX, particle.aperture.centerY]) + return coords + + def getNumberOfParticlesOfAssignment(self, assignment): num = 0 for particle in self.particles: diff --git a/cythonModules/getMaxRect.pyx b/cythonModules/getMaxRect.pyx new file mode 100644 index 0000000000000000000000000000000000000000..6224056f2edd182e2e8325ab8f0ec98e24daa0d9 --- /dev/null +++ b/cythonModules/getMaxRect.pyx @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- 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 + +cimport numpy as np +cimport cython + +NP_INT32 = np.int32 +ctypedef np.int32_t INT32_t + +@cython.boundscheck(False) # assume: no index larger than N-1 +@cython.wraparound(False) # assume: no neg. index +def findMaxRect(np.ndarray[np.uint8_t, ndim=2] img): + + '''http://stackoverflow.com/a/30418912/5008845''' + cdef int nrows, ncols, row, col, dh, height, width, minw, area, max_area + cdef np.ndarray[INT32_t, ndim=2] w, h + cdef np.ndarray[INT32_t, ndim=1] rectangle + rectangle = np.zeros(6, dtype=NP_INT32) + nrows, ncols = img.shape[0], img.shape[1] + w = np.zeros((nrows, ncols), dtype=NP_INT32) + h = np.zeros((nrows, ncols), dtype=NP_INT32) + max_area = 0 + + for row in range(nrows): + for col in range(ncols): + if img[row, col] == 1: + continue + + if row == 0: + h[row, col] = 1 + else: + h[row, col] = h[row-1, col]+1 + + if col == 0: + w[row, col] = 1 + else: + w[row, col] = w[row, col-1]+1 + + minw = w[row, col] + for dh in range(h[row, col]): + minw = min(minw, w[row-dh, col]) + area = (dh+1)*minw + if area > max_area: + max_area = area + rectangle[0], rectangle[1] = row-dh, col-minw+1 + rectangle[2], rectangle[3] = row, col + rectangle[4], rectangle[5] = dh+1, minw + + point1 = rectangle[0], rectangle[1] + point2 = rectangle[2], rectangle[3] + height, width = rectangle[4], rectangle[5] + return point1, point2, width, height, max_area \ No newline at end of file diff --git a/cythonModules/setup_getMaxRect.py b/cythonModules/setup_getMaxRect.py new file mode 100644 index 0000000000000000000000000000000000000000..2954e9aa35286bb0cb0842cff5e17a26ee1ebafb --- /dev/null +++ b/cythonModules/setup_getMaxRect.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- 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, os + +if os.path.exists('getMaxRect.c'): + print('removing c file') + os.remove('getMaxRect.c') + +if len(sys.argv)==1: + sys.argv.append("build_ext") + sys.argv.append("--inplace") + +ext = Extension("getMaxRect", ["getMaxRect.pyx"], + extra_compile_args=['-O3'],) + +setup( + name="optimized find max rect-module", + ext_modules=cythonize([ext]), + include_dirs=[np.get_include()] +) diff --git a/external/setuptsp.py b/cythonModules/setuptsp.py similarity index 100% rename from external/setuptsp.py rename to cythonModules/setuptsp.py diff --git a/external/tsp.pyx b/cythonModules/tsp.pyx similarity index 100% rename from external/tsp.pyx rename to cythonModules/tsp.pyx diff --git a/detectionview.py b/detectionview.py index 879343b72196465704cc025f18ad51693925ec02..653a20687b5d4f62de878a4d1c59e4d7b02fab08 100644 --- a/detectionview.py +++ b/detectionview.py @@ -26,6 +26,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg import matplotlib.pyplot as plt from threading import Thread +from win32api import GetSystemMetrics # I use that to find out screen resolution. Might not work on Linux that way?? from .segmentation import Segmentation from .analysis.particleCharacterization import getParticleStatsWithPixelScale, loadZValImageFromDataset @@ -34,7 +35,9 @@ from .uielements import TimeEstimateProgressbar from .scenePyramid import ScenePyramid from .gepardlogging import setDefaultLoggingConfig -Nscreen = 1000 +screenHeight = GetSystemMetrics(1) +Nscreen = np.clip(1000, 0, round(screenHeight*0.9)) # account for too small displays + class HistWidget(QtWidgets.QWidget): def __init__(self, histcallback, curvecallback, parent=None): @@ -416,8 +419,13 @@ class ParticleDetectionView(QtWidgets.QWidget): grid.addWidget(QtWidgets.QLabel("Click mouse to add seeds, Click+Shift to add deletepoints"), i+3, 0, 1, 2, QtCore.Qt.AlignLeft) grid.addWidget(QtWidgets.QLabel("Click+Alt removes seeds near cursor"), i+4, 0, 1, 2, QtCore.Qt.AlignLeft) self.detectParamsGroup.setLayout(grid) - vbox.addWidget(self.detectParamsGroup) - + paramGroupScrollArea = QtWidgets.QScrollArea(self) + + maxHeight = np.clip(1000, 0, Nscreen*0.8) + paramGroupScrollArea.setFixedHeight(maxHeight) + paramGroupScrollArea.setWidget(self.detectParamsGroup) + vbox.addWidget(paramGroupScrollArea) + self.updateSeedsInSampleViewBtn = QtWidgets.QPushButton("Update Seedpoints in fullimage view", self) self.updateSeedsInSampleViewBtn.released.connect(self.updateSeedsInSampleview) self.hideSeedsInSampleViewBtn = QtWidgets.QPushButton("Hide Seedpoints in fullimage view", self) @@ -777,7 +785,7 @@ class ParticleDetectionView(QtWidgets.QWidget): showErrorMessageAsWidget('Fatal error in particle detection, see detectionlog for info') detectLogger.exception('Fatal error in particle detection') - if measurementPoints is None: # computation was canceled + if measurementPoints is None: # computation was canceled return if self.dataset is not None: @@ -812,6 +820,11 @@ class ParticleDetectionView(QtWidgets.QWidget): self.dataset.save() def getParticleStats(self, contours): + # processWindow = TimeEstimateProgressbar() + # processWindow.setWindowTitle('Updating Particle Stats') + # processWindow.setMaxValue(len(contours)) + # processWindow.enable() + # # processWindow.show() particlestats = [] zvalimg = loadZValImageFromDataset(self.dataset) invalidParticleIndices = [] @@ -823,6 +836,7 @@ class ParticleDetectionView(QtWidgets.QWidget): invalidParticleIndices.append(contourIndex) continue particlestats.append(stats) + # processWindow.setValue(contourIndex) return particlestats, invalidParticleIndices diff --git a/legacyConvert.py b/legacyConvert.py index 0db729744fdb0beed4277c95184e502a1d3e0725..dede92173417932ac6a9c4caeafb879a0c55c066 100644 --- a/legacyConvert.py +++ b/legacyConvert.py @@ -181,7 +181,7 @@ def updateParticleStats(dset): markForDeletion(particle) continue - particleImg = pc.getParticleImageFromFullimage(particle.contour, fullimage) + particleImg, _extrema = pc.getParticleImageFromFullimage(particle.contour, fullimage) if particleImg.shape[0] == 0 or particleImg.shape[1] == 0: markForDeletion(particle) else: diff --git a/opticalscan.py b/opticalscan.py index 9dd4799f23bf1ac1eb677f5f712df338d0225caf..3fa0c396f1b7f3c0a7299adbe819d2a07400b924 100644 --- a/opticalscan.py +++ b/opticalscan.py @@ -52,6 +52,7 @@ def scan(path, sol, zpositions, grid, controlclass, dataqueue, try: ramanctrl = controlclass(logger) + ramanctrl.setupToDatasetPath(os.path.dirname(logpath)) ramanctrl.connect() zlist = list(enumerate(zpositions)) for i, p in enumerate(grid): @@ -502,7 +503,7 @@ class OpticalScan(QtWidgets.QWidget): if len(self.dataset.fitindices)>1: self.areaOptionsGroup.setEnabled(True) softwarez = self.ramanctrl.getSoftwareZ() - if abs(softwarez) >0.1: + if abs(softwarez) > 0.1 and self.ramanctrl.name == 'WITecCOM': 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), diff --git a/ramancom/configRaman.py b/ramancom/configRaman.py index 39028b09c02a573e93acdb4c9d01773eef84d26a..2b08ec659f30a3e9ab8b25e1b96f22bda2099e9f 100644 --- a/ramancom/configRaman.py +++ b/ramancom/configRaman.py @@ -167,6 +167,7 @@ class RamanSettingParam(object): elif self.dtype == 'selectBtn': return obj.text() + if __name__ == '__main__': import sys app = QtWidgets.QApplication(sys.argv) diff --git a/ramancom/ramanbase.py b/ramancom/ramanbase.py index 1fa969edd65eadde2b6121a49804f944cbd2bc12..3edb6ef3accd85ba5fb33968b708d72effef8279 100644 --- a/ramancom/ramanbase.py +++ b/ramancom/ramanbase.py @@ -70,4 +70,12 @@ class RamanBase(object): raise NotImplementedError def finishMeasurement(self, aborted=False): - raise NotImplementedError \ No newline at end of file + raise NotImplementedError + + def setupToDatasetPath(self, dsetpath: str) -> None: + """ + Can be overloaded if the ramancontrol needs to know where the current project is located + :param dsetpath: filepath to the dataset, i.e. the + :return: + """ + pass \ No newline at end of file diff --git a/ramancom/ramancontrol.py b/ramancom/ramancontrol.py index 6a74e72086dfa52887bb6576c83fc28c07a7c8c9..cd59666a9b65b6c44cfa69c6a229d4f6e16be36a 100644 --- a/ramancom/ramancontrol.py +++ b/ramancom/ramancontrol.py @@ -72,4 +72,9 @@ elif interface == "RENISHAW_CONTROL": simulatedRaman = False +elif interface == "THERMO_FTIR": + from .thermoFTIRCom import ThermoFTIRCom + RamanControl = ThermoFTIRCom + simulatedRaman = False + diff --git a/ramancom/thermoFTIR/OmnicDDE.py b/ramancom/thermoFTIR/OmnicDDE.py new file mode 100644 index 0000000000000000000000000000000000000000..bee4ca703c3e35fe1baf0b91fcfb92e875c27d83 --- /dev/null +++ b/ramancom/thermoFTIR/OmnicDDE.py @@ -0,0 +1,7 @@ +import win32ui +import dde +import time +from ddeclient import DDEClient + + + diff --git a/ramancom/thermoFTIR/ddeclient.py b/ramancom/thermoFTIR/ddeclient.py new file mode 100644 index 0000000000000000000000000000000000000000..fd18bead4f3e47af8aff5720b80adee04f8115da --- /dev/null +++ b/ramancom/thermoFTIR/ddeclient.py @@ -0,0 +1,452 @@ +# Taken from: https://github.com/xzos/PyZDDE/blob/master/pyzdde/ddeclient.py + +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Name: ddeclient.py +# Purpose: DDE Management Library (DDEML) client application for communicating +# with Zemax +# +# Notes: This code has been adapted from David Naylor's dde-client code from +# ActiveState's Python recipes (Revision 1). +# Author of original Code: David Naylor, Apr 2011 +# Modified by Indranil Sinharoy +# Copyright: (c) David Naylor +# Licence: New BSD license (Please see the file Notice.txt for further details) +# Website: http://code.activestate.com/recipes/577654-dde-client/ +#------------------------------------------------------------------------------- +from __future__ import print_function +import sys +from ctypes import c_int, c_double, c_char_p, c_void_p, c_ulong, c_char, pointer, cast +from ctypes import windll, byref, create_string_buffer, Structure, sizeof +from ctypes import POINTER, WINFUNCTYPE +from ctypes.wintypes import BOOL, HWND, MSG, DWORD, BYTE, INT, LPCWSTR, UINT, ULONG, LPCSTR + +# DECLARE_HANDLE(name) typedef void *name; +HCONV = c_void_p # = DECLARE_HANDLE(HCONV) +HDDEDATA = c_void_p # = DECLARE_HANDLE(HDDEDATA) +HSZ = c_void_p # = DECLARE_HANDLE(HSZ) +LPBYTE = c_char_p # POINTER(BYTE) +LPDWORD = POINTER(DWORD) +LPSTR = c_char_p +ULONG_PTR = c_ulong + +# See windows/ddeml.h for declaration of struct CONVCONTEXT +PCONVCONTEXT = c_void_p + +# DDEML errors +DMLERR_NO_ERROR = 0x0000 # No error +DMLERR_ADVACKTIMEOUT = 0x4000 # request for synchronous advise transaction timed out +DMLERR_DATAACKTIMEOUT = 0x4002 # request for synchronous data transaction timed out +DMLERR_DLL_NOT_INITIALIZED = 0x4003 # DDEML functions called without iniatializing +DMLERR_EXECACKTIMEOUT = 0x4006 # request for synchronous execute transaction timed out +DMLERR_NO_CONV_ESTABLISHED = 0x400a # client's attempt to establish a conversation has failed (can happen during DdeConnect) +DMLERR_POKEACKTIMEOUT = 0x400b # A request for a synchronous poke transaction has timed out. +DMLERR_POSTMSG_FAILED = 0x400c # An internal call to the PostMessage function has failed. +DMLERR_SERVER_DIED = 0x400e + +# Predefined Clipboard Formats +CF_TEXT = 1 +CF_BITMAP = 2 +CF_METAFILEPICT = 3 +CF_SYLK = 4 +CF_DIF = 5 +CF_TIFF = 6 +CF_OEMTEXT = 7 +CF_DIB = 8 +CF_PALETTE = 9 +CF_PENDATA = 10 +CF_RIFF = 11 +CF_WAVE = 12 +CF_UNICODETEXT = 13 +CF_ENHMETAFILE = 14 +CF_HDROP = 15 +CF_LOCALE = 16 +CF_DIBV5 = 17 +CF_MAX = 18 + +# DDE constants for wStatus field +DDE_FACK = 0x8000 +DDE_FBUSY = 0x4000 +DDE_FDEFERUPD = 0x4000 +DDE_FACKREQ = 0x8000 +DDE_FRELEASE = 0x2000 +DDE_FREQUESTED = 0x1000 +DDE_FAPPSTATUS = 0x00FF +DDE_FNOTPROCESSED = 0x0000 + +DDE_FACKRESERVED = (~(DDE_FACK | DDE_FBUSY | DDE_FAPPSTATUS)) +DDE_FADVRESERVED = (~(DDE_FACKREQ | DDE_FDEFERUPD)) +DDE_FDATRESERVED = (~(DDE_FACKREQ | DDE_FRELEASE | DDE_FREQUESTED)) +DDE_FPOKRESERVED = (~(DDE_FRELEASE)) + +# DDEML Transaction class flags +XTYPF_NOBLOCK = 0x0002 +XTYPF_NODATA = 0x0004 +XTYPF_ACKREQ = 0x0008 + +XCLASS_MASK = 0xFC00 +XCLASS_BOOL = 0x1000 +XCLASS_DATA = 0x2000 +XCLASS_FLAGS = 0x4000 +XCLASS_NOTIFICATION = 0x8000 + +XTYP_ERROR = (0x0000 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) +XTYP_ADVDATA = (0x0010 | XCLASS_FLAGS) +XTYP_ADVREQ = (0x0020 | XCLASS_DATA | XTYPF_NOBLOCK) +XTYP_ADVSTART = (0x0030 | XCLASS_BOOL) +XTYP_ADVSTOP = (0x0040 | XCLASS_NOTIFICATION) +XTYP_EXECUTE = (0x0050 | XCLASS_FLAGS) +XTYP_CONNECT = (0x0060 | XCLASS_BOOL | XTYPF_NOBLOCK) +XTYP_CONNECT_CONFIRM = (0x0070 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) +XTYP_XACT_COMPLETE = (0x0080 | XCLASS_NOTIFICATION ) +XTYP_POKE = (0x0090 | XCLASS_FLAGS) +XTYP_REGISTER = (0x00A0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) +XTYP_REQUEST = (0x00B0 | XCLASS_DATA ) +XTYP_DISCONNECT = (0x00C0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) +XTYP_UNREGISTER = (0x00D0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK ) +XTYP_WILDCONNECT = (0x00E0 | XCLASS_DATA | XTYPF_NOBLOCK) +XTYP_MONITOR = (0x00F0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK) + +XTYP_MASK = 0x00F0 +XTYP_SHIFT = 4 + +# DDE Timeout constants +TIMEOUT_ASYNC = 0xFFFFFFFF + +# DDE Application command flags / Initialization flag (afCmd) +APPCMD_CLIENTONLY = 0x00000010 + +# Code page for rendering string. +CP_WINANSI = 1004 # default codepage for windows & old DDE convs. +CP_WINUNICODE = 1200 + +# Declaration +DDECALLBACK = WINFUNCTYPE(HDDEDATA, UINT, UINT, HCONV, HSZ, HSZ, HDDEDATA, ULONG_PTR, ULONG_PTR) + +# PyZDDE specific globals +number_of_apps_communicating = 0 # to keep an account of the number of zemax + # server objects --'ZEMAX', 'ZEMAX1' etc + +class CreateServer(object): + """This is really just an interface class so that PyZDDE can use either the + current dde code or the pywin32 transparently. This object is created only + once. The class name cannot be anything else if compatibility has to be maintained + between pywin32 and this dde code. + """ + def __init__(self): + self.serverName = 'None' + + def Create(self, client): + """Set a DDE client that will communicate with the DDE server + + Parameters + ---------- + client : string + Name of the DDE client, most likely this will be 'ZCLIENT' + """ + self.clientName = client # shall be used in `CreateConversation` + + def Shutdown(self, createConvObj): + """The shutdown should ideally be requested only once per CreateConversation + object by the PyZDDE module, but for ALL CreateConversation objects, if there + are more than one. If multiple CreateConversation objects were created and + then not cleared, there will be memory leak, and eventually the program will + error out when run multiple times + + Parameters + ---------- + createConvObj : CreateConversation object + + Exceptions + ---------- + An exception occurs if a Shutdown is attempted with a CreateConvObj that + doesn't have a conversation object (connection with ZEMAX established) + """ + global number_of_apps_communicating + #print("Shutdown requested by {}".format(repr(createConvObj))) # for debugging + if number_of_apps_communicating > 0: + #print("Deleting object ...") # for debugging + createConvObj.ddec.__del__() + number_of_apps_communicating -=1 + + +class CreateConversation(object): + """This is really just an interface class so that PyZDDE can use either the + current dde code or the pywin32 transparently. + + Multiple objects of this type may be instantiated depending upon the + number of simultaneous channels of communication with Zemax that the user + program wants to establish using `ln = pyz.PyZDDE()` followed by `ln.zDDEInit()` + calls. + """ + def __init__(self, ddeServer): + """ + Parameters + ---------- + ddeServer : + d + """ + self.ddeClientName = ddeServer.clientName + self.ddeServerName = 'None' + self.ddetimeout = 50 # default dde timeout = 50 seconds + + def ConnectTo(self, appName, data=None): + """Exceptional error is handled in zdde Init() method, so the exception + must be re-raised""" + global number_of_apps_communicating + + if number_of_apps_communicating >= 2: + raise DDEError('Too many open communications') + + self.ddeServerName = appName + try: + self.ddec = DDEClient(self.ddeServerName, self.ddeClientName) # establish conversation + except DDEError: + raise + else: + number_of_apps_communicating +=1 + #print("Number of apps communicating: ", number_of_apps_communicating) # for debugging + + def Request(self, item, timeout=None): + """Request DDE client + timeout in seconds + Note ... handle the exception within this function. + """ + if not timeout: + timeout = self.ddetimeout + try: + reply = self.ddec.request(item, int(timeout*1000)) # convert timeout into milliseconds + except DDEError: + err_str = str(sys.exc_info()[1]) + error = err_str[err_str.find('err=')+4:err_str.find('err=')+10] + if error == hex(DMLERR_DATAACKTIMEOUT): + print("TIMEOUT REACHED. Please use a higher timeout.\n") + if (sys.version_info > (3, 0)): #this is only evaluated in case of an error + reply = b'-998' #Timeout error value + else: + reply = '-998' #Timeout error value + return reply + + def RequestArrayTrace(self, ddeRayData, timeout=None): + """Request bulk ray tracing + + Parameters + ---------- + ddeRayData : the ray data for array trace + """ + pass + # TO DO!!! + # 1. Assign proper timeout as in Request() function + # 2. Create the rayData structure conforming to ctypes structure + # 3. Process the reply and return ray trace data + # 4. Handle errors + #reply = self.ddec.poke("RayArrayData", rayData, timeout) + + def SetDDETimeout(self, timeout): + """Set DDE timeout + timeout : timeout in seconds + """ + self.ddetimeout = timeout + + def GetDDETimeout(self): + """Returns the current timeout value in seconds + """ + return self.ddetimeout + + +def get_winfunc(libname, funcname, restype=None, argtypes=(), _libcache={}): + """Retrieve a function from a library/DLL, and set the data types.""" + if libname not in _libcache: + _libcache[libname] = windll.LoadLibrary(libname) + func = getattr(_libcache[libname], funcname) + func.argtypes = argtypes + func.restype = restype + return func + +class DDE(object): + """Object containing all the DDEML functions""" + AccessData = get_winfunc("user32", "DdeAccessData", LPBYTE, (HDDEDATA, LPDWORD)) + ClientTransaction = get_winfunc("user32", "DdeClientTransaction", HDDEDATA, (LPBYTE, DWORD, HCONV, HSZ, UINT, UINT, DWORD, LPDWORD)) + Connect = get_winfunc("user32", "DdeConnect", HCONV, (DWORD, HSZ, HSZ, PCONVCONTEXT)) + CreateDataHandle = get_winfunc("user32", "DdeCreateDataHandle", HDDEDATA, (DWORD, LPBYTE, DWORD, DWORD, HSZ, UINT, UINT)) + CreateStringHandle = get_winfunc("user32", "DdeCreateStringHandleW", HSZ, (DWORD, LPCWSTR, UINT)) # Unicode version + #CreateStringHandle = get_winfunc("user32", "DdeCreateStringHandleA", HSZ, (DWORD, LPCSTR, UINT)) # ANSI version + Disconnect = get_winfunc("user32", "DdeDisconnect", BOOL, (HCONV,)) + GetLastError = get_winfunc("user32", "DdeGetLastError", UINT, (DWORD,)) + Initialize = get_winfunc("user32", "DdeInitializeW", UINT, (LPDWORD, DDECALLBACK, DWORD, DWORD)) # Unicode version of DDE initialize + #Initialize = get_winfunc("user32", "DdeInitializeA", UINT, (LPDWORD, DDECALLBACK, DWORD, DWORD)) # ANSI version of DDE initialize + FreeDataHandle = get_winfunc("user32", "DdeFreeDataHandle", BOOL, (HDDEDATA,)) + FreeStringHandle = get_winfunc("user32", "DdeFreeStringHandle", BOOL, (DWORD, HSZ)) + QueryString = get_winfunc("user32", "DdeQueryStringA", DWORD, (DWORD, HSZ, LPSTR, DWORD, c_int)) # ANSI version of QueryString + UnaccessData = get_winfunc("user32", "DdeUnaccessData", BOOL, (HDDEDATA,)) + Uninitialize = get_winfunc("user32", "DdeUninitialize", BOOL, (DWORD,)) + +class DDEError(RuntimeError): + """Exception raise when a DDE error occures.""" + def __init__(self, msg, idInst=None): + if idInst is None: + RuntimeError.__init__(self, msg) + else: + RuntimeError.__init__(self, "%s (err=%s)" % (msg, hex(DDE.GetLastError(idInst)))) + +class DDEClient(object): + """The DDEClient class. + + Use this class to create and manage a connection to a service/topic. To get + classbacks subclass DDEClient and overwrite callback.""" + + def __init__(self, service, topic): + """Create a connection to a service/topic.""" + self._idInst = DWORD(0) # application instance identifier. + self._hConv = HCONV() + + self._callback = DDECALLBACK(self._callback) + # Initialize and register application with DDEML + res = DDE.Initialize(byref(self._idInst), self._callback, APPCMD_CLIENTONLY, 0) + if res != DMLERR_NO_ERROR: + raise DDEError("Unable to register with DDEML (err=%s)" % hex(res)) + + hszServName = DDE.CreateStringHandle(self._idInst, service, CP_WINUNICODE) + + if hszServName is None: + raise DDEError("Unable to get proper String Handle for Server") + + hszTopic = DDE.CreateStringHandle(self._idInst, topic, CP_WINUNICODE) + # Try to establish conversation with the Zemax server + self._hConv = DDE.Connect(self._idInst, hszServName, hszTopic, PCONVCONTEXT()) + DDE.FreeStringHandle(self._idInst, hszTopic) + DDE.FreeStringHandle(self._idInst, hszServName) + if not self._hConv: + raise DDEError("Unable to establish a conversation with server", self._idInst) + + def __del__(self): + """Cleanup any active connections and free all DDEML resources.""" + if self._hConv: + DDE.Disconnect(self._hConv) + if self._idInst: + DDE.Uninitialize(self._idInst) + + def advise(self, item, stop=False): + """Request updates when DDE data changes.""" + hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) + hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_ADVSTOP if stop else XTYP_ADVSTART, TIMEOUT_ASYNC, LPDWORD()) + DDE.FreeStringHandle(self._idInst, hszItem) + if not hDdeData: + raise DDEError("Unable to %s advise" % ("stop" if stop else "start"), self._idInst) + DDE.FreeDataHandle(hDdeData) + + def execute(self, command): + """Execute a DDE command.""" + pData = c_char_p(command) + cbData = DWORD(len(command) + 1) + hDdeData = DDE.ClientTransaction(pData, cbData, self._hConv, HSZ(), CF_TEXT, XTYP_EXECUTE, TIMEOUT_ASYNC, LPDWORD()) + if not hDdeData: + raise DDEError("Unable to send command", self._idInst) + DDE.FreeDataHandle(hDdeData) + + def request(self, item, timeout=5000): + """Request data from DDE service.""" + hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) + #hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_REQUEST, timeout, LPDWORD()) + pdwResult = DWORD(0) + hDdeData = DDE.ClientTransaction(LPBYTE(), 0, self._hConv, hszItem, CF_TEXT, XTYP_REQUEST, timeout, byref(pdwResult)) + DDE.FreeStringHandle(self._idInst, hszItem) + if not hDdeData: + raise DDEError("Unable to request item", self._idInst) + + if timeout != TIMEOUT_ASYNC: + pdwSize = DWORD(0) + pData = DDE.AccessData(hDdeData, byref(pdwSize)) + if not pData: + DDE.FreeDataHandle(hDdeData) + raise DDEError("Unable to access data in request function", self._idInst) + DDE.UnaccessData(hDdeData) + else: + pData = None + DDE.FreeDataHandle(hDdeData) + return pData + + def poke(self, item, data, timeout=5000): + """Poke (unsolicited) data to DDE server""" + hszItem = DDE.CreateStringHandle(self._idInst, item, CP_WINUNICODE) + pData = c_char_p(data) + cbData = DWORD(len(data) + 1) + pdwResult = DWORD(0) + #hData = DDE.CreateDataHandle(self._idInst, data, cbData, 0, hszItem, CP_WINUNICODE, 0) + #hDdeData = DDE.ClientTransaction(hData, -1, self._hConv, hszItem, CF_TEXT, XTYP_POKE, timeout, LPDWORD()) + hDdeData = DDE.ClientTransaction(pData, cbData, self._hConv, hszItem, CF_TEXT, XTYP_POKE, timeout, byref(pdwResult)) + DDE.FreeStringHandle(self._idInst, hszItem) + #DDE.FreeDataHandle(dData) + if not hDdeData: + print("Value of pdwResult: ", pdwResult) + raise DDEError("Unable to poke to server", self._idInst) + + if timeout != TIMEOUT_ASYNC: + pdwSize = DWORD(0) + pData = DDE.AccessData(hDdeData, byref(pdwSize)) + if not pData: + DDE.FreeDataHandle(hDdeData) + raise DDEError("Unable to access data in poke function", self._idInst) + # TODO: use pdwSize + DDE.UnaccessData(hDdeData) + else: + pData = None + DDE.FreeDataHandle(hDdeData) + return pData + + def callback(self, value, item=None): + """Callback function for advice.""" + print("callback: %s: %s" % (item, value)) + + def _callback(self, wType, uFmt, hConv, hsz1, hsz2, hDdeData, dwData1, dwData2): + """DdeCallback callback function for processing Dynamic Data Exchange (DDE) + transactions sent by DDEML in response to DDE events + + Parameters + ---------- + wType : transaction type (UINT) + uFmt : clipboard data format (UINT) + hConv : handle to conversation (HCONV) + hsz1 : handle to string (HSZ) + hsz2 : handle to string (HSZ) + hDDedata : handle to global memory object (HDDEDATA) + dwData1 : transaction-specific data (DWORD) + dwData2 : transaction-specific data (DWORD) + + Returns + ------- + ret : specific to the type of transaction (HDDEDATA) + """ + if wType == XTYP_ADVDATA: # value of the data item has changed [hsz1 = topic; hsz2 = item; hDdeData = data] + dwSize = DWORD(0) + pData = DDE.AccessData(hDdeData, byref(dwSize)) + if pData: + item = create_string_buffer('\000' * 128) + DDE.QueryString(self._idInst, hsz2, item, 128, CP_WINANSI) + self.callback(pData, item.value) + DDE.UnaccessData(hDdeData) + return DDE_FACK + else: + print("Error: AccessData returned NULL! (err = %s)"% (hex(DDE.GetLastError(self._idInst)))) + if wType == XTYP_DISCONNECT: + print("Disconnect notification received from server") + + return 0 + +def WinMSGLoop(): + """Run the main windows message loop.""" + LPMSG = POINTER(MSG) + LRESULT = c_ulong + GetMessage = get_winfunc("user32", "GetMessageW", BOOL, (LPMSG, HWND, UINT, UINT)) + TranslateMessage = get_winfunc("user32", "TranslateMessage", BOOL, (LPMSG,)) + # restype = LRESULT + DispatchMessage = get_winfunc("user32", "DispatchMessageW", LRESULT, (LPMSG,)) + + msg = MSG() + lpmsg = byref(msg) + while GetMessage(lpmsg, HWND(), 0, 0) > 0: + TranslateMessage(lpmsg) + DispatchMessage(lpmsg) + +if __name__ == "__main__": + pass diff --git a/ramancom/thermoFTIR/omnic_test.py b/ramancom/thermoFTIR/omnic_test.py new file mode 100644 index 0000000000000000000000000000000000000000..19871fbbe04a601fefdd0cc7406e0fe8d68c21ca --- /dev/null +++ b/ramancom/thermoFTIR/omnic_test.py @@ -0,0 +1,34 @@ +from OmnicDDE import * + +omnicCom = OmnicDDE() +# omnicCom.new_window('krasser test') +# omnicCom.collect_backround() +# omnicCom.collect_sample() +# omnicCom.clear_window() +# omnicCom.search_library() # currently fails -> no library selected?? +# omnicCom.SetNumScans(2) +# omnicCom.conversation.Poke('Display RegionEnd', str(1800)) +# omnicCom.conversation.Poke('Display RegionStart', str(2500)) +# omnicCom.conversation.Exec('[Export """C:\my documents\omnic\test.spa"""]') + +numIter: int = 1000 +coords: list = [] +for i in range(numIter): + print(f'iteration {i + 1} of {numIter}') + try: + x, y, z = omnicCom.get_stage_coords() + coords.append((x, y, z)) + + print('Current Stage Coordinates:', x, y, z) + if i % 2 == 0: + omnicCom.set_stage_coords(x + 100, y + 100, z + 10) + else: + omnicCom.set_stage_coords(x - 100, y - 100, z - 10) + + # x, y, z = omnicCom.get_stage_coords() + # print('Stage Coordinate after moving:', x, y, z) + except: + omnicCom.close_connection() + raise + +omnicCom.close_connection() diff --git a/ramancom/thermoFTIR/processScreenshot.py b/ramancom/thermoFTIR/processScreenshot.py new file mode 100644 index 0000000000000000000000000000000000000000..c9d73cd53f496d622dcdf1b246b5a91faf56c81e --- /dev/null +++ b/ramancom/thermoFTIR/processScreenshot.py @@ -0,0 +1,216 @@ +import cv2 +import numpy as np +import time +import os +import json +from PIL import ImageGrab +from PyQt5 import QtWidgets, QtGui + + +def acquire_screenshot(bbox: tuple = None) -> np.ndarray: + """ + Captures a screenshot of the current screen and returns it a numpy array + :param bbox: Tuple of left, upper, right, and lower pixel coordinates + :return: + """ + # img: np.ndarray = cv2.imread('PictaScreenshot.tif') + # img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + # if bbox is not None: + # _xStart, _yStart, _xEnd, _yEnd = bbox + # img = img[_yStart:_yEnd, _xStart:_xEnd, :] + img: np.ndarray = ImageGrab.grab(bbox) + return np.array(img) # could also be a memoryview... + + +def get_img_bbox(binimg: np.ndarray, ticklength: int = 3) -> tuple: + """ + ASSUMES: lines in binImg are 1, others is 0 + ASSUMES: mic image is at least 25 % of height and 25 % of width + ASSUMES: No other image is as large as that! + :param binimg: + :param ticklength: lenght of the ticks in pixels + :return: topX, topY, Width, Height + """ + _x, _y, _width, _height = -1, -1, -1, -1 + _contours, _hierarchy = cv2.findContours(binimg, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + for cnt in _contours: + _x, _y, _width, _height = cv2.boundingRect(cnt) + if _width >= binimg.shape[1]/4 and _height >= binimg.shape[1]/4: + break + assert _x != -1 and _y != -1 and _width != -1 and _height != -1 + _startX: int = _x + ticklength + 1 + _startY: int = _y + _endX: int = _startX + _width - ticklength - 1 + _endY: int = _startY + _height - ticklength + return _startX, _startY, _endX, _endY + + +def get_pixelscale(binImg: np.ndarray, startY: int, startX: int, tickareawidth: int, tickDistMicrons: float = 50.0) -> float: + """ + Extracts the ticks from below the image and calculates pixel scale + ASSUMES: TICKDISTANCE in Microns is 50.0 -> Can be different when image is scaled very large or very small + ASSUMES: ticks are one pix wide + ASSUMES: pixelscale_x == pixelscale_y + :param binImg: binary image, 0 background, 1 foreground + :param startY: y coord where the area of the image with the axis ticks starts + :param startX: x coord where the area of the image with the axis ticks starts + :param tickareawidth: width of the area of the image with the axis ticks + :param tickDistMicrons: distance in microns between every two ticks + :return: scale in microns/px + """ + _tickArea: np.ndarray = binImg[startY:startY + 1, startX:startX + tickareawidth] + _tickPositions: np.ndarray = np.where(_tickArea == 1)[1] + _numTicks: int = _tickPositions.shape[0] + _diffs: list = [_tickPositions[i] - _tickPositions[i - 1] for i in reversed(np.arange(1, _numTicks))] + _tickDistPx: float = float(np.mean(_diffs)) + return tickDistMicrons / _tickDistPx + + +class ImageInterface(object): # it is inherited from QWidget as user input is required at some points + def __init__(self, logger): + self.logger = logger + self.pixelscale: float = None + self.configDir: str = '' + self.boundingBox: tuple = None # x_start, y_start, x_end, y_end + self.pixelscale: float = None + self.correctConfig: bool = False + self.setupWin = None + + @property + def configPath(self) -> str: + return os.path.join(self.configDir, 'imgInterface.json') + + def save_config(self) -> None: + print('saving config') + assert os.path.exists(self.configDir) + config: dict = {'bbox': self.boundingBox, + 'pixelscale': self.pixelscale} + with open(self.configPath, 'w') as fp: + json.dump(config, fp) + + def _load_config(self) -> None: + with open(self.configPath) as fp: + config: dict = json.load(fp) + self.boundingBox = config['bbox'] + self.pixelscale = config['pixelscale'] + self.correctConfig = True + + def update_config_from_directory(self, fpath: str) -> None: + self.configDir = fpath + if not os.path.exists(self.configPath): + self.logger.info(f'Recreating Image interface configuration, because non was found at {fpath}') + self._setup_config() + else: + timeMod: float = os.path.getmtime(self.configPath) + hoursSinceMod: float = (time.time() - timeMod) / 3600 + maxDeltaT: float = 12 # i.e. 12 hours + if hoursSinceMod > maxDeltaT: + self.logger.info(f'Config found at {fpath} is older than 12 hours, recreating...') + self._setup_config() + else: + self.logger.info(f'Config found at {fpath} is only {round(hoursSinceMod, 2)} hours old and is used.') + self._load_config() + + def _setup_config(self) -> None: + self.setupWin = PromptConfigInterface(self) + self.setupWin.initialize_interface() + + def get_videoImage(self) -> np.ndarray: + """ + Returns the current video Image from OmnicPicta. + :param bbox: Pixel coordinates of the video image + :param settingUpConfig: is the image used for setting up the config? + :return: + """ + assert self.correctConfig + screenshot: np.ndarray = acquire_screenshot(self.boundingBox) + return screenshot + + +class PromptConfigInterface(QtWidgets.QWidget): + def __init__(self, imgInterface: ImageInterface): + super(PromptConfigInterface, self).__init__() + self.setWindowTitle('OmnicPicta Image Capture') + + layout: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + self.boundingBox: tuple = None + self.pixelscale: float = None + self.imgInterface: ImageInterface = imgInterface + + self.imglabel: QtWidgets.QLabel = QtWidgets.QLabel() + self.pxScaleLabel: QtWidgets.QLabel = QtWidgets.QLabel() + + self.acceptBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Accept') + self.acceptBtn.clicked.connect(self._accept) + self.retryBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Retry') + self.retryBtn.clicked.connect(self.initialize_interface) + self.abortBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Abort') + self.abortBtn.clicked.connect(self._abort) + btnLayout = QtWidgets.QHBoxLayout() + btnLayout.addWidget(self.acceptBtn) + btnLayout.addWidget(self.retryBtn) + btnLayout.addWidget(self.abortBtn) + + layout.addWidget(QtWidgets.QLabel('Please verify the detected video image!')) + layout.addWidget(self.imglabel) + layout.addWidget(self.pxScaleLabel) + layout.addLayout(btnLayout) + + def initialize_interface(self) -> None: + msg: str = "The video interface has to be initialized, which requires the OmnicPicta interface to be opened." + msg += "\n\nPlease ensure that the live video image in OmnicPicta is scaled to at least 25 % of" \ + "screen size in both x and y.\n\n" \ + "Make sure that the tick-distance at both axis is 50 µm by adjusting the video image size.\n\n" \ + "Make sure that the virtual joystick does not occlude the video image.\n\n" \ + "After clicking OK, please bring OmnicPicta to front. After two seconds, the video interface" \ + "will run its initialization protocol. " + + QtWidgets.QMessageBox.information(self, "Video interface setup", msg) + time.sleep(2.0) + img: np.ndarray = acquire_screenshot() + blackwhite: np.ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + blackwhite[blackwhite > 0] = 1 + blackwhite = 1 - blackwhite + x_start, y_start, x_end, y_end = get_img_bbox(blackwhite) + self.pixelscale = get_pixelscale(blackwhite, y_end, x_start, x_end-x_start) + self.boundingBox = x_start, y_start, x_end, y_end + padding: int = 20 + previewBbox: tuple = (x_start-padding, y_start-padding, x_end+padding, y_end+padding) + verifyImg: np.ndarray = acquire_screenshot(previewBbox) + + mask: np.ndarray = np.ones(verifyImg.shape[:2], dtype=np.float) / 3.0 + mask[padding:mask.shape[0]-padding, padding:mask.shape[1]-padding] *= 3.0 + verifyImg = np.array(verifyImg, dtype=np.float) + for i in range(3): + verifyImg[:, :, i] *= mask + self._set_imagelabel(np.uint8(verifyImg)) + self._set_pixelscaleLabel() + self.show() + + def _set_imagelabel(self, image: np.ndarray) -> None: + height, width, channel = image.shape + bytesPerLine: int = 3 * width + + pix: QtGui.QPixmap = QtGui.QPixmap() + qimg: QtGui.QImage = QtGui.QImage(image.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888) + pix.convertFromImage(qimg) + self.imglabel.setPixmap(pix) + + def _set_pixelscaleLabel(self) -> None: + self.pxScaleLabel.setText(f'\nPixelScale: {round(self.pixelscale, 2)} µm/px\n') + + def _accept(self) -> None: + self.imgInterface.boundingBox = self.boundingBox + self.imgInterface.pixelscale = self.pixelscale + self.imgInterface.correctConfig = True + self.imgInterface.save_config() + self.close() + + def _abort(self) -> None: + self.imgInterface.correctConfig = False + QtWidgets.QMessageBox.critical(self, 'Error', 'The image interface could not be initialized.\n' + 'Please adjust OmnicPicta and retry. If the error persists,\n' + 'please ask a programmer.') + raise AssertionError diff --git a/ramancom/thermoFTIRCom.py b/ramancom/thermoFTIRCom.py new file mode 100644 index 0000000000000000000000000000000000000000..d76af60cdea4cf97bfcef83343824a1f4cf977d3 --- /dev/null +++ b/ramancom/thermoFTIRCom.py @@ -0,0 +1,302 @@ +# -*- 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 +import win32ui +import cv2 +import dde +import time +import numpy as np + + +try: + from .ramanbase import RamanBase + from .thermoFTIR.ddeclient import DDEClient + from .thermoFTIR.processScreenshot import ImageInterface + # from .configRaman import RamanSettingParam +except ImportError: + from ramanbase import RamanBase + from thermoFTIR.ddeclient import DDEClient + from thermoFTIR.processScreenshot import ImageInterface + # from configRaman import RamanSettingParam + + +class OmnicDDE: + Application = "Omnic" + Thread = "Spectra" + + def __init__(self, logger): + self.server = None + self.conversation = None + self.ddeClient = None + self.logger = logger + + def connect(self): + self.server = dde.CreateServer() + self.server.Create("MyOmnic") + self.conversation = dde.CreateConversation(self.server) + self.conversation.ConnectTo(self.Application, self.Thread) + self.ddeClient: DDEClient = DDEClient('OMNIC', 'Spectra') + self.logger.info("Connection With Omnic Established") + + def request(self, item: str) -> str: + return self.ddeClient.request(item).decode() + + def execute(self, cmd: str) -> None: + self.conversation.Exec(cmd) + + def poke(self, parameter: str, value) -> None: + # TODO: IMPLEMENT! + pass + + def new_window(self, windowName: str = "") -> None: + if windowName == "": + cmd = '[NewWindow]' + else: + cmd = f'[NewWindow """{windowName}"""]' + self.execute(cmd) + currentWindow = self.request('Window Title') + self.logger.info(f"Window Created with Name: {currentWindow}") + + def clear_window(self): + curWindow = self.request('Window Title') + self.execute("[Select All]") + self.execute("[DeleteSelectedSpectra]") + self.logger.debug(f"Window {curWindow} Cleared") + + def collect_backround(self, title: str = ""): + cmd = '[Invoke CollectBackground """' + title + '""" Auto Polling]' + # cmd = '[CollectBackground """' + title + '"""]' + self.execute(cmd) + self.logger.info(f"Collecting Background {title} ...") + + # curWindow = self.request('Window Title') + # self.execute('[MaximizeWindow """' + curWindow + '"""]') + menustatus: str = self.request('MenuStatus CollectBackground') + while menustatus == "Disabled": + time.sleep(1) + menustatus = self.request('MenuStatus CollectBackground') + self.logger.debug("Background Collection Finished") + + # If the user does not specify the title, he probably doesn't want to save it + if title == "": + time.sleep(0.5) + self.execute("[DeleteSelectedSpectra]") + + # def set_as_background(self, backgroundName: str) -> None: + # self.execute(f'[Select """{backgroundName}"""]') + # self.execute("[SetAsBackground]") + # self.logger.debug(f"Set {backgroundName} as background") + + def collect_sample(self, title: str = ""): + cmd = f'[Invoke CollectSample """{title}""" Auto Polling]' + self.execute(cmd) + self.logger.info(f"Collecting Sample Spectrum {title} ...") + # currentWindow = self.request('Window Title') + # self.execute(f'[MaximizeWindow """{currentWindow}"""]') + menustate: str = self.request('MenuStatus CollectSample') + while menustate == "Disabled": + time.sleep(1) + menustate = self.request('MenuStatus CollectSample') + + # print(time.asctime() + "\t" + "Sample Spectrum Collection Finished") + # print(time.asctime() + "\t" + "Sample Spectrum Title: " + self.request('Spectrum Title')) + + def save_spec_to_file(self, specName: str, fpath: str) -> None: + self.logger.debug(f'saving {specName} to {fpath}') + self.execute(f'[Select """{specName}"""]') + self.execute(rf'[ExportAs """{fpath}"""]') + + def search_library(self): + self.execute("[Search]") + self.execute('[SelectHit 1]') + ResultMatch = self.request('SearchHit MatchValue') + ResultIDX = self.request('SearchHit Index') + ResultLib = self.request('SearchHit FileName') + cmd = '[GetLibSpectrumTitle ' + ResultIDX + ' """' + ResultLib + '"""]' + self.execute(cmd) + ResultTitle = self.request("Result Current") + print(time.asctime() + "\t" + "Search Match: " + ResultMatch + "\t" + ResultTitle) + + def set_num_scans(self, numScans: int): + self.conversation.Poke('Collect NumScans', str(numScans)) + + def get_stage_coords(self) -> tuple: + self.execute('[StageGetXY]') + strXY: list = self.request("Result Current").split() + x: float = float(strXY[0][2:].replace(',', '.')) + y: float = float(strXY[1][2:].replace(',', '.')) + self.execute('[StageGetZ]') + strZ: str = self.request("Result Current") + z: float = float(strZ[2:].replace(',', '.')) + return x, y, z + + def get_stage_z(self) -> float: + self.execute('[StageGetZ]') + strZ: str = self.request("Result Current") + z: float = float(strZ[2:].replace(',', '.')) + return z + + def set_stage_xy(self, x: float, y: float): + strCoords: list = [str(val).replace('.', ',') for val in [x, y]] + self.execute(f'[StageSetXY {strCoords[0]} {strCoords[1]}]') + + def set_stage_z(self, z: float): + zstr: str = str(z).replace('.', ',') + self.execute(f'[StageSetZ {zstr}]') + + def set_aperture(self, width: float, height: float, angle: float) -> None: + width = np.clip(width, 5, 400) + height = np.clip(height, 5, 400) + self.logger.info(f'setting aperture to {np.round(width)} {np.round(height)} {np.round(angle)}') + self.execute(f'[ApertureSetXYR {np.round(width)} {np.round(height)} {np.round(angle)}]') + + def close_connection(self): + self.server.Destroy() + self.ddeClient.__del__() + self.logger.info("DDE Connection with Omnic was Terminated") + + +class ThermoFTIRCom(RamanBase): + magn = 20 + + ramanParameters = [] + + def __init__(self, logger): + super().__init__() + self.name = 'ThermoFTIRCom' + self.logger = logger + self.dde: OmnicDDE = OmnicDDE(logger) + self.connected: bool = False + self.pixelscale = None + self.imageInterface: ImageInterface = ImageInterface(logger) + self.initialZ: float = 0.0 + + def connect(self): + try: + self.dde.connect() + self.connected = True + except dde.error: + self.connected = False + self.logger.error('Connection to Thermo FTIR / Omnic did not work!') + + self.initialZ = self.getUserZ() + return self.connected + + def setupToDatasetPath(self, dsetPath: str) -> None: + self.imageInterface.update_config_from_directory(dsetPath) + + def getRamanPositionShift(self): + return 0, 0 + + def disconnect(self): + if self.connected: + self.dde.close_connection() + self.connected = False + + def getPosition(self): + assert self.connected + return self.dde.get_stage_coords() + + def moveToAbsolutePosition(self, x: float, y: float, z: float = None, epsxy: float = 0.5, epsz: float = 1): + assert self.connected + self.dde.set_stage_xy(x, y) + if z is not None: + self.moveZto(z) + + x_set, y_set, z_set = self.dde.get_stage_coords() + assert abs(x_set - x) <= epsxy + assert abs(y_set - y) <= epsxy + if z is not None: + diffz: float = abs(z_set - z) + assert diffz <= epsz, f'diff z is {diffz}, max allowed: {epsz}' + + def moveZto(self, z, epsz=1): + assert self.connected + self.dde.set_stage_z(z) + x_set, y_set, z_set = self.dde.get_stage_coords() + diffz: float = abs(z_set - z) + assert diffz <= epsz, f'diff z is {diffz}, max allowed: {epsz}' + + def setAperture(self, width, height, angle) -> None: + self.dde.set_aperture(width, height, angle) + + def saveImage(self, fname): + """ + Save current camera image to file name + :return: + """ + assert self.connected + assert self.imageInterface.correctConfig + img = self.imageInterface.get_videoImage() + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + cv2.imwrite(fname, img) + + def getImageDimensions(self, mode='df'): + """ + Get the image width and height in um and the orientation angle in degrees. + """ + assert self.connected + assert self.imageInterface.correctConfig + x_start, y_start, x_end, y_end = self.imageInterface.boundingBox + width: float = (x_end - x_start) * self.imageInterface.pixelscale + height: float = (y_end - y_start) * self.imageInterface.pixelscale + angle: float = 0.0 + return width, height, angle + + def getSoftwareZ(self): + return self.initialZ + + def getUserZ(self): + return self.dde.get_stage_z() + + def initiateMeasurement(self, ramanSettings): + self.logger.info('measurement start') + + def triggerMeasurement(self, num): + self.logger.info(f'measuring point {num}') + + def finishMeasurement(self, aborted=False): + self.logger.info(f'Measurement finished, aborted: {aborted}') + + +if __name__ == '__main__': + import logging + import os + logger: logging.Logger = logging.Logger('TestLogger') + logger.addHandler(logging.StreamHandler()) + omnicDDE = OmnicDDE(logger) + omnicDDE.connect() + + try: + numIterations: int = 3 + # for i in range(numIterations): + # omnicDDE.collect_backround(f"background {i}") + + for i in range(numIterations): + title: str = f"sampleSpec_{i}" + # omnicDDE.set_as_background(f"background {i}") + omnicDDE.collect_sample(title) + omnicDDE.save_spec_to_file(title, os.path.join(r"C:\Users\LabUser\Documents\Josef", title + ".jdx")) + except: + print('error') + omnicDDE.close_connection() diff --git a/ramancom/thermoFTIRtesting.py b/ramancom/thermoFTIRtesting.py new file mode 100644 index 0000000000000000000000000000000000000000..f4d918ef6e4948dc1968821bd8e58b6516d54d6c --- /dev/null +++ b/ramancom/thermoFTIRtesting.py @@ -0,0 +1,73 @@ +# -*- 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 socket import gethostname +from thermoFTIRCom import ThermoFTIRCom + +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(r"C:\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() + + diff --git a/ramanscanui.py b/ramanscanui.py index a213ae92543d303058e2aeff6ad85c1089316c45..40db1a70b5e191480afd638f0e4cdbe7a912e5f6 100644 --- a/ramanscanui.py +++ b/ramanscanui.py @@ -27,9 +27,10 @@ from time import time, localtime, strftime import os import logging import logging.handlers -from .external import tsp +from .cythonModules import tsp from .uielements import TimeEstimateProgressbar from .gepardlogging import setDefaultLoggingConfig +from .analysis.particleCharacterization import FTIRAperture def reorder(points, N=20): """ @@ -80,12 +81,18 @@ def scan(ramanSettings, positions, controlclass, dataqueue, stopevent, try: ramanctrl = controlclass(logger) ramanctrl.connect() - logger.info('connected:' + strftime("%d %b %Y %H:%M:%S", localtime())) ramanctrl.initiateMeasurement(ramanSettings) + logger.info(ramanctrl.name) for i, p in enumerate(positions): - x, y, z = p - logger.info('time:' + strftime("%d %b %Y %H:%M:%S", localtime())) - logger.info("position:" + str(x) + str(y) + str(z)) + if not ramanctrl.name == 'ThermoFTIRCom': + x, y, z = p + else: + x, y, z = p.centerX, p.centerY, p.centerZ + width, height, angle = p.width, p.height, p.angle + logger.info(f'{width}, {height}, {angle}') + ramanctrl.setAperture(width, height, angle) + + logger.info(f"position: {x}, {y}, {z}") ramanctrl.moveToAbsolutePosition(x, y, z) logger.info("move done") ramanctrl.triggerMeasurement(i) @@ -233,33 +240,66 @@ class RamanScanUI(QtWidgets.QWidget): return self.view.imparent.ramanSwitch.hide() self.view.setMicroscopeMode() - - points = np.asarray(self.dataset.particleContainer.getMeasurementPixelCoords()) + + if not self.ramanctrl.name == 'ThermoFTIRCom': + points = np.asarray(self.dataset.particleContainer.getMeasurementPixelCoords()) + else: + points = np.asarray(self.dataset.particleContainer.getApertureCenterCoords()) + + numPoints: int = len(points) ramanSettings = {'filename': self.dataset.name, - 'numPoints': len(points), + 'numPoints': numPoints, 'path': self.dataset.path} for index, param in enumerate(self.params): - try: ramanSettings[self.ramanctrl.ramanParameters[index].name] = self.ramanctrl.ramanParameters[index].value_of(param) - except: self.logger.critical('raman Parameter not found' + str(param)) + try: + ramanSettings[self.ramanctrl.ramanParameters[index].name] = self.ramanctrl.ramanParameters[index].value_of(param) + except: + self.logger.critical('raman Parameter not found' + str(param)) lmin = None - for i in range(20,41): + 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 l zmax: + zmax = newAperture.centerZ + softwarez = self.ramanctrl.getSoftwareZ() # get current software z zmin -= softwarez zmax -= softwarez reply = QtWidgets.QMessageBox.question(self, 'Starting raman scan', "Please switch to Raman laser. Microscope will move"\ - " (%4.0f,%4.0f) µm relative to current position. Proceed?"%(zmin,zmax), + " (%4.0f,%4.0f) µm relative to current position. Proceed?"%(zmin, zmax), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) @@ -280,23 +320,30 @@ class RamanScanUI(QtWidgets.QWidget): self.view.saveDataSet() self.close() return - + else: self.view.highLightRamanIndex(0) self.view.blockUI() self.paramsGroup.setEnabled(False) self.progressbar.enable() self.progressbar.resetTimerAndCounter() - self.progressbar.setMaxValue(len(scanpoints)) + self.progressbar.setMaxValue(numPoints) self.ramanctrl.disconnect() self.processstopevent = Event() self.dataqueue = Queue() logpath = os.path.join(self.dataset.path, 'ramanscanlog.txt') - self.process = Process(target=scan, args=(ramanSettings, scanpoints, - self.ramanctrl.__class__, - self.dataqueue, - self.processstopevent, - logpath)) + if not self.ramanctrl.name == 'ThermoFTIRCom': + self.process = Process(target=scan, args=(ramanSettings, scanpoints, + self.ramanctrl.__class__, + self.dataqueue, + self.processstopevent, + logpath)) + else: + self.process = Process(target=scan, args=(ramanSettings, apertures, + self.ramanctrl.__class__, + self.dataqueue, + self.processstopevent, + logpath)) self.process.start() self.starttime = time() self.timer = QtCore.QTimer(self) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a3785961293da8d264d1de7ae9bf1e3a632aa5c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +numpy~=1.18.5 +PyQt5~=5.15.0 +opencv-python~=4.2.0.34 +setuptools~=41.2.0 +scipy~=1.4.1 +matplotlib~=3.2.1 +Pillow~=7.1.2 +scikit-image~=0.17.2 +scikit-fuzzy~=0.4.2 +cython~=0.29.17 \ No newline at end of file diff --git a/sampleview.py b/sampleview.py index e440dccd327f5955e7064a8caf15830b63ed0b69..eb652ec5442c051352b7469d51021097eccabbb8 100644 --- a/sampleview.py +++ b/sampleview.py @@ -30,7 +30,7 @@ from .opticalscan import OpticalScan from .ramanscanui import RamanScanUI from .detectionview import ParticleDetectionView from .zeissimporter import ZeissImporter -from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo, SeedPoint +from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo, SeedPoint, FTIRApertureIndicator from .helperfunctions import polygoncovering from .analysis.colorlegend import getColorFromNameWithSeed from .analysis.particleEditor import ParticleEditor @@ -47,13 +47,9 @@ class SampleView(QtWidgets.QGraphicsView): super(SampleView, self).__init__() self.logger = logger - #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) @@ -90,6 +86,7 @@ class SampleView(QtWidgets.QGraphicsView): self.imgdata = None self.isblocked = False self.contourItems = [] + self.apertureItems = [] self.selectedParticleIndices = [] self.seedPoints = [] self.particlePainter = None @@ -247,6 +244,7 @@ class SampleView(QtWidgets.QGraphicsView): del widget self.dataset = loadData(fname) + self.ramanctrl.setupToDatasetPath(self.dataset.path) self.setupParticleEditor() self.setMicroscopeMode() @@ -272,6 +270,7 @@ class SampleView(QtWidgets.QGraphicsView): self.dataset.save() self.dataset = DataSet(fname, newProject=True) self.setupParticleEditor() + self.ramanctrl.setupToDatasetPath(self.dataset.path) self.setMicroscopeMode() self.pyramid.fromDataset(self.dataset) self.pyramid.setMicroscopeMode(self.microscopeMode) @@ -280,7 +279,7 @@ class SampleView(QtWidgets.QGraphicsView): self.activateMaxMode(loadnew=True) self.imparent.snapshotAct.setEnabled(True) self.setupLoggerToDataset() - + def setupLoggerToDataset(self): """ Adds a new handler to the logger to create a log also in the dataset directory @@ -434,7 +433,7 @@ class SampleView(QtWidgets.QGraphicsView): noz = (self.mode in ['OpticalScan', 'RamanScan']) x, y, z = self.dataset.mapToLengthRaman([pos.x(), pos.y()], microscopeMode=self.microscopeMode, noz=noz) if z is not None: - assert z>-100. + assert z > -100. self.ramanctrl.moveToAbsolutePosition(x, y, z) def checkForContourSelection(self, event): @@ -702,10 +701,20 @@ class SampleView(QtWidgets.QGraphicsView): t0 = time.time() for cnt in self.contourItems: self.scene().removeItem(cnt) + + for apt in self.apertureItems: + self.scene().removeItem(apt) + self.contourItems = [] + self.apertureItems = [] if self.dataset is not None: for particle in self.dataset.particleContainer.particles: self.addParticleContourToIndex(particle.contour, particle.index) + if hasattr(particle, 'aperture') and particle.aperture is not None: + newApt = FTIRApertureIndicator(particle.aperture) + self.scene().addItem(newApt) + self.apertureItems.append(newApt) + self.update() self.logger.info('resetted contours: {} ms'.format(round((time.time()-t0)*1000))) diff --git a/segmentation.py b/segmentation.py index 8e1e24dd1e4a80d660dce2bb115719568da10512..3f3ddb20bba2c6171eda10698bdc347f708d11b8 100644 --- a/segmentation.py +++ b/segmentation.py @@ -26,7 +26,8 @@ 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 skimage.morphology import watershed +from skimage.segmentation import watershed import skfuzzy as fuzz import random from PyQt5 import QtCore diff --git a/uielements.py b/uielements.py index 4de3496c632c0eb462722b0ea2c47e80f88a287e..1626078e56c5cc9f2cad6fe69a2a04b19cb97ada 100644 --- a/uielements.py +++ b/uielements.py @@ -23,7 +23,9 @@ from PyQt5 import QtWidgets from time import time import datetime -class TimeEstimateProgressbar(QtWidgets.QGroupBox): + +# class TimeEstimateProgressbar(QtWidgets.QGroupBox): +class TimeEstimateProgressbar(QtWidgets.QWidget): """ A progressbar with time estimate for computationally expensive tasks. It contains the actual progressbar together with a text label that is updated accordingly. It also estimates the remaining progress time. @@ -108,4 +110,3 @@ class TimeEstimateProgressbar(QtWidgets.QGroupBox): ttot = timerunning*self.maxVal/(self.progressbar.value()+1) time2go = ttot - timerunning self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) - \ No newline at end of file diff --git a/viewitems.py b/viewitems.py index cd455e0a7ffccc79bedab451c35526c33a56c2b3..d198112832afdde963ab91980936a9e89e7358d9 100644 --- a/viewitems.py +++ b/viewitems.py @@ -19,8 +19,11 @@ along with this program, see COPYING. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui +import numpy as np +from copy import deepcopy from .analysis.particleEditor import ParticleContextMenu -from .analysis.particleCharacterization import getParticleCenterPoint +from .analysis.particleCharacterization import getParticleCenterPoint, FTIRAperture + class SegmentationContour(QtWidgets.QGraphicsItem): def __init__(self, viewparent, contourData, pos=(0,0)): @@ -83,8 +86,6 @@ class SegmentationContour(QtWidgets.QGraphicsItem): painter.drawPolygon(self.polygon) self.viewparent.update() - else: - print('painting not present contour') def contextMenuEvent(self, event): if self.isSelected: @@ -144,11 +145,7 @@ class SeedPoint(QtWidgets.QGraphicsItem): rect = QtCore.QRectF(-self.radius,-self.radius, 2*self.radius,2*self.radius) 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)): @@ -195,7 +192,46 @@ class RamanScanIndicator(QtWidgets.QGraphicsItem): painter.setFont(font) painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) - + +class FTIRApertureIndicator(QtWidgets.QGraphicsItem): + def __init__(self, aperture: FTIRAperture): + super(FTIRApertureIndicator, self).__init__() + self.aperture = aperture + self.setZValue(2) + self.setPos(aperture.centerX, aperture.centerY) + self.polygon: QtGui.QPolygonF = QtGui.QPolygonF() + self.getBrectAndPolygon() + + def getBrectAndPolygon(self) -> None: + """ + Calculates the bounding rect (needed for drawing the QGraphicsView) and converts the aperture to a polygon. + :return: + """ + meanX: float = self.aperture.width/2 + meanY: float = self.aperture.height/2 + points: np.ndarray = np.array([[-meanX, meanY], + [meanX, meanY], + [meanX, -meanY], + [-meanX, -meanY]]) + + angleRad: np.ndarray = np.deg2rad(self.aperture.angle) + sin, cos = np.sin(angleRad), np.cos(angleRad) + for i in range(points.shape[0]): + point: np.ndarray = deepcopy(points[i, :]) + points[i, 0] = point[0]*cos - point[1]*sin + points[i, 1] = point[0]*sin + point[1]*cos + self.polygon.append(QtCore.QPointF(points[i, 0], points[i, 1])) + + def boundingRect(self) -> QtCore.QRectF: + return self.polygon.boundingRect() + + def paint(self, painter, option, widget): + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtCore.Qt.green) + painter.setOpacity(0.6) + painter.drawPolygon(self.polygon) + + class ScanIndicator(QtWidgets.QGraphicsItem): def __init__(self, number, wx, wy, pos=(0,0)): super().__init__()