Loading analysis/analysisplots.py +4 −4 Original line number Diff line number Diff line Loading @@ -83,7 +83,6 @@ class TypeHistogramView(QtWidgets.QScrollArea): self.widgets = [] def updateTypeHistogram(self, types): # print("Updating polymer type view", flush=True) for pi in self.widgets: self.indicatorbox.removeWidget(pi) pi.setParent(None) Loading @@ -97,7 +96,6 @@ class TypeHistogramView(QtWidgets.QScrollArea): for index, entry in enumerate(types): num, text, color = entry # print("num, text, color:", num, text, color, flush=True) pi = ParticleIndicator(num, numtotal, color, text) self.indicatorbox.addWidget(pi) pi.clicked.connect(getIndexFunction(index)) Loading Loading @@ -129,7 +127,7 @@ class SpectraPlot(QtWidgets.QGroupBox): self.spectra = self.dataset.particleContainer.getSpectraFromDisk() self.canvas.draw() def updateParticleSpectrum(self, specIndex, assignment, particleSize, hqi, color): def updateParticleSpectrum(self, specIndex, assignment, hqi): #draw Sample Spectrum self.spec_axis.axis("on") self.spec_axis.clear() Loading @@ -138,9 +136,11 @@ class SpectraPlot(QtWidgets.QGroupBox): self.spec_axis.set_ylabel('Counts', fontsize = 15) if self.spectra is not None: specInfo = f'ScanPoint Number {specIndex+1}, with assignment {assignment} (hqi = {hqi})' self.spec_axis.plot(self.spectra[:, 0], self.spectra[:, specIndex+1]) self.spec_axis.set_title('{}, ScanPoint Number {}, Size = {} µm, HQI = {}, color = {}'.format(assignment, specIndex+1, particleSize, hqi, color)) self.spec_axis.set_title(specInfo, fontsize=13) self.spec_axis.set_xbound(100, (3400 if self.spectra[-1, 0] > 3400 else self.spectra[-1, 0])) wavenumber_diff = list(self.spectra[:, 0]-100) y_start = wavenumber_diff.index(min(wavenumber_diff)) y_min = min(self.spectra[y_start:, specIndex+1]) Loading analysis/analysisview.py +27 −61 Original line number Diff line number Diff line Loading @@ -45,7 +45,7 @@ except: class ParticleAnalysis(QtWidgets.QMainWindow): def __init__(self, dataset, viewparent=None): super(ParticleAnalysis, self).__init__(viewparent) self.resize(1680, 1050) # self.resize(1680, 1050) self.setWindowTitle('Results of polymer analysis') self.layout = QtWidgets.QHBoxLayout() self.widget = QtWidgets.QWidget() Loading Loading @@ -203,8 +203,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.layout.addLayout(self.menuLayout) self.layout.addLayout(viewLayout) self.createActions() self.createMenus() self.applyHQIThresholdToResults() Loading @@ -220,39 +218,44 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.loadSpectraAct = QtWidgets.QAction("Load &Spectra", self) self.loadSpectraAct.triggered.connect(self.initializeSpecPlot) self.noOverlayAct = QtWidgets.QAction("&No Overlay", self) self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self) self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self) self.databaseAct = QtWidgets.QAction("&ManageDatabase", self) self.databaseAct.triggered.connect(self.launchDBManager) self.transpAct = QtWidgets.QAction("&Transparent Overlay", self) self.expExcelAct= QtWidgets.QAction("Export &Excel List", self) self.expExcelAct.setDisabled(True) self.expExcelAct.triggered.connect(self.exportToExcel) self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self) self.expSQLAct.setDisabled(True) self.expSQLAct.triggered.connect(self.exportToSQL) self.getAndActivateActionsFromGepardMain() def getAndActivateActionsFromGepardMain(self): gepard = self.viewparent.imparent self.noOverlayAct = gepard.noOverlayAct self.selOverlayAct = gepard.selOverlayAct self.fullOverlayAct = gepard.fullOverlayAct self.transpAct = gepard.transpAct self.transpAct.triggered.connect(self.updateContourColors) self.hideLabelAct = QtWidgets.QAction('&Hide Polymer Numbers', self) self.hideLabelAct = gepard.hideLabelAct self.hideLabelAct.triggered.connect(self.show_hide_labels) self.darkenAct = QtWidgets.QAction("&Darken Image", self) self.darkenAct = gepard.darkenAct self.darkenAct.triggered.connect(self.darkenBackground) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct]: act.setCheckable(True) self.fullOverlayAct.setChecked(True) self.seedAct = QtWidgets.QAction("&Set Color Seed", self) self.seedAct = gepard.seedAct self.seedAct.triggered.connect(self.updateColorSeed) self.removeTinyParticlesAct = QtWidgets.QAction("&Remove not unknown Particles < 1 µm") self.removeTinyParticlesAct.triggered.connect(self.removeTinyParticles) self.databaseAct = QtWidgets.QAction("&ManageDatabase", self) self.databaseAct.triggered.connect(self.launchDBManager) self.expExcelAct= QtWidgets.QAction("Export &Excel List", self) self.expExcelAct.setDisabled(True) self.expExcelAct.triggered.connect(self.exportToExcel) self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self) self.expSQLAct.setDisabled(True) self.expSQLAct.triggered.connect(self.exportToSQL) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]: act.setDisabled(False) def createMenus(self): self.importMenu = QtWidgets.QMenu("&Import Spectra and Results") Loading @@ -272,7 +275,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct]) self.toolMenu = QtWidgets.QMenu("&Tools") self.toolMenu.addAction(self.removeTinyParticlesAct) self.toolMenu.addAction(self.databaseAct) self.exportMenu = QtWidgets.QMenu("&Export", self) Loading Loading @@ -308,7 +310,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): del self.importWindow self.importWindow = LoadTrueMatchResults(self.particleContainer, self) # self.importWindow.exec() @QtCore.pyqtSlot() def applyHQIThresholdToResults(self): Loading Loading @@ -452,11 +453,9 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.sizeHist_ax.figure.canvas.draw() def updateSpecPlot(self): particleSize = np.round(self.particleContainer.getSizeOfParticleByIndex(self.currentParticleIndex)) hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex) color = self.particleContainer.getParticleColorByIndex(self.currentParticleIndex) self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, particleSize, hqi, color) self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, hqi) if self.refSelector.isEnabled() and self.refSelector.currentText() != '': refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText()) Loading Loading @@ -574,39 +573,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.dataset.colorSeed = text self.updateHistogramsAndContours() def removeTinyParticles(self): indices = [] for particle in self.particleContainer.particles: if particle.getParticleAssignment() != 'unknown' and particle.getParticleSize() < 1: indices.append(particle.index) indices = sorted(indices, reverse=True) numIndices = len(indices) for index, partIndex in enumerate(indices): self.setWidgetsToNewParticleIndex(partIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(partIndex) specIndices = self.particleContainer.getSpectraIndicesOfParticle(partIndex) self.viewparent.highLightContour(partIndex) self.viewparent.centerOnRamanIndex(specIndices[0]) reply = QtWidgets.QMessageBox.question(self, f'Particle {index+1} of {numIndices}', f"Do you want to remove that particle? Type = {assignment}", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.viewparent.removeParticleContour(partIndex) self.particleContainer.removeParticles([partIndex]) elif reply == QtWidgets.QMessageBox.Cancel: self.particleContainer.resetParticleIndices() self.viewparent.resetContourIndices() return self.particleContainer.resetParticleIndices() self.viewparent.resetContourIndices() self.updateHistogramsAndContours() def show_hide_labels(self): hidden = self.hideLabelAct.isChecked() for scanIndicator in self.viewparent.ramanscanitems: Loading analysis/loadresults.py +1 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ class LoadTrueMatchResults(QtWidgets.QWidget): def show3FlagsReview(self): self.editEntryWindow = ModifyManualEdits(self, self.manualPolymers, self.manualAdditives) self.setDisabled(True) self.editEntryWindow.show() def getImportFiles(self): Loading analysis/particleAndMeasurement.py +10 −24 Original line number Diff line number Diff line Loading @@ -19,17 +19,14 @@ along with this program, see COPYING. If not, see <https://www.gnu.org/licenses/>. """ import numpy as np from viewitems import SegmentationContour, RamanScanIndicator from .particleCharacterization import getParticleColor class Particle(object): def __init__(self): super(Particle, self).__init__() self.index = None self.longSize_ellipse = np.nan self.shortSize_ellipse = np.nan self.longSize_box = np.nan self.shortSize_box = np.nan self.longSize = np.nan self.shortSize = np.nan self.height = None self.area = None self.contour = None self.measurements = [] Loading Loading @@ -78,26 +75,18 @@ class Particle(object): return assignments[indexOfHighestHQI] def getParticleSize(self): if not np.isnan(self.longSize_ellipse): size = self.longSize_ellipse elif not np.isnan(self.longSize_box): size = self.longSize_box else: if np.isnan(self.longSize): print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}') raise ValueError assert size is not None, f'Error, size of particle {self.index} is None' return size else: return self.longSize def getShortParticleSize(self): if not np.isnan(self.shortSize_ellipse): size = self.shortSize_ellipse elif not np.isnan(self.shortSize_box): size = self.shortSize_box else: if np.isnan(self.shortSize): print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}') raise ValueError assert size is not None, f'Error, short size of particle {self.index} is None' return size else: return self.shortSize def getNumberOfMeasurements(self): return len(self.measurements) Loading @@ -120,9 +109,6 @@ class Particle(object): for measurement in self.measurements: measurement.applyHQIThreshold(minHQI) def recreateViewItem(self): self.viewItem = SegmentationContour() class Measurement(object): def __init__(self): Loading analysis/particleCharacterization.py +117 −100 Original line number Diff line number Diff line Loading @@ -24,66 +24,44 @@ import numpy as np import cv2 from copy import deepcopy def getContourStatsWithPixelScale(cnt, pixelscale): long, short, longellipse, shortellipse, area = getContourStats(cnt) return long*pixelscale, short*pixelscale, longellipse*pixelscale, shortellipse*pixelscale, area*pixelscale**2 def getContourStats(cnt): ##characterize particle longellipse, shortellipse = np.nan, np.nan if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... ellipse = cv2.fitEllipse(cnt) shortellipse, longellipse = ellipse[1] from .particleClassification.colorClassification import ColorClassifier from .particleClassification.shapeClassification import ShapeClassifier from segmentation import closeHolesOfSubImage from errors import NotConnectedContoursError, InvalidParticleError class ParticleStats(object): longSize = None shortSize = None height = None area = None shape = None color = None def particleIsValid(particle): if particle.longSize == 0 or particle.shortSize == 0: return False rect = cv2.minAreaRect(cnt) long, short = rect[1] if short>long: long, short = short, long if cv2.contourArea(particle.contour) == 0: return False return True area = cv2.contourArea(cnt) return long, short, longellipse, shortellipse, area def getParticleStatsWithPixelScale(cnt, pixelscale, fullimage, dataset): newStats = ParticleStats() class ColorRangeHSV(object): def __init__(self, name, hue, hue_tolerance, min_sat, max_sat): self.name = name self.minHue = hue-hue_tolerance/2 self.maxHue = hue+hue_tolerance/2 self.minSat = min_sat self.maxSat = max_sat newStats.longSize, newStats.shortSize, newStats.area = getContourStats(cnt) newStats.longSize *= pixelscale newStats.shortSize *= pixelscale newStats.area *= (pixelscale**2) def containsHSV(self, hsv): hue = hsv[0] sat = hsv[1] newStats.height = getParticleHeight(cnt, dataset) print('newHeight =', newStats.height) newStats.shape = getParticleShape(cnt, newStats.height) if self.minHue <= hue <= self.maxHue and self.minSat <= sat <= self.maxSat: return True else: if self.name != 'white': return False else: if sat < 128 and hsv[2] > 70: return True partImg = getParticleImageFromFullimage(cnt, fullimage) newStats.color = getParticleColor(partImg) class ColorClassifier(object): def __init__(self): hue_tolerance = 30 self.colors = [ColorRangeHSV('yellow', 30, hue_tolerance, 30, 255), ColorRangeHSV('blue', 120, hue_tolerance, 80, 255), ColorRangeHSV('red', 180, hue_tolerance, 50, 255), ColorRangeHSV('red', 0, hue_tolerance, 50, 255), ColorRangeHSV('green', 70, hue_tolerance, 50, 255), ColorRangeHSV('white', 128, 256, 0, 50)] def classifyColor(self, meanHSV): result = 'non-determinable' for color in self.colors: if color.containsHSV(meanHSV): result = color.name break return result return newStats def getParticleColor(imgRGB, colorClassifier=None): img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL) Loading @@ -93,6 +71,44 @@ def getParticleColor(imgRGB, colorClassifier=None): color = colorClassifier.classifyColor(meanHSV) return color def getParticleShape(contour, particleHeight, shapeClassifier=None): if shapeClassifier is None: shapeClassifier = ShapeClassifier() try: shape = shapeClassifier.classifyShape(contour, particleHeight) except InvalidParticleError: raise return shape def getParticleHeight(contour, dataset): zimg = getParticleImageFromFullimage(contour, dataset.getZvalImg()) if zimg.shape[0] == 0 or zimg.shape[1] == 0: raise InvalidParticleError zimg = cv2.medianBlur(zimg, 5) avg_ZValue = np.mean(zimg[zimg > 0]) if np.isnan(avg_ZValue): #i.e., only zeros in zimg avg_ZValue = 0 z0, z1 = dataset.zpositions.min(), dataset.zpositions.max() 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... ellipse = cv2.fitEllipse(cnt) short, long = ellipse[1] else: rect = cv2.minAreaRect(cnt) long, short = rect[1] if short>long: long, short = short, long area = cv2.contourArea(cnt) return long, short, area def mergeContours(contours): img, xmin, ymin, padding = contoursToImg(contours) return imgToCnt(img, xmin, ymin, padding) Loading @@ -115,13 +131,15 @@ def getParticleImageFromFullimage(contour, fullimage): img = np.array(img, dtype = np.uint8) return img def contoursToImg(contours, padding=2): def contoursToImg(contours, padding=0): contourCopy = deepcopy(contours) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) padding = padding #pixel in each direction rangex = int(np.round((xmax-xmin)+2*padding)) rangey = int(np.round((ymax-ymin)+2*padding)) if rangex == 0 or rangey == 0: raise InvalidParticleError img = np.zeros((rangey, rangex)) for curCnt in contourCopy: Loading @@ -130,42 +148,44 @@ def contoursToImg(contours, padding=2): curCnt[i][0][1] -= ymin-padding cv2.drawContours(img, [curCnt], -1, 255, -1) cv2.drawContours(img, [curCnt], -1, 255, 1) img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3)))) return img, xmin, ymin, padding def imgToCnt(img, xmin, ymin, padding): def getSimpleContour(img): def imgToCnt(img, xmin, ymin, padding=0): def getContour(img, flag): if cv2.__version__ > '3.5': contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag) else: temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) if len(contour)>1: raise NotConnectedContoursError return contour temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag) def getFullContour(img): if cv2.__version__ > '3.5': contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) else: temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) if len(contours) == 0: #i.e., no contour found raise InvalidParticleError elif len(contours) == 1: #i.e., exactly one contour found contour = contours[0] else: #i.e., multiple contours found contour = getLargestContour(contours) if len(contour)>1: raise NotConnectedContoursError return contour contour = getSimpleContour(img) def getLargestContour(contours): areas = [] for contour in contours: areas.append(cv2.contourArea(contour)) maxIndex = areas.index(max(areas)) print(f'{len(contours)} contours found, getting the largest one. Areas are: {areas}, taking contour at index {maxIndex}') return contours[maxIndex] if len(contour[0]) < 5: contour = getFullContour(img) img = closeHolesOfSubImage(img) contour = getContour(img, flag=cv2.CHAIN_APPROX_SIMPLE) newContour = contour[0] for i in range(len(newContour )): newContour [i][0][0] += xmin-padding newContour [i][0][1] += ymin-padding if len(contour) < 5: contour = getContour(img, flag=cv2.CHAIN_APPROX_NONE) return newContour for i in range(len(contour)): contour [i][0][0] += xmin-padding contour [i][0][1] += ymin-padding return contour def getContourExtrema(contours): try: Loading @@ -175,21 +195,18 @@ def getContourExtrema(contours): except IndexError: #i.e., not a list of contours was passed, but an individual contour. Hence, the above indexing does not work xmin, xmax = cnt[:, 0].min(), cnt[:, 0].max() ymin, ymax = cnt[:, 1].min(), cnt[:, 1].max() return xmin, xmax, ymin, ymax class NotConnectedContoursError(Exception): pass if __name__ == '__main__': colors = {'white': (41, 25, 66), "red": (128, 121, 57), "red2": (23, 88, 49), "yellow": (25, 121, 91), "pink": (11, 79, 51), "brown": (32, 38, 64), "green": (54, 99, 53)} classifier= ColorClassifier() # print(classifier.hsv) for name, mean in colors.items(): print(name, classifier.classifyColor(mean)) xmin, xmax = int(round(xmin)), int(round(xmax)) ymin, ymax = int(round(ymin)), int(round(ymax)) return xmin, xmax, ymin, ymax def getParticleCenterPoint(contour): img, xmin, ymin, padding = contoursToImg(contour) dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) ind = np.argmax(dist) y = ind//dist.shape[1]-1 x = ind%dist.shape[1]-1 x += xmin y += ymin return x, y Loading
analysis/analysisplots.py +4 −4 Original line number Diff line number Diff line Loading @@ -83,7 +83,6 @@ class TypeHistogramView(QtWidgets.QScrollArea): self.widgets = [] def updateTypeHistogram(self, types): # print("Updating polymer type view", flush=True) for pi in self.widgets: self.indicatorbox.removeWidget(pi) pi.setParent(None) Loading @@ -97,7 +96,6 @@ class TypeHistogramView(QtWidgets.QScrollArea): for index, entry in enumerate(types): num, text, color = entry # print("num, text, color:", num, text, color, flush=True) pi = ParticleIndicator(num, numtotal, color, text) self.indicatorbox.addWidget(pi) pi.clicked.connect(getIndexFunction(index)) Loading Loading @@ -129,7 +127,7 @@ class SpectraPlot(QtWidgets.QGroupBox): self.spectra = self.dataset.particleContainer.getSpectraFromDisk() self.canvas.draw() def updateParticleSpectrum(self, specIndex, assignment, particleSize, hqi, color): def updateParticleSpectrum(self, specIndex, assignment, hqi): #draw Sample Spectrum self.spec_axis.axis("on") self.spec_axis.clear() Loading @@ -138,9 +136,11 @@ class SpectraPlot(QtWidgets.QGroupBox): self.spec_axis.set_ylabel('Counts', fontsize = 15) if self.spectra is not None: specInfo = f'ScanPoint Number {specIndex+1}, with assignment {assignment} (hqi = {hqi})' self.spec_axis.plot(self.spectra[:, 0], self.spectra[:, specIndex+1]) self.spec_axis.set_title('{}, ScanPoint Number {}, Size = {} µm, HQI = {}, color = {}'.format(assignment, specIndex+1, particleSize, hqi, color)) self.spec_axis.set_title(specInfo, fontsize=13) self.spec_axis.set_xbound(100, (3400 if self.spectra[-1, 0] > 3400 else self.spectra[-1, 0])) wavenumber_diff = list(self.spectra[:, 0]-100) y_start = wavenumber_diff.index(min(wavenumber_diff)) y_min = min(self.spectra[y_start:, specIndex+1]) Loading
analysis/analysisview.py +27 −61 Original line number Diff line number Diff line Loading @@ -45,7 +45,7 @@ except: class ParticleAnalysis(QtWidgets.QMainWindow): def __init__(self, dataset, viewparent=None): super(ParticleAnalysis, self).__init__(viewparent) self.resize(1680, 1050) # self.resize(1680, 1050) self.setWindowTitle('Results of polymer analysis') self.layout = QtWidgets.QHBoxLayout() self.widget = QtWidgets.QWidget() Loading Loading @@ -203,8 +203,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.layout.addLayout(self.menuLayout) self.layout.addLayout(viewLayout) self.createActions() self.createMenus() self.applyHQIThresholdToResults() Loading @@ -220,39 +218,44 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.loadSpectraAct = QtWidgets.QAction("Load &Spectra", self) self.loadSpectraAct.triggered.connect(self.initializeSpecPlot) self.noOverlayAct = QtWidgets.QAction("&No Overlay", self) self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self) self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self) self.databaseAct = QtWidgets.QAction("&ManageDatabase", self) self.databaseAct.triggered.connect(self.launchDBManager) self.transpAct = QtWidgets.QAction("&Transparent Overlay", self) self.expExcelAct= QtWidgets.QAction("Export &Excel List", self) self.expExcelAct.setDisabled(True) self.expExcelAct.triggered.connect(self.exportToExcel) self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self) self.expSQLAct.setDisabled(True) self.expSQLAct.triggered.connect(self.exportToSQL) self.getAndActivateActionsFromGepardMain() def getAndActivateActionsFromGepardMain(self): gepard = self.viewparent.imparent self.noOverlayAct = gepard.noOverlayAct self.selOverlayAct = gepard.selOverlayAct self.fullOverlayAct = gepard.fullOverlayAct self.transpAct = gepard.transpAct self.transpAct.triggered.connect(self.updateContourColors) self.hideLabelAct = QtWidgets.QAction('&Hide Polymer Numbers', self) self.hideLabelAct = gepard.hideLabelAct self.hideLabelAct.triggered.connect(self.show_hide_labels) self.darkenAct = QtWidgets.QAction("&Darken Image", self) self.darkenAct = gepard.darkenAct self.darkenAct.triggered.connect(self.darkenBackground) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct]: act.setCheckable(True) self.fullOverlayAct.setChecked(True) self.seedAct = QtWidgets.QAction("&Set Color Seed", self) self.seedAct = gepard.seedAct self.seedAct.triggered.connect(self.updateColorSeed) self.removeTinyParticlesAct = QtWidgets.QAction("&Remove not unknown Particles < 1 µm") self.removeTinyParticlesAct.triggered.connect(self.removeTinyParticles) self.databaseAct = QtWidgets.QAction("&ManageDatabase", self) self.databaseAct.triggered.connect(self.launchDBManager) self.expExcelAct= QtWidgets.QAction("Export &Excel List", self) self.expExcelAct.setDisabled(True) self.expExcelAct.triggered.connect(self.exportToExcel) self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self) self.expSQLAct.setDisabled(True) self.expSQLAct.triggered.connect(self.exportToSQL) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]: act.setDisabled(False) def createMenus(self): self.importMenu = QtWidgets.QMenu("&Import Spectra and Results") Loading @@ -272,7 +275,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct]) self.toolMenu = QtWidgets.QMenu("&Tools") self.toolMenu.addAction(self.removeTinyParticlesAct) self.toolMenu.addAction(self.databaseAct) self.exportMenu = QtWidgets.QMenu("&Export", self) Loading Loading @@ -308,7 +310,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): del self.importWindow self.importWindow = LoadTrueMatchResults(self.particleContainer, self) # self.importWindow.exec() @QtCore.pyqtSlot() def applyHQIThresholdToResults(self): Loading Loading @@ -452,11 +453,9 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.sizeHist_ax.figure.canvas.draw() def updateSpecPlot(self): particleSize = np.round(self.particleContainer.getSizeOfParticleByIndex(self.currentParticleIndex)) hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex) color = self.particleContainer.getParticleColorByIndex(self.currentParticleIndex) self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, particleSize, hqi, color) self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, hqi) if self.refSelector.isEnabled() and self.refSelector.currentText() != '': refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText()) Loading Loading @@ -574,39 +573,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow): self.dataset.colorSeed = text self.updateHistogramsAndContours() def removeTinyParticles(self): indices = [] for particle in self.particleContainer.particles: if particle.getParticleAssignment() != 'unknown' and particle.getParticleSize() < 1: indices.append(particle.index) indices = sorted(indices, reverse=True) numIndices = len(indices) for index, partIndex in enumerate(indices): self.setWidgetsToNewParticleIndex(partIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(partIndex) specIndices = self.particleContainer.getSpectraIndicesOfParticle(partIndex) self.viewparent.highLightContour(partIndex) self.viewparent.centerOnRamanIndex(specIndices[0]) reply = QtWidgets.QMessageBox.question(self, f'Particle {index+1} of {numIndices}', f"Do you want to remove that particle? Type = {assignment}", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.viewparent.removeParticleContour(partIndex) self.particleContainer.removeParticles([partIndex]) elif reply == QtWidgets.QMessageBox.Cancel: self.particleContainer.resetParticleIndices() self.viewparent.resetContourIndices() return self.particleContainer.resetParticleIndices() self.viewparent.resetContourIndices() self.updateHistogramsAndContours() def show_hide_labels(self): hidden = self.hideLabelAct.isChecked() for scanIndicator in self.viewparent.ramanscanitems: Loading
analysis/loadresults.py +1 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ class LoadTrueMatchResults(QtWidgets.QWidget): def show3FlagsReview(self): self.editEntryWindow = ModifyManualEdits(self, self.manualPolymers, self.manualAdditives) self.setDisabled(True) self.editEntryWindow.show() def getImportFiles(self): Loading
analysis/particleAndMeasurement.py +10 −24 Original line number Diff line number Diff line Loading @@ -19,17 +19,14 @@ along with this program, see COPYING. If not, see <https://www.gnu.org/licenses/>. """ import numpy as np from viewitems import SegmentationContour, RamanScanIndicator from .particleCharacterization import getParticleColor class Particle(object): def __init__(self): super(Particle, self).__init__() self.index = None self.longSize_ellipse = np.nan self.shortSize_ellipse = np.nan self.longSize_box = np.nan self.shortSize_box = np.nan self.longSize = np.nan self.shortSize = np.nan self.height = None self.area = None self.contour = None self.measurements = [] Loading Loading @@ -78,26 +75,18 @@ class Particle(object): return assignments[indexOfHighestHQI] def getParticleSize(self): if not np.isnan(self.longSize_ellipse): size = self.longSize_ellipse elif not np.isnan(self.longSize_box): size = self.longSize_box else: if np.isnan(self.longSize): print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}') raise ValueError assert size is not None, f'Error, size of particle {self.index} is None' return size else: return self.longSize def getShortParticleSize(self): if not np.isnan(self.shortSize_ellipse): size = self.shortSize_ellipse elif not np.isnan(self.shortSize_box): size = self.shortSize_box else: if np.isnan(self.shortSize): print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}') raise ValueError assert size is not None, f'Error, short size of particle {self.index} is None' return size else: return self.shortSize def getNumberOfMeasurements(self): return len(self.measurements) Loading @@ -120,9 +109,6 @@ class Particle(object): for measurement in self.measurements: measurement.applyHQIThreshold(minHQI) def recreateViewItem(self): self.viewItem = SegmentationContour() class Measurement(object): def __init__(self): Loading
analysis/particleCharacterization.py +117 −100 Original line number Diff line number Diff line Loading @@ -24,66 +24,44 @@ import numpy as np import cv2 from copy import deepcopy def getContourStatsWithPixelScale(cnt, pixelscale): long, short, longellipse, shortellipse, area = getContourStats(cnt) return long*pixelscale, short*pixelscale, longellipse*pixelscale, shortellipse*pixelscale, area*pixelscale**2 def getContourStats(cnt): ##characterize particle longellipse, shortellipse = np.nan, np.nan if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... ellipse = cv2.fitEllipse(cnt) shortellipse, longellipse = ellipse[1] from .particleClassification.colorClassification import ColorClassifier from .particleClassification.shapeClassification import ShapeClassifier from segmentation import closeHolesOfSubImage from errors import NotConnectedContoursError, InvalidParticleError class ParticleStats(object): longSize = None shortSize = None height = None area = None shape = None color = None def particleIsValid(particle): if particle.longSize == 0 or particle.shortSize == 0: return False rect = cv2.minAreaRect(cnt) long, short = rect[1] if short>long: long, short = short, long if cv2.contourArea(particle.contour) == 0: return False return True area = cv2.contourArea(cnt) return long, short, longellipse, shortellipse, area def getParticleStatsWithPixelScale(cnt, pixelscale, fullimage, dataset): newStats = ParticleStats() class ColorRangeHSV(object): def __init__(self, name, hue, hue_tolerance, min_sat, max_sat): self.name = name self.minHue = hue-hue_tolerance/2 self.maxHue = hue+hue_tolerance/2 self.minSat = min_sat self.maxSat = max_sat newStats.longSize, newStats.shortSize, newStats.area = getContourStats(cnt) newStats.longSize *= pixelscale newStats.shortSize *= pixelscale newStats.area *= (pixelscale**2) def containsHSV(self, hsv): hue = hsv[0] sat = hsv[1] newStats.height = getParticleHeight(cnt, dataset) print('newHeight =', newStats.height) newStats.shape = getParticleShape(cnt, newStats.height) if self.minHue <= hue <= self.maxHue and self.minSat <= sat <= self.maxSat: return True else: if self.name != 'white': return False else: if sat < 128 and hsv[2] > 70: return True partImg = getParticleImageFromFullimage(cnt, fullimage) newStats.color = getParticleColor(partImg) class ColorClassifier(object): def __init__(self): hue_tolerance = 30 self.colors = [ColorRangeHSV('yellow', 30, hue_tolerance, 30, 255), ColorRangeHSV('blue', 120, hue_tolerance, 80, 255), ColorRangeHSV('red', 180, hue_tolerance, 50, 255), ColorRangeHSV('red', 0, hue_tolerance, 50, 255), ColorRangeHSV('green', 70, hue_tolerance, 50, 255), ColorRangeHSV('white', 128, 256, 0, 50)] def classifyColor(self, meanHSV): result = 'non-determinable' for color in self.colors: if color.containsHSV(meanHSV): result = color.name break return result return newStats def getParticleColor(imgRGB, colorClassifier=None): img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL) Loading @@ -93,6 +71,44 @@ def getParticleColor(imgRGB, colorClassifier=None): color = colorClassifier.classifyColor(meanHSV) return color def getParticleShape(contour, particleHeight, shapeClassifier=None): if shapeClassifier is None: shapeClassifier = ShapeClassifier() try: shape = shapeClassifier.classifyShape(contour, particleHeight) except InvalidParticleError: raise return shape def getParticleHeight(contour, dataset): zimg = getParticleImageFromFullimage(contour, dataset.getZvalImg()) if zimg.shape[0] == 0 or zimg.shape[1] == 0: raise InvalidParticleError zimg = cv2.medianBlur(zimg, 5) avg_ZValue = np.mean(zimg[zimg > 0]) if np.isnan(avg_ZValue): #i.e., only zeros in zimg avg_ZValue = 0 z0, z1 = dataset.zpositions.min(), dataset.zpositions.max() 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... ellipse = cv2.fitEllipse(cnt) short, long = ellipse[1] else: rect = cv2.minAreaRect(cnt) long, short = rect[1] if short>long: long, short = short, long area = cv2.contourArea(cnt) return long, short, area def mergeContours(contours): img, xmin, ymin, padding = contoursToImg(contours) return imgToCnt(img, xmin, ymin, padding) Loading @@ -115,13 +131,15 @@ def getParticleImageFromFullimage(contour, fullimage): img = np.array(img, dtype = np.uint8) return img def contoursToImg(contours, padding=2): def contoursToImg(contours, padding=0): contourCopy = deepcopy(contours) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) padding = padding #pixel in each direction rangex = int(np.round((xmax-xmin)+2*padding)) rangey = int(np.round((ymax-ymin)+2*padding)) if rangex == 0 or rangey == 0: raise InvalidParticleError img = np.zeros((rangey, rangex)) for curCnt in contourCopy: Loading @@ -130,42 +148,44 @@ def contoursToImg(contours, padding=2): curCnt[i][0][1] -= ymin-padding cv2.drawContours(img, [curCnt], -1, 255, -1) cv2.drawContours(img, [curCnt], -1, 255, 1) img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3)))) return img, xmin, ymin, padding def imgToCnt(img, xmin, ymin, padding): def getSimpleContour(img): def imgToCnt(img, xmin, ymin, padding=0): def getContour(img, flag): if cv2.__version__ > '3.5': contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag) else: temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) if len(contour)>1: raise NotConnectedContoursError return contour temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag) def getFullContour(img): if cv2.__version__ > '3.5': contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) else: temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) if len(contours) == 0: #i.e., no contour found raise InvalidParticleError elif len(contours) == 1: #i.e., exactly one contour found contour = contours[0] else: #i.e., multiple contours found contour = getLargestContour(contours) if len(contour)>1: raise NotConnectedContoursError return contour contour = getSimpleContour(img) def getLargestContour(contours): areas = [] for contour in contours: areas.append(cv2.contourArea(contour)) maxIndex = areas.index(max(areas)) print(f'{len(contours)} contours found, getting the largest one. Areas are: {areas}, taking contour at index {maxIndex}') return contours[maxIndex] if len(contour[0]) < 5: contour = getFullContour(img) img = closeHolesOfSubImage(img) contour = getContour(img, flag=cv2.CHAIN_APPROX_SIMPLE) newContour = contour[0] for i in range(len(newContour )): newContour [i][0][0] += xmin-padding newContour [i][0][1] += ymin-padding if len(contour) < 5: contour = getContour(img, flag=cv2.CHAIN_APPROX_NONE) return newContour for i in range(len(contour)): contour [i][0][0] += xmin-padding contour [i][0][1] += ymin-padding return contour def getContourExtrema(contours): try: Loading @@ -175,21 +195,18 @@ def getContourExtrema(contours): except IndexError: #i.e., not a list of contours was passed, but an individual contour. Hence, the above indexing does not work xmin, xmax = cnt[:, 0].min(), cnt[:, 0].max() ymin, ymax = cnt[:, 1].min(), cnt[:, 1].max() return xmin, xmax, ymin, ymax class NotConnectedContoursError(Exception): pass if __name__ == '__main__': colors = {'white': (41, 25, 66), "red": (128, 121, 57), "red2": (23, 88, 49), "yellow": (25, 121, 91), "pink": (11, 79, 51), "brown": (32, 38, 64), "green": (54, 99, 53)} classifier= ColorClassifier() # print(classifier.hsv) for name, mean in colors.items(): print(name, classifier.classifyColor(mean)) xmin, xmax = int(round(xmin)), int(round(xmax)) ymin, ymax = int(round(ymin)), int(round(ymax)) return xmin, xmax, ymin, ymax def getParticleCenterPoint(contour): img, xmin, ymin, padding = contoursToImg(contour) dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) ind = np.argmax(dist) y = ind//dist.shape[1]-1 x = ind%dist.shape[1]-1 x += xmin y += ymin return x, y