From c438bccac3207e15d2beabf3c10f2a6760caa696 Mon Sep 17 00:00:00 2001 From: Hackmet <34997035+Hackmet@users.noreply.github.com> Date: Sun, 3 Feb 2019 20:02:43 +0100 Subject: [PATCH] Peparation for RenishawCom, inclusion of analysis module --- README.md | 2 +- analysis/analysisWidgets.py | 108 ++ analysis/analysisview.py | 953 ++++++++++++++++++ analysis/editParticles.py | 161 +++ analysis/loadresults.py | 347 +++++++ dataset.py | 128 ++- detectionview.py | 9 +- {externalmodules => external}/__init__.py | 0 {externalmodules => external}/setuptsp.py | 0 {externalmodules => external}/tsp.pyx | 0 gepard.py | 46 +- helperfunctions.py | 14 +- opticalscan.py | 177 +++- WITecCOM.py => ramancom/WITecCOM.py | 42 +- ramancom/__init__.py | 0 ramancom/configRaman.py | 175 ++++ ramancom/ramanSwitch.py | 180 ++++ ramanbase.py => ramancom/ramanbase.py | 11 +- ramancontrol.py => ramancom/ramancontrol.py | 32 +- .../simulatedraman.py | 19 +- witectesting.py => ramancom/witectesting.py | 0 ramanscanui.py | 119 ++- sample_gepard.cfg | 16 +- sampleview.py | 222 +++- scalebar.py | 6 +- segmentation.py | 6 +- viewitems.py | 164 ++- 27 files changed, 2678 insertions(+), 259 deletions(-) create mode 100644 analysis/analysisWidgets.py create mode 100644 analysis/analysisview.py create mode 100644 analysis/editParticles.py create mode 100644 analysis/loadresults.py rename {externalmodules => external}/__init__.py (100%) rename {externalmodules => external}/setuptsp.py (100%) mode change 100755 => 100644 rename {externalmodules => external}/tsp.pyx (100%) mode change 100755 => 100644 rename WITecCOM.py => ramancom/WITecCOM.py (91%) create mode 100644 ramancom/__init__.py create mode 100644 ramancom/configRaman.py create mode 100644 ramancom/ramanSwitch.py rename ramanbase.py => ramancom/ramanbase.py (86%) rename ramancontrol.py => ramancom/ramancontrol.py (55%) rename simulatedraman.py => ramancom/simulatedraman.py (90%) rename witectesting.py => ramancom/witectesting.py (100%) diff --git a/README.md b/README.md index 0184319..068dd63 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Requirements: for 64bit as many use cases require a lot of memory (16 GB better 32 GB recommended) -* the tsp module in externalmodules can be built with +* the tsp module in external can be built with python setuptsp.py please note: for this step a valid compiler needs to be installed in the system; Otherwise use the precompiled tsp-module diff --git a/analysis/analysisWidgets.py b/analysis/analysisWidgets.py new file mode 100644 index 0000000..6a9a44a --- /dev/null +++ b/analysis/analysisWidgets.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +""" +GEPARD - Gepard-Enabled PARticle Detection +Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für +Polymerforschung Dresden e. V. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program, see COPYING. +If not, see . +""" +from PyQt5 import QtCore, QtWidgets, QtGui +from PIL import ImageFont +import numpy as np + + +WX, WY = 1024, 200 + +class Legend(QtWidgets.QMdiSubWindow): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.FramelessWindowHint) + + self.setFixedSize(200, 800) + self.wscale = 1 + self.drag = None + self.items = [] #list of items to display (text, color) + self.tileSize = 12 + self.fontSize = 15 + self.spacer = 10 + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.LeftButton: + self.drag = event.pos() + + def mouseMoveEvent(self, event): + if self.drag is not None: + p0 = event.pos() + self.move(self.mapToParent(p0-self.drag)) + self.parentWidget().update() + else: + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self.drag = None + super().mouseReleaseEvent(event) + + def paintEvent(self, event): + numEntries = len(self.items) + if numEntries > 0: + + def getSize(fontsize, text, tileSize, spacer): +# font = ImageFont.truetype('arial.ttf', fontsize) +# size = font.getsize(text) + size = 5*len(text), fontsize+2 + width, height = size[0]*1.5 + tileSize + spacer, numEntries * (tileSize+1*spacer) + 2*spacer + + return width, height + + + fontSize, tileSize, spacer = self.fontSize, self.tileSize, self.spacer + longestEntry = max([i[0] for i in self.items], key=len) + width, height = getSize(fontSize, longestEntry, tileSize, spacer) + + #scale smaller, if necessary + if height > 1024: + factor = 1024/height + #prevent text getting tooo small: + factor = np.clip(factor, 0.6, 1) #0.6*15 would be fontSize 9 + + height, tileSize, fontSize, spacer = height*factor, tileSize*factor, int(fontSize*factor), spacer*factor + + width, height = getSize(fontSize, longestEntry, tileSize, spacer) + + self.setFixedSize(width, height) + + qp = QtGui.QPainter() + qp.begin(self) + font = QtGui.QFont() + font.setPixelSize(fontSize) + qp.setFont(font) + + for index, item in enumerate(self.items): + #draw Text + x0 = tileSize+2*spacer + y0 = index*(fontSize+spacer) + rect= QtCore.QRectF(x0, y0, width, float(fontSize+spacer)) + qp.drawText(rect, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter, item[0]) + + #draw colored Box + qp.setBrush(item[1]) + x0 = spacer + y0 = index*(fontSize+spacer) + (fontSize - tileSize)/2 +spacer/2 + qp.drawRect(x0, y0, tileSize, tileSize) + + qp.end() + + \ No newline at end of file diff --git a/analysis/analysisview.py b/analysis/analysisview.py new file mode 100644 index 0000000..fad7040 --- /dev/null +++ b/analysis/analysisview.py @@ -0,0 +1,953 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu May 31 10:07:45 2018 + +@author: brandt +""" + +""" +GEPARD - Gepard-Enabled PARticle Detection +Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für +Polymerforschung Dresden e. V. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program, see COPYING. +If not, see . +""" + +import numpy as np +from PyQt5 import QtCore, QtGui, QtWidgets +import sys +import operator +import os +import random +import colorsys + +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar + +import pandas as pd +from analysis.loadresults import LoadWITecResults +from analysis.sqlexport import SQLExport +from analysis.editParticles import ParticleEditor + + +class ParticleAnalysis(QtWidgets.QWidget): + def __init__(self, parent): + super(ParticleAnalysis, self).__init__() + self.setGeometry(100, 100, 1680, 1050) + self.setWindowTitle('Results of polymer analysis') + self.layout = QtWidgets.QHBoxLayout() + self.setLayout(self.layout) + + self.parent = parent + if self.parent is not None: + self.config = self.parent.dataset.resultParams + + self.editor = ParticleEditor(self) + + self.spectraResults = None #entire List of all spectra assignments + self.additiveResults = None #entire List of all additives + self.particlestats = None + self.particleResults = None #final assignment for each particle + + self.currentPolymers = None #list of polymers after setting entries with low hqi to unknown + self.currentAdditives = None #same thing for the additives + self.uniquePolymers = None #list of present polymer types + self.spectra = None #acquired spectra + self.indices = None #assignment of what spectra-indices belong to what substance + self.expWindow = None + self.additivePlot = None + self.importWindow = None + self.directory = None + self.particles2spectra = None + self.manualPolymers = {} + self.manualAdditives = {} + self.polymerCheckBoxes = [] + self.lastSelectedCheckBoxNames = [] + + self.currentParticleIndex = 0 + self.currentSpectrumIndex = 0 + self.lastSpectrumInFocus = None + + self.typeHistogramCanvas = FigureCanvas(Figure()) + self.sizeHistogramCanvas = FigureCanvas(Figure()) + + self.typeHist_ax = self.typeHistogramCanvas.figure.subplots() + self.typeHist_ax.axis('off') + sizeHistGroup = QtWidgets.QGroupBox() + sizeHistLayout = QtWidgets.QHBoxLayout() + self.sizeHist_ax = self.sizeHistogramCanvas.figure.subplots() + self.sizeHist_ax.axis('off') + self.sizeHistogramCanvas.figure.subplots_adjust(left=0.1, top=0.93, bottom=0.15, right=0.995) + histNavigation = NavigationToolbar(self.sizeHistogramCanvas, self) + histNavigation.setOrientation(QtCore.Qt.Vertical) + histNavigation.setMinimumWidth(50) + histNavigation.setMaximumWidth(50) + sizeHistLayout.addWidget(histNavigation) + sizeHistLayout.addWidget(self.sizeHistogramCanvas) + sizeHistGroup.setLayout(sizeHistLayout) + + specGroup = QtWidgets.QGroupBox() + specLayout = QtWidgets.QHBoxLayout() + self.specCanvas = FigureCanvas(Figure()) + self.spec_ax = self.specCanvas.figure.subplots() + self.spec_ax.axis("off") + self.specCanvas.figure.subplots_adjust(left=0.1, top=0.93, bottom=0.15, right=0.995) + specNavigation = NavigationToolbar(self.specCanvas, self) + specNavigation.setOrientation(QtCore.Qt.Vertical) + specNavigation.setMinimumWidth(50) + specNavigation.setMaximumWidth(50) + + specLayout.addWidget(specNavigation) + specLayout.addWidget(self.specCanvas) + specGroup.setLayout(specLayout) + + viewLayout = QtWidgets.QVBoxLayout() + self.menuLayout = QtWidgets.QVBoxLayout() + + splitter1 = QtWidgets.QSplitter(QtCore.Qt.Vertical) + splitter1.addWidget(specGroup) + splitter1.addWidget(sizeHistGroup) + splitter2 = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + splitter2.addWidget(splitter1) + splitter2.addWidget(self.typeHistogramCanvas) + splitter2.setSizes([300, 150]) + + self.navigationGroup = QtWidgets.QGroupBox('Navigate through polymers') + self.navigationGroup.setDisabled(True) + navigationLayout = QtWidgets.QHBoxLayout() + self.polymerComboBox = QtWidgets.QComboBox() + self.polymerComboBox.currentIndexChanged.connect(self.displayNewPolymerType) + self.polymerComboBox.setMinimumWidth(150) + self.particleSelector = QtWidgets.QSpinBox() + self.particleSelector.valueChanged.connect(self.selectParticle) + self.spectrumSelector = QtWidgets.QSpinBox() + self.spectrumSelector.valueChanged.connect(self.selectSpectrum) + for spinbox in [self.particleSelector, self.spectrumSelector]: + spinbox.setMinimum(1) + spinbox.setSingleStep(1) + spinbox.setValue(1) + + self.overlayGroup = QtWidgets.QGroupBox('Color Overlay') + self.overlayLayout = QtWidgets.QHBoxLayout() + self.noOverlayBtn = QtWidgets.QRadioButton('None') + self.selOverlayBtn = QtWidgets.QRadioButton('Selected') + self.fullOverlayBtn = QtWidgets.QRadioButton('Full') + for index, button in enumerate([self.noOverlayBtn, self.selOverlayBtn, self.fullOverlayBtn]): + if index == 2: + button.setChecked(True) + else: + button.setChecked(False) + button.released.connect(self.createPolymerOverlay) + button.released.connect(self.updateHistogram) + self.overlayLayout.addWidget(button) + + self.seedBtn = QtWidgets.QPushButton('Set Color Seed') + self.seedBtn.released.connect(self.updateColorSeed) + self.overlayLayout.addWidget(self.seedBtn) + + self.hideLabelBtn = QtWidgets.QCheckBox('Hide Polymer Numbers') + self.hideLabelBtn.stateChanged.connect(self.show_hide_labels) + self.hideLabelBtn.setChecked(False) +# self.hideLabelBtn.setChecked(True) #change twice in order to run the connected function... + self.overlayLayout.addWidget(self.hideLabelBtn) + + self.transpBtn = QtWidgets.QCheckBox('Transparent') + self.transpBtn.setChecked(False) + self.transpBtn.stateChanged.connect(self.createPolymerOverlay) + self.overlayLayout.addWidget(self.transpBtn) + + self.darkenBtn = QtWidgets.QCheckBox('Darken Image') + self.darkenBtn.setChecked(False) + self.darkenBtn.stateChanged.connect(self.darkenBackground) + self.overlayLayout.addWidget(self.darkenBtn) + + self.overlayGroup.setLayout(self.overlayLayout) + + navigationLayout.addWidget(QtWidgets.QLabel('Select Polymer Type:')) + navigationLayout.addWidget(self.polymerComboBox) + navigationLayout.addStretch() + navigationLayout.addWidget(QtWidgets.QLabel('Select Particle')) + navigationLayout.addWidget(self.particleSelector) + navigationLayout.addWidget(QtWidgets.QLabel('Select Spectrum')) + navigationLayout.addWidget(self.spectrumSelector) + navigationLayout.addStretch() + + self.navigationGroup.setLayout(navigationLayout) + + topLayout = QtWidgets.QHBoxLayout() + topLayout.addWidget(self.navigationGroup) + topLayout.addWidget(self.overlayGroup) + + viewLayout.addLayout(topLayout) + viewLayout.addWidget(splitter2) + viewLayout.setStretch(1, 1) + + reloadGroup = QtWidgets.QGroupBox('Reload Results from:') + reloadLayout = QtWidgets.QVBoxLayout() + self.reloadWITec = QtWidgets.QRadioButton('WITec True Match') + self.reloadWITec.setChecked(True) + self.reloadTxt = QtWidgets.QRadioButton('Ordered text file') + reloadBtn = QtWidgets.QPushButton('reload Results') + reloadBtn.clicked.connect(self.importResults) + reloadLayout.addWidget(self.reloadWITec) + reloadLayout.addWidget(self.reloadTxt) + reloadLayout.addWidget(reloadBtn) + reloadGroup.setLayout(reloadLayout) + + self.optionsGroup = QtWidgets.QGroupBox('Further Options') + optionsLayout = QtWidgets.QFormLayout() + + self.hqiSpinBox = QtWidgets.QDoubleSpinBox() + self.hqiSpinBox.setValue(75.0) + self.hqiSpinBox.setDecimals(1) + self.hqiSpinBox.setMinimum(0) + optionsLayout.addRow(QtWidgets.QLabel('min HQI:'), self.hqiSpinBox) + + self.compHqiSpinBox = QtWidgets.QDoubleSpinBox() + self.compHqiSpinBox.setValue(30.0) + self.compHqiSpinBox.setDecimals(1) + self.compHqiSpinBox.setMinimum(0) + self.compHqiSpinBox.setDisabled(True) + optionsLayout.addRow(QtWidgets.QLabel('min component HQI'), self.compHqiSpinBox) + + self.dispResultSpinBox = QtWidgets.QSpinBox() + self.dispResultSpinBox.setValue(20) + self.dispResultSpinBox.setMinimum(1) + self.dispResultSpinBox.valueChanged.connect(self.updateHistogram) + optionsLayout.addRow(QtWidgets.QLabel('Max. items in display:'), self.dispResultSpinBox) + + for spinbox in [self.hqiSpinBox, self.compHqiSpinBox]: + spinbox.setMaximum(100) + spinbox.setMaximumWidth(45) + + self.updateBtn = QtWidgets.QPushButton('Update Results') + self.updateBtn.setDisabled(True) + optionsLayout.addRow(self.updateBtn) + + self.optionsGroup.setLayout(optionsLayout) + self.optionsGroup.setMinimumWidth(175) + self.optionsGroup.setDisabled(True) + + self.resultScrollarea = QtWidgets.QScrollArea(self) + self.resultScrollarea.setFixedWidth(250) + self.resultScrollarea.setWidgetResizable(True) + + widget = QtWidgets.QWidget() + self.resultScrollarea.setWidget(widget) + self.layout_SArea = QtWidgets.QVBoxLayout(widget) + + self.resultCheckBoxes = QtWidgets.QGroupBox('Display Polymer Types:') + self.resultCheckBoxesLayout = QtWidgets.QVBoxLayout() + self.showTotalSelector = QtWidgets.QCheckBox('Show Total Distribution') + self.showTotalSelector.setChecked(True) + self.showTotalSelector.setDisabled(True) + self.resultCheckBoxesLayout.addWidget(self.showTotalSelector) + + self.resultCheckBoxesLayout.addStretch() + self.resultCheckBoxes.setLayout(self.resultCheckBoxesLayout) + + self.layout_SArea.addWidget(self.resultCheckBoxes) +# self.layout_SArea.addStretch(1) + + + self.exportbtn= QtWidgets.QPushButton('Export Results') + self.exportbtn.clicked.connect(self.exportData) + self.exportbtn.setDisabled(True) + + self.menuLayout.addWidget(reloadGroup) + self.menuLayout.addWidget(self.optionsGroup) + self.menuLayout.addWidget(self.resultScrollarea) + self.menuLayout.addWidget(self.exportbtn) + + self.layout.addLayout(self.menuLayout) + self.layout.addLayout(viewLayout) + +# update config, if present: + if self.parent is not None: + if self.config['minHQI'] is not None: + self.hqiSpinBox.setValue(self.config['minHQI']) + self.compHqiSpinBox.setValue(self.config['compHQI']) + + self.updateData() + + def loadSpectra(self, fname): + try: + return np.loadtxt(fname) + except: + return None + + def updateData(self): + self.spectraResults = self.parent.dataset.results['polymers'] + self.additiveResults = self.parent.dataset.results['additives'] + self.hqis = self.parent.dataset.results['hqis'] + self.addhqis = self.parent.dataset.results['additive_hqis'] + + self.colorSeed = self.parent.dataset.colorSeed + if type(self.colorSeed) != str: + self.colorSeed = 'default' + + #load Spectra + self.spectra = self.loadSpectra(os.path.join(self.parent.dataset.path, self.parent.dataset.name + '_000_Spec.Data 1.txt')) + if self.spectra is None: + fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Select Spectra File', self.parent.dataset.path, 'text file (*.txt)')[0] + self.spectra = self.loadSpectra(fname) + if self.spectra is None: + QtWidgets.QMessageBox.critical(self, 'ERROR!', 'spectra file could not be opened with np.loadtxt...') + return + self.spec_ax.set_xbound(100, (3400 if self.spectra[-1, 0] > 3400 else self.spectra[-1, 0])) + + self.loadParticleData() + + def loadParticleData(self): + self.particlestats = np.array(self.parent.dataset.particlestats) + pixelscale = (self.parent.dataset.pixelscale_df if self.parent.dataset.imagescanMode == 'df' else self.parent.dataset.pixelscale_bf) + #convert to mikrometer scale + for index in range(len(self.particlestats)): + for subindex in range(5): + self.particlestats[index][subindex] = self.particlestats[index][subindex] * pixelscale #multiply by pixelscale + if subindex == 4: + self.particlestats[index][subindex] = self.particlestats[index][subindex] * pixelscale #again for the area... + + self.particles2spectra = self.parent.dataset.particles2spectra + + sortindices = self.parent.dataset.ramanscansortindex + if self.particles2spectra is None: + print('creating default particles2spectra list') + #no assignment found, so we assume one measurement per particle and use ramanscansortindex for assignment + self.particles2spectra = [[int(np.where(sortindices == i)[0])] for i in range(len(sortindices))] + + #check, if dataset already contains results. Otherwise load them... + if self.spectraResults is None or (len(self.spectraResults) != len(sortindices)): + QtWidgets.QMessageBox.about(self, 'Info', 'No (or inconsistent) spectra results found, please run import dialog.') + else: + self.updateBtn.clicked.connect(self.formatResults) + self.formatResults() + + def importResults(self): + if self.reloadWITec.isChecked(): + self.importWindow = LoadWITecResults(self) + self.importWindow.show() + elif self.reloadTxt.isChecked(): + QtWidgets.QMessageBox.about(self, 'sorry...', 'Ordered Text import not yet implemented...') + + def getAdditivePlot(self, event): + clickedindex = int(np.round(event.xdata)) + polymer = self.typehistogram[clickedindex][0] #get the polymer name, that was clicked on + + if len(self.sorted_additives[clickedindex]) > 0: + self.additivePlot = AdditiveViewer(polymer, self.sorted_additives[clickedindex]) + self.additivePlot.show() + + def formatResults(self): + print('formatResults') + self.updateBtn.setDisabled(False) + self.optionsGroup.setDisabled(False) + del self.currentPolymers, self.currentAdditives + + #convert to arrays (makes indexing easier...) + self.currentPolymers, self.hqis = np.array(self.spectraResults), np.array(self.hqis) + + if self.additiveResults is not None: + self.currentAdditives, self.addhqis = np.array(self.additiveResults), np.array(self.addhqis) + self.compHqiSpinBox.setDisabled(False) + else: + self.currentAdditives = None + + #set poor HQI results to unknown + self.currentPolymers[self.hqis < self.hqiSpinBox.value()] = 'unknown' + + if self.currentAdditives is not None: + self.currentAdditives[self.addhqis < self.compHqiSpinBox.value()] = 'unknown' + + self.createHistogramData() + + def createHistogramData(self): + self.uniquePolymers = np.unique(self.currentPolymers) + self.particleResults = [None]*len(self.particlestats) + self.typehistogram = {i: 0 for i in self.uniquePolymers} + + assert len(self.particles2spectra) == len(self.particlestats), 'inconsistent data!!' + + for particleID, specList in enumerate(self.particles2spectra): + assignment = self.currentPolymers[specList[0]] #we take the first result as particle result. Hence, all spectra per particle have to have the same result + self.particleResults[particleID] = assignment + self.typehistogram[assignment] += 1 + + self.particleResults = np.array(self.particleResults) + + ##sort typehistogram, it will be converted into a list!! + self.typehistogram = sorted(self.typehistogram.items(), key = operator.itemgetter(1), reverse = True) + + self.uniquePolymers = [i[0] for i in self.typehistogram] + + self.indices = [] #what particles belong to which polymer type? + for polymer in self.uniquePolymers: + self.indices.append(list(np.where(self.particleResults == polymer)[0])) + + + ###generate additive array for each type in typehistogram: + if self.currentAdditives is None: + self.sorted_additives = None + else: + self.sorted_additives = [] + + for polymer in self.typehistogram: #get additives of each polymer type + self.sorted_additives.append(self.currentAdditives[np.where(self.currentPolymers == polymer[0])]) + for i in range(len(self.sorted_additives)): #sort out 'none' entries + nonentries = np.where(self.sorted_additives[i] == 'none') + self.sorted_additives[i] = np.delete(self.sorted_additives[i], nonentries) + + ###Handle Checkboxes for all polymers... + self.menuLayout.removeWidget(self.resultScrollarea) + for i in [self.resultCheckBoxes, self.resultCheckBoxesLayout, self.resultScrollarea, self.layout_SArea]: + i.setParent(None) + del i + + for i in self.polymerCheckBoxes: #remove present boxlabels + i.setParent(None) + del i + self.showTotalSelector.setParent(None) + self.showTotalSelector.setDisabled(False) + self.showTotalSelector.stateChanged.connect(self.updateHistogram) + + del self.resultCheckBoxes + del self.resultCheckBoxesLayout + del self.resultScrollarea + del self.layout_SArea + + self.resultScrollarea = QtWidgets.QScrollArea(self) + self.resultScrollarea.setFixedWidth(250) + self.resultScrollarea.setWidgetResizable(True) + + widget = QtWidgets.QWidget() + self.resultScrollarea.setWidget(widget) + self.layout_SArea = QtWidgets.QVBoxLayout(widget) + + self.resultCheckBoxes = QtWidgets.QGroupBox('Show Polymer Types:') + self.resultCheckBoxesLayout = QtWidgets.QVBoxLayout() + self.resultCheckBoxesLayout.addWidget(self.showTotalSelector) + #generate new checkboxes + self.polymerCheckBoxes = [] + for index, polymer in enumerate(self.uniquePolymers): + self.polymerCheckBoxes.append(QtWidgets.QCheckBox(self)) + self.polymerCheckBoxes[index].setText(polymer) + self.resultCheckBoxesLayout.addWidget(self.polymerCheckBoxes[index]) + if polymer in self.lastSelectedCheckBoxNames: + self.polymerCheckBoxes[index].setChecked(True) + + self.polymerCheckBoxes[index].stateChanged.connect(self.updateHistogram) + self.polymerCheckBoxes[index].stateChanged.connect(self.createPolymerOverlay) + + self.resultCheckBoxesLayout.addStretch() + self.resultCheckBoxes.setLayout(self.resultCheckBoxesLayout) + + self.layout_SArea.addWidget(self.resultCheckBoxes) + + self.menuLayout.addWidget(self.resultScrollarea) + + if self.currentAdditives is not None: + self.typeHistogramCanvas.setCursor(QtGui.QCursor(QtCore.Qt.WhatsThisCursor)) + self.typeHistogramCanvas.mpl_connect('button_press_event', self.getAdditivePlot) + + self.exportbtn.setDisabled(False) + + self.navigationGroup.setEnabled(True) + self.polymerComboBox.currentIndexChanged.disconnect() + self.polymerComboBox.clear() + self.polymerComboBox.addItems(self.uniquePolymers) + self.polymerComboBox.currentIndexChanged.connect(self.displayNewPolymerType) + + self.polymerIndex = self.polymerComboBox.currentIndex() + if self.lastSpectrumInFocus is not None: + self.currentSpectrumIndex = self.lastSpectrumInFocus + self.displayNewPolymerType(resetCurrentIndex=False) + else: + self.displayNewPolymerType() + self.updateHistogram() + self.createPolymerOverlay() + + + def exportData(self): + if self.expWindow is None: + self.expWindow = ExportDialog(self) + self.expWindow.show() + else: + self.expWindow.__init__(self) + self.expWindow.show() + + def updatePolymerSpectrum(self, centerOn=True, highlightContour=True): + specIndex = self.currentSpectrumIndex + self.spec_ax.axis("on") + self.spec_ax.clear() + self.spec_ax.plot(self.spectra[:, 0], self.spectra[:, specIndex+1]) + self.spec_ax.tick_params(axis='both', which='both', labelsize=15) + self.spec_ax.set_xlabel('Wavenumber (cm-1)', fontsize = 15) + self.spec_ax.set_ylabel('Counts', fontsize = 15) + self.spec_ax.set_title('ScanPoint Number {}, Size = {} µm'.format(specIndex+1, np.round(self.particlestats[self.currentParticleIndex][2], 1))) + self.spec_ax.figure.canvas.draw() + self.parent.centerOnRamanIndex(specIndex, centerOn=centerOn, highlightContour=highlightContour) + self.parent.highLightRamanIndex(specIndex) + self.lastSpectrumInFocus = specIndex + + def displayNewPolymerType(self, resetCurrentIndex=True): + self.polymerIndex = self.polymerComboBox.currentIndex() + self.particleSelector.setMaximum(len(self.indices[self.polymerIndex])) + if resetCurrentIndex: + self.particleSelector.setValue(1) + self.spectrumSelector.setValue(1) + self.spectrumSelector.setMaximum(len(self.particles2spectra[self.currentParticleIndex])) + self.currentParticleIndex = self.indices[self.polymerIndex][self.particleSelector.value()-1] + self.currentSpectrumIndex = self.particles2spectra[self.currentParticleIndex][self.spectrumSelector.value()-1] + self.updatePolymerSpectrum(centerOn=True) + else: + self.currentParticleIndex = self.indices[self.polymerIndex][self.particleSelector.value()-1] + self.currentSpectrumIndex = self.particles2spectra[self.currentParticleIndex][self.spectrumSelector.value()-1] + self.updatePolymerSpectrum(centerOn=False) + + def selectParticle(self, resetSpectrumCount=True): + if self.particles2spectra is not None: + self.currentParticleIndex = self.indices[self.polymerIndex][self.particleSelector.value()-1] + self.spectrumSelector.setMaximum(len(self.particles2spectra[self.currentParticleIndex])) + if resetSpectrumCount: + self.spectrumSelector.setValue(1) + self.currentSpectrumIndex = self.particles2spectra[self.currentParticleIndex][self.spectrumSelector.value()-1] + self.updatePolymerSpectrum() + else: + print('no spectrum assignment found...') + + def selectSpectrum(self): + if self.particles2spectra is not None: + self.currentSpectrumIndex = self.particles2spectra[self.currentParticleIndex][self.spectrumSelector.value()-1] + self.updatePolymerSpectrum() + + def updateHistogram(self): + self.sizeHist_ax.clear() + self.typeHist_ax.clear() + + self.typeHist_ax.axis('on') + self.sizeHist_ax.axis('on') + + #draw the general histogram + colorList = [] + if self.selOverlayBtn.isChecked(): + abundancyList = [] + for index, checkbox in enumerate(self.polymerCheckBoxes): + if checkbox.isChecked(): + abundancyList.append(self.typehistogram[index][1]) + curColor = self.getColorFromName(self.typehistogram[index][0], base255 = False) + colorList.append(curColor) + + else: + abundancyList = [i[1] for i in self.typehistogram] + for polymer in self.typehistogram: + curColor = self.getColorFromName(polymer[0], base255 = False) + colorList.append(curColor) + + self.typeHist_ax.barh(range(len(abundancyList)), abundancyList, color=colorList) + itemsInPlot = (len(abundancyList) if len(abundancyList) < self.dispResultSpinBox.value() else self.dispResultSpinBox.value()) + self.typeHist_ax.set_ylim([itemsInPlot, -1]) #plot in inverse order (have index 0 (highest abundancy) at top) + + ###add text labels + self.histPlotTextLabels = [] + y_label_position = 0 + for index, i in enumerate(self.typehistogram): + if not self.selOverlayBtn.isChecked() or self.polymerCheckBoxes[index].isChecked(): + if self.sorted_additives is None: + numads = '' + else: + numads = len(np.unique(self.sorted_additives[index])) + if numads == 0: + numads = '' + else: + numads = '(' + str(numads) + ')' + numpolymers = i[1] + label = ('{} x ' + self.typehistogram[index][0] + ' {}').format(numpolymers, numads) + x_label_position = self.typeHist_ax.get_xlim()[1]*0.05 + self.histPlotTextLabels.append(self.typeHist_ax.text(x_label_position, y_label_position, label, fontsize = 15, rotation = 0, verticalalignment = 'bottom')) + y_label_position += 1 + + for label in self.histPlotTextLabels: + pos = label.get_position() + curLimits = self.typeHist_ax.get_ylim() + if curLimits[1] < pos[1] < curLimits[0]: + label.set_alpha(1) + else: + label.set_alpha(0) + + self.typeHist_ax.set_title('Polymer Type Distribution', fontsize = 15) + self.typeHist_ax.tick_params(axis='y', which='both', left=False, labelleft=False) + self.typeHist_ax.tick_params(axis='both', which='both', labelsize=15) + self.typeHist_ax.set_ylabel('Polymer Type', fontsize = 15) + self.typeHist_ax.set_xlabel('Number', fontsize = 15) + + if len(self.typehistogram) > self.dispResultSpinBox.value(): + + def wheelScroll(event): + step = -0.05*event.step*self.dispResultSpinBox.value() + ymin, ymax = self.typeHist_ax.get_ylim() + if ymin > ymax: + ymin, ymax = ymax, ymin + self.typeHist_ax.set_ylim([ymax+step, ymin+step]) + + for label in self.histPlotTextLabels: + pos = label.get_position() + if ymin+step < pos[1] < ymax+step: + label.set_alpha(1) + else: + label.set_alpha(0) + self.typeHist_ax.figure.canvas.draw() + + self.typeHistogramCanvas.mpl_connect('scroll_event', wheelScroll) + + self.typeHist_ax.figure.canvas.draw() + + #general size histogram + self.bins = np.logspace(0.1, 3, 20) + self.sizes = [i[0] if np.isnan(i[2]) else i[2] for i in self.particlestats] #extract long size (if ellipse fit is nan -> box fit) + sizehist = np.histogram(self.sizes, self.bins) + self.totalhistx = [] + for i in range(19): + self.totalhistx.append(np.mean((sizehist[1][i], sizehist[1][i+1]))) + self.totalhisty = sizehist[0] + + self.sizeHist_ax.tick_params(axis='both', which='both', labelsize=15) + self.sizeHist_ax.set_xlabel('Size (µm)', fontsize = 15) + self.sizeHist_ax.set_ylabel('Number', fontsize = 15) + self.sizeHist_ax.set_xlim(3, 1100) + self.sizeHist_ax.figure.canvas.draw() + + if self.showTotalSelector.isChecked(): + self.sizeHist_ax.semilogx(self.totalhistx, self.totalhisty, label = 'total') + + #get selected boxes + selected = [] + for i in self.polymerCheckBoxes: + if i.isChecked() == True: + selected.append(i.text()) + + for i in selected: + sizes = [self.sizes[index] for index in range(len(self.sizes)) if self.currentPolymers[index] == i] + sizehist = np.histogram(sizes, self.bins) + self.sizeHist_ax.semilogx(self.totalhistx, sizehist[0], label = i, color = self.getColorFromName(i, base255 = False)) + + self.sizeHist_ax.legend(prop = {'size': 15}) + self.sizeHist_ax.tick_params(axis='both', which='both', labelsize=15) + self.sizeHist_ax.set_xlabel('Size (µm)', fontsize = 15) + self.sizeHist_ax.set_ylabel('Number', fontsize = 15) + self.sizeHist_ax.set_xlim(3, 1100) + self.sizeHist_ax.figure.canvas.draw() + + self.lastSelectedCheckBoxNames = [checkbox.text() for checkbox in self.polymerCheckBoxes if checkbox.isChecked()] + + + def darkenBackground(self): + self.parent.darkenPixmap = self.darkenBtn.isChecked() + + if self.darkenBtn.isChecked(): + self.parent.scene().setBackgroundBrush(QtGui.QColor(5, 5, 5)) + self.parent.item.setOpacity(0.2) + else: + self.parent.scene().setBackgroundBrush(QtCore.Qt.darkGray) + self.parent.item.setOpacity(1) + + def updateColorSeed(self): + text, ok = QtWidgets.QInputDialog.getText(self, 'Color Seed', 'Enter New Seed here', text=self.colorSeed) + if ok: + self.colorSeed = text + self.parent.dataset.colorSeed = text + self.updateHistogram() + self.createPolymerOverlay() + + def getColorFromName(self, name, base255=True): + random.seed(self.colorSeed + name) + hue = random.random() + random.seed((self.colorSeed + name)*2) + saturation = random.random()/4 + 0.75 #i.e., between 0.75 and 1 + random.seed((self.colorSeed + name)*3) + value = random.random()/5 + 0.8 #i.e., between 0.8 and 1 + color = colorsys.hsv_to_rgb(hue, saturation, value) + if base255: + color = list(color) + for i in range(3): + color[i] = np.round(color[i]*255) + color = tuple(color) + return color + + def createPolymerOverlay(self): + if not self.noOverlayBtn.isChecked() and self.indices is not None: + if len(self.indices) > 0: +# ramansortindices = self.parent.dataset.ramanscansortindex + + alpha = (128 if self.transpBtn.isChecked() else 255) + #get colors for each polymer type + colorList = [QtGui.QColor(255, 255, 255, alpha=50)]*len(self.particleResults) + legendItems = [] + + for index, indexList in enumerate(self.indices): + if self.fullOverlayBtn.isChecked() or (self.selOverlayBtn.isChecked() and self.polymerCheckBoxes[index].isChecked()): + color = self.getColorFromName(self.uniquePolymers[index], base255=True) + color = QtGui.QColor(color[0], color[1], color[2], alpha=alpha) + legendItems.append((self.uniquePolymers[index], color)) + for i in indexList: +# colorList[ramansortindices[i]] = color + colorList[i] = color + + self.parent.contouritem.colorList = colorList + self.parent.contouritem.update() + + self.parent.imparent.legend.items = legendItems + self.parent.imparent.legend.update() + self.parent.imparent.legend.show() + + else: + self.parent.contouritem.colorList = [] + self.parent.contouritem.update() + self.parent.imparent.legend.items = [] + self.parent.imparent.legend.update() + self.parent.imparent.legend.hide() + + def show_hide_labels(self): + hidden = self.hideLabelBtn.isChecked() + for scanIndicator in self.parent.ramanscanitems: + scanIndicator.hidden = hidden + scanIndicator.update() + + def saveAnalysisResults(self): + self.parent.dataset.results = {'polymers': self.spectraResults, + 'hqis': self.hqis, + 'additives': self.additiveResults, + 'additive_hqis': self.addhqis} + + self.parent.dataset.resultParams = {'minHQI': self.hqiSpinBox.value(), + 'compHQI': self.compHqiSpinBox.value()} +# 'dispResults': self.dispResultSpinBox.value()} + self.parent.dataset.save() + print('saved dataset') + + + def closeEvent(self, event): + for window in [self.expWindow, self.additivePlot, self.importWindow]: + try: window.close() + except: pass + self.saveAnalysisResults() + event.accept() + + +class ExportDialog(QtWidgets.QWidget): + def __init__(self, parent): + super(ExportDialog, self).__init__() + self.setWindowTitle('Export Options') + self.setGeometry(200,200, 300, 300) + + self.parent = parent + self.particles = self.parent.particlestats + self.polymers = self.parent.particleResults + self.additives = self.parent.currentAdditives + self.hqis = self.parent.hqis + + self.layout = QtWidgets.QHBoxLayout() + self.setLayout(self.layout) + + excelvbox = QtWidgets.QVBoxLayout() + excelvbox.addWidget(QtWidgets.QLabel('Select Parameters for Export')) + excelgroup = QtWidgets.QGroupBox("Export to Excel", self) + + self.exportOptions = ['Polymer Type (mandatory)', 'Additives', 'Long Size (µm)', 'Short Size (µm)', 'Area (µm²)', 'HQI', 'Size Classes'] + self.checkBoxes = [] + self.sizeClasses = [5, 10, 20, 50, 100, 1e6] + self.directory = self.parent.parent.dataset.path + + for index, option in enumerate(self.exportOptions): + self.checkBoxes.append(QtWidgets.QCheckBox(self)) + self.checkBoxes[-1].setText(option) + self.checkBoxes[-1].setChecked(True) + + if option == 'Polymer Type (mandatory)': + self.checkBoxes[-1].setEnabled(False) #is mandatory!!! + + if option == 'Additives': + if self.additives is None: + self.checkBoxes[-1].setEnabled(False) + self.checkBoxes[-1].setChecked(False) + + excelvbox.addWidget(self.checkBoxes[-1]) + + self.xlsFileName = QtWidgets.QLineEdit() + self.xlsFileName.setText('{}_Particle_List'.format(self.parent.parent.dataset.name)) + excelvbox.addWidget(QtWidgets.QLabel('Filename:')) + excelvbox.addWidget(self.xlsFileName) + + self.exlbtn = QtWidgets.QPushButton('Export to Excel') + self.exlbtn.resize(self.exlbtn.sizeHint()) + self.exlbtn.clicked.connect(self.toExcel) + + excelvbox.addWidget(self.exlbtn) + excelgroup.setLayout(excelvbox) + + self.layout.addWidget(excelgroup) + + sqlGroup = QtWidgets.QGroupBox('Export to SQL') + sqlLayout = QtWidgets.QVBoxLayout() + + self.sqlbtn = QtWidgets.QPushButton('Export to SQL Database') + self.sqlbtn.resize(self.sqlbtn.sizeHint()) + self.sqlbtn.clicked.connect(self.toSQL) + + self.sqlExport = None + + sqlLayout.addWidget(self.sqlbtn) + sqlGroup.setLayout(sqlLayout) + + self.layout.addWidget(sqlGroup) + self.show() + + def toExcel(self): + requiredcolumns = [] + self.sizes = np.round(np.array([i[0] if np.isnan(i[2]) else i[2] for i in self.particles]), 1) + for box in self.checkBoxes: + if box.isChecked() == True: + if box.text() != 'Size Classes': + requiredcolumns.append(box.text()) + if box.text() == 'Long Size (µm)': + longSize = self.sizes + elif box.text() == 'Short Size (µm)': + shortSize = np.round(np.array([i[1] if np.isnan(i[3]) else i[3] for i in self.particles]), 1) + elif box.text() == 'Area (µm²)': + area = np.array([np.round(float(entry[4]), 1) for entry in self.particles]) + else: + requiredcolumns.append('0 - 5 µm') + requiredcolumns.append('5 - 10 µm') + requiredcolumns.append('10 - 20 µm') + requiredcolumns.append('20 - 50 µm') + requiredcolumns.append('50 - 100 µm') + requiredcolumns.append('> 100 µm') + + self.finalData = np.array(['nothing to see here, iam a boring placeholder']*((self.polymers.shape[0]+1)*len(requiredcolumns))).reshape(((self.polymers.shape[0]+1), len(requiredcolumns))) + + #create header: + self.finalData[0, :] = np.array(requiredcolumns) + self.finalData[0, 0] = 'Polymer Type' + + rowindex = 1 + for polymer in np.unique(self.polymers): + indices = np.where(self.polymers == polymer)[0] + numentries = int(len(indices)) + + for colindex, column in enumerate(requiredcolumns): + if column == 'Polymer Type (mandatory)': + self.finalData[rowindex:rowindex+numentries, colindex] = self.polymers[indices] + if column == 'Additives': + self.finalData[rowindex:rowindex+numentries, colindex] = self.additives[indices] + if column == 'Long Size (µm)': + self.finalData[rowindex:rowindex+numentries, colindex] = longSize[indices] + if column == 'Short Size (µm)': + self.finalData[rowindex:rowindex+numentries, colindex] = shortSize[indices] + if column == 'Area (µm²)': + self.finalData[rowindex:rowindex+numentries, colindex] = area[indices] + if column == 'HQI': + self.finalData[rowindex:rowindex+numentries, colindex] = self.hqis[indices] + + if '> 100 µm' in requiredcolumns: + ##append size classes + numPrevCols = len(requiredcolumns) - 6 #number of previous columns + for tableindex, dataindex in enumerate(indices): + for classindex in range(len(self.sizeClasses)): + upLimit = self.sizeClasses[classindex] + if classindex == 0: lowLimit = 0 + else: lowLimit = self.sizeClasses[classindex-1] + curSize = self.sizes[dataindex] + + if curSize > lowLimit and curSize <= upLimit: + self.finalData[rowindex+tableindex, numPrevCols + classindex] = np.int(1) + else: + self.finalData[rowindex+tableindex, numPrevCols + classindex] = np.int(0) + + rowindex = rowindex + numentries + + #dump into excel file + xlsname = self.directory + '//' + self.xlsFileName.text() + '.xlsx' + print('exporting excel to:\n file name: {} in directory: {}'.format(self.xlsFileName.text(), self.directory)) + validFileName = False + incr = 1 + while not validFileName: + if not os.path.exists(xlsname): + validFileName = True + else: + xlsname = self.directory + self.xlsFileName.text() + ' {}.xlsx'.format(incr) + incr += 1 + + writer = pd.ExcelWriter(xlsname, engine = 'xlsxwriter') + + df = pd.DataFrame(self.finalData) + df.to_excel(writer, sheet_name = 'Individual Particles', header = False, index = False) + if '> 100 µm' in requiredcolumns: + #generate particle statistics report + report = [['Polymer Type', '0 - 5 µm', '5 - 10 µm', '10 - 20 µm', '20 - 50 µm', '50 - 100 µm', '> 100 µm']] + + for polymer in np.unique(self.polymers): + indices = np.where(self.polymers == polymer)[0] + classes = np.array([0, 0, 0, 0, 0, 0]) + for size in self.sizes[indices]: + if size < 5: classes[0] += 1 + elif size < 10: classes[1] += 1 + elif size < 20: classes[2] += 1 + elif size < 50: classes[3] += 1 + elif size < 100: classes[4] += 1 + else: classes[5] += 1 + report.append([polymer, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5]]) + + report = pd.DataFrame(np.array(report)) + report.to_excel(writer, sheet_name = 'Particle Statistics', header = False, index = False) + QtWidgets.QMessageBox.about(self, 'Done!', 'Particle Data exported') + + def toSQL(self): + self.sqlExport = SQLExport(self.parent) + self.sqlExport.show() + + def closeEvent(self, event): + if self.sqlExport is not None: + self.sqlExport.close() + event.accept() + + +class AdditiveViewer(QtWidgets.QWidget): + def __init__(self, polymername, sortedAdditives): + super(AdditiveViewer, self).__init__() + self.setGeometry(200,200, 800, 600) + self.setWindowTitle('Additives of {}'.format(polymername)) + + self.layout = QtWidgets.QGridLayout() + self.setLayout(self.layout) + + self.canvas = FigureCanvas(Figure(figsize=(5, 3))) + self.ax = self.canvas.figure.subplots() + + self.layout.addWidget(self.canvas, 0, 0) + + self.ax.hist(sortedAdditives) + self.ax.set_ylabel('Number', fontsize = 15) + self.ax.tick_params(axis='both', which='both', labelsize=15) + + +if __name__ == '__main__': + def run(): + app = QtWidgets.QApplication(sys.argv) + meas = ParticleAnalysis(None) + meas.showMaximized() + ret = app.exec_() + + run() \ No newline at end of file diff --git a/analysis/editParticles.py b/analysis/editParticles.py new file mode 100644 index 0000000..430b7bf --- /dev/null +++ b/analysis/editParticles.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Jan 16 12:43:00 2019 + +@author: brandt +""" + +""" +GEPARD - Gepard-Enabled PARticle Detection +Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für +Polymerforschung Dresden e. V. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program, see COPYING. +If not, see . +""" +import numpy as np +import cv2 +#import matplotlib.pyplot as plt + +class ParticleEditor(object): + def __init__(self, parent): + self.parent = parent #the assigned analysis widget + + def createSafetyBackup(self): + self.parent.parent.dataset.saveBackup() + + def combineParticles(self, contourIndices, new_assignment): + contourIndices = sorted(contourIndices) #we want to keep the contour with lowest index + print('selected contours:', contourIndices) + self.createSafetyBackup() + #get contours: + contours = [self.parent.parent.dataset.particlecontours[i] for i in contourIndices] + cnt = np.vstack(tuple(contours)) #combine contous + + #draw contours + xmin, xmax = cnt[:,0,:][:, 0].min(), cnt[:,0,:][:, 0].max() + ymin, ymax = cnt[:,0,:][:, 1].min(), cnt[:,0,:][:, 1].max() + + padding = 2 #pixel in each direction + rangex = int(np.round((xmax-xmin)+2*padding)) + rangey = int(np.round((ymax-ymin)+2*padding)) + + for i in range(len(cnt)): + cnt[i][0][0] -= xmin-padding + cnt[i][0][1] -= ymin-padding + + img = np.zeros((rangey, rangex)) + cv2.drawContours(img, [cnt], 0, 1, -1) + cv2.drawContours(img, [cnt], 0, 1, 1) + img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3)))) + + temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + + newContour = contours[0] + stats = self.characterizeParticle(newContour) + + for i in range(len(newContour)): + newContour[i][0][0] += xmin-padding + newContour[i][0][1] += ymin-padding + + + #check, if dataset contains (already modified) particle2spectra, otherwise create new. + if self.parent.parent.dataset.particles2spectra is None: #create default assignment + print('recreating particles2spectra from within edit particles...') + sortindices = self.parent.parent.dataset.ramanscansortindex + self.parent.parent.dataset.particles2spectra = [[int(np.where(sortindices == i)[0])] for i in range(len(sortindices))] + + + #Contour indices are the same as the original particlestats, which are contained in the dataset. + #We have to modify that and reload in the analysisview + #first, overwrite first index with new particlestats + self.parent.parent.dataset.particlestats[contourIndices[0]] = stats + + #now, delete the rest... + self.parent.parent.dataset.particlestats = [i for ind, i in enumerate(self.parent.parent.dataset.particlestats) if ind not in contourIndices[1:]] + + #same with the contours + self.parent.parent.dataset.particlecontours[contourIndices[0]] = newContour + self.parent.parent.dataset.particlecontours = [i for ind, i in enumerate(self.parent.parent.dataset.particlecontours) if ind not in contourIndices[1:]] + + + #update particle2spectra_list + #what is the current particle index?? + specIndices = [] + #other spectra indices: + for index in contourIndices: + specIndices.append(self.parent.particles2spectra[index]) + + #flatten index list (in case, that a nested list was created...) + specIndices = list(np.unique(np.array(specIndices))) + for i in specIndices: + self.parent.spectraResults[i] = new_assignment + self.parent.hqis[i] = 100 #avoid sorting them out again by hqi-filter... + print(f'spectrum {i} of particle{contourIndices[0]} is now {new_assignment}') + + + #modify particles2spectra.. + self.parent.parent.dataset.particles2spectra[contourIndices[0]] = specIndices + for index in reversed(contourIndices[1:]): + print('removing index from particles2spectra:', index) + del self.parent.parent.dataset.particles2spectra[index] + + #save dataset + self.parent.parent.dataset.save() + + #update contours in sampleview + self.parent.parent.contouritem.resetContours(self.parent.parent.dataset.particlecontours) + + self.parent.loadParticleData() + + + def reassignParticles(self, contourindices, new_assignment): + self.createSafetyBackup() + for partIndex in contourindices: + for specIndex in self.parent.particles2spectra[partIndex]: + self.parent.currentPolymers[specIndex] = new_assignment + self.parent.spectraResults[specIndex] = new_assignment + self.parent.hqis[specIndex] = 100 + + self.parent.createHistogramData() + + + def deleteParticles(self): + self.createSafetyBackup() + pass + + def splitParticles(self): + self.createSafetyBackup() + pass + + def characterizeParticle(self, contours): + ##characterize particle + longellipse, shortellipse = np.nan, np.nan + + cnt = contours + + if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... + ellipse = cv2.fitEllipse(cnt) + shortellipse, longellipse = ellipse[1] + + rect = cv2.minAreaRect(cnt) + long, short = rect[1] + if short>long: + long, short = short, long + + return long, short, longellipse, shortellipse, cv2.contourArea(cnt) + + +#if __name__ == '__main__': +# import \ No newline at end of file diff --git a/analysis/loadresults.py b/analysis/loadresults.py new file mode 100644 index 0000000..99d9a38 --- /dev/null +++ b/analysis/loadresults.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu May 31 10:07:45 2018 + +@author: brandt +""" + +import numpy as np +from PyQt5 import QtWidgets +import sys +from os import chdir, getcwd + +class LoadWITecResults(QtWidgets.QDialog): + def __init__(self, parent): + super(LoadWITecResults, self).__init__() + self.setGeometry(400, 400, 200, 300) + self.setWindowTitle('Get Truematch Results') + self.layout = QtWidgets.QGridLayout() + self.setLayout(self.layout) + + self.parent = parent + self.parent.setDisabled(True) + self.trueMatchResults = None + self.polymertypes = None + self.additives = None + self.hqis = None + self.addhqis = None + + self.btn1 = QtWidgets.QPushButton('LoadTrueMatchResults') + self.btn1.resize(self.btn1.sizeHint()) + self.btn1.clicked.connect(self.loadFileManually) + + optionsLayout = QtWidgets.QFormLayout() + self.optionsGroup = QtWidgets.QGroupBox('Compute Options') + self.optionsGroup.setDisabled(True) + self.btn3 = QtWidgets.QPushButton('Compute') + self.btn3.clicked.connect(self.runCalculations) + + self.label1 = QtWidgets.QLabel('HQI-Threshold:') + self.spinbox1 = QtWidgets.QDoubleSpinBox(self) + self.spinbox1.valueChanged.connect(self.updateParentSpinboxes) + optionsLayout.addRow(self.label1, self.spinbox1) + + self.label2 = QtWidgets.QLabel('ComponentThreshold:') + self.spinbox2 = QtWidgets.QDoubleSpinBox(self) + self.spinbox2.valueChanged.connect(self.updateParentSpinboxes) + optionsLayout.addRow(self.label2, self.spinbox2) + self.label3 = QtWidgets.QLabel('Max Items in Display:') + self.numDispSpinbox = QtWidgets.QSpinBox(self) + self.numDispSpinbox.setValue(20) + self.numDispSpinbox.setMinimum(1) + self.numDispSpinbox.setMaximum(40) + self.numDispSpinbox.valueChanged.connect(self.updateParentSpinboxes) + optionsLayout.addRow(self.label3, self.numDispSpinbox) + optionsLayout.addRow(self.btn3) + + for box in [self.spinbox1, self.spinbox2]: + box.setValue(50) + box.setDecimals(1) + box.setSingleStep(5) + box.setMinimum(0) + box.setMaximum(100) + self.spinbox2.setValue(30) #more reasonable... + + self.optionsGroup.setLayout(optionsLayout) + + self.reviewGroup = QtWidgets.QGroupBox('Review Changes') + self.reviewGroup.setDisabled(True) + reviewLayout = QtWidgets.QVBoxLayout() + + self.otherBtn = QtWidgets.QPushButton('manual overwrite') + self.otherBtn.clicked.connect(self.show3FlagsReview) + reviewLayout.addWidget(self.otherBtn) + reviewLayout.addStretch() + self.reviewGroup.setLayout(reviewLayout) + + self.layout.addWidget(self.btn1, 0, 0) + self.layout.addWidget(self.optionsGroup, 1, 0) + self.layout.addWidget(self.reviewGroup, 2, 0) + + self.manualPolymers = {} + self.manualAdditives = {} + self.expWindow = None + self.additivePlot = None + self.editEntryWindow = None + + self.numFlagForTakeSpectrum = 1 + self.numFlagForSetUnknown = 2 + self.numFlagForPrompt = 3 + + def updateParentSpinboxes(self): + self.parent.hqiSpinBox.setValue(self.spinbox1.value()) + self.parent.compHqiSpinBox.setValue(self.spinbox2.value()) + self.parent.dispResultSpinBox.setValue(self.numDispSpinbox.value()) + + def show3FlagsReview(self): + self.editEntryWindow = ModifyManualEdits(self, self.manualPolymers, self.manualAdditives) + self.editEntryWindow.show() + + def loadFileManually(self): + dsetpath = self.parent.parent.dataset.path + fnames =QtWidgets.QFileDialog.getOpenFileNames(self, 'Select TrueMatch result file', dsetpath, 'text file (*.txt)')[0] + if len(fnames) > 1: + QtWidgets.QMessageBox.about(self, 'Info', 'The following order of files was loaded. If incorrect, please call a coder!\n{}'.format('\n'.join([fname for fname in fnames]))) + self.trueMatchResults = [] + for fileindex, fname in enumerate(fnames): + with open(fname) as file: + if fileindex == 0: + for line in file: + self.trueMatchResults.append(line) + else: ##for additional files skip first line (header..) + for lineindex, line in enumerate(file): + if lineindex > 0: + self.trueMatchResults.append(line) + + self.btn1.setText('Data loaded') + self.optionsGroup.setDisabled(False) + + def formatResults(self, rawResults): #get rid of header line, first data interpretation + results = [] + for index,line in enumerate(rawResults): + if index == 0: + line = line.strip().split(';')[:-1] #disregard the last entry of each line, as each line ends with ; <- that produces an empty entry... + if line[0] != 'Search Spectrum Name': + print('incompatible data format') + break + #detect, whether one- or multicomponent-search was done + if line[-1] == 'IsMarked': + numhits = np.int(line[-2].split(' ')[-1]) + numcomps = 1 + else: + numhits = np.int(line[-1].split(' ')[-1]) + numcomps = np.int(line[-1].split(' ')[-3]) + else: + results.append(line) + + numspectra = len(results) + if numspectra > 0: + print('{} components, {} hits per sample, {} spectra'.format(numcomps, numhits, numspectra)) + + return results, numspectra, numcomps, numhits + + def interpretEntry(self, index, entry, numhits, numcomps): + entry = entry.split(';') + polymertype, additive, hqi, addhqi = None, None, None, None #assign default None + + #find yes-flags + flags = np.where(np.array(entry) == 'yes')[0] + if len(flags) == 0: + #take highest HQI entry + if numcomps == 1: + if float(entry[1]) > self.spinbox1.value(): + polymertype = entry[2] + hqi = entry[1] + else: + polymertype = 'unknown' + hqi = 0 + else: + if float(entry[1]) > self.spinbox1.value(): + polymertype = entry[5] + hqi = entry[1] + if float(entry[6]) > self.spinbox2.value(): + additive = entry[7] + addhqi = entry[6] + else: + additive = 'none' + addhqi = 0 + + else: + polymertype = 'unknown' + additive = 'none' + hqi = 0 + addhqi = 0 + + elif len(flags) == 1: + #exactly one flag was placed, take this entry + if numcomps == 1: + polymertype = entry[int(flags[0])-1] + hqi = 100 + else: + polymertype = entry[int(flags[0])+2] + additive = entry[int(flags[0])+4] + hqi = 100 + addhqi = 100 + + elif len(flags) == 2: + polymertype = 'unknown' + hqi = 0 + if numcomps > 1: + additive = 'none' + addhqi = 0 + + elif len(flags) == 3: + + hqi = 100 + if index not in self.manualPolymers: + polymertype, ok = QtWidgets.QInputDialog.getText(self, 'Name of main component', 'Spectrum at index {} is:'.format(index)) + self.manualPolymers[index] = polymertype + else: + polymertype = self.manualPolymers[index] + if numcomps > 1: + addhqi = 100 + if entry[0] not in self.manualAdditives: + additive, ok = QtWidgets.QInputDialog.getText(self, 'Name of additive', 'Additive at index {} is:'.format(index)) + self.manualAdditives[index] = additive + else: + additive = self.manualAdditives[index] + else: + QtWidgets.QMessageBox.about(self, 'Error!', 'No rule for {} flags, found at spectrum index {}'.format(len(flags), index)) + + if addhqi is None: + addhqi = 0 + + return polymertype, additive, float(hqi), float(addhqi) + + def runCalculations(self): + self.resultList, numspectra, numcomps, numhits = self.formatResults(self.trueMatchResults) + self.dispresults = self.numDispSpinbox.value() + + self.polymertypes =[] + self.hqis = [] + + if numcomps == 1: #####SINGLE COMPONENT SEARCH + self.additives = None + self.addhqis = None + self.spinbox2.setEnabled(False) + for index, entry in enumerate(self.resultList): + if len(entry) == 0: + del self.resultList[index] + else: + polymertype, additive, hqi, addhqi = self.interpretEntry(index, entry, numhits, numcomps) + self.polymertypes.append(polymertype) + self.hqis.append(hqi) + + else: #####MULTI-COMPONENT SEARCH + self.additives = [] + self.addhqis = [] + + for index, entry in enumerate(self.resultList): + if len(entry) > 0: + polymertype, additive, hqi = self.interpretEntry(index, entry, numhits, numcomps) + self.polymertypes.append(polymertype) + self.hqis.append(hqi) + self.additives.append(additive) + self.addhqis.append(addhqi) + + assert len(self.polymertypes) == len(self.resultList), 'incorrect number of polymer types added...' + + del self.parent.spectraResults, self.parent.additiveResults, self.parent.hqis, self.parent.addhqis + self.parent.spectraResults = self.polymertypes + self.parent.additiveResults = self.additives + self.parent.hqis = self.hqis + self.parent.addhqis = self.addhqis + self.parent.formatResults() + + if len(self.manualPolymers) > 0: + self.reviewGroup.setDisabled(False) + + def closeEvent(self, event): + del self.parent.spectraResults, self.parent.additiveResults, self.parent.hqis, self.parent.addhqis + self.parent.spectraResults = self.polymertypes + self.parent.additiveResults = self.additives + self.parent.hqis = self.hqis + self.parent.addhqis = self.addhqis + self.parent.updateBtn.clicked.connect(self.parent.formatResults) + self.parent.formatResults() + self.parent.show_hide_labels() + self.parent.saveAnalysisResults() + self.parent.setEnabled(True) + event.accept() + + +class ModifyManualEdits(QtWidgets.QWidget): + def __init__(self, parentWindow, polymerEdits, additiveEdits = None): + super(ModifyManualEdits, self).__init__() + self.setWindowTitle('Edit Manual Changes') + self.setGeometry(200, 200, 800, 500) + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + self.parent = parentWindow + self.polymerEdits = polymerEdits + self.additiveEdits = additiveEdits + self.labels = [] + self.polymerLineEdits = [] + self.additiveLineEdits = [] + + groupBox = QtWidgets.QGroupBox('Manually entered edits') + groupLayout = QtWidgets.QGridLayout() + + #create Labels and LineEdits + for index in self.polymerEdits: + self.labels.append(QtWidgets.QLabel('Spectrum index:' + str(index))) + print(self.labels[-1].text()) + self.polymerLineEdits.append(QtWidgets.QLineEdit()) + self.polymerLineEdits[-1].setText(self.polymerEdits[index]) + if len(self.additiveEdits) > 0: + self.additiveLineEdits.append(QtWidgets.QLineEdit()) + self.additiveLineEdits[-1].setText(self.additiveEdits[index]) + + for i in range(len(self.labels)): + groupLayout.addWidget(self.labels[i], i, 0) + groupLayout.addWidget(self.polymerLineEdits[i], i, 1) + if len(self.additiveEdits) > 0: + groupLayout.addWidget(self.additiveLineEdits[i], i, 2) + + groupBox.setLayout(groupLayout) + + scrollarea = QtWidgets.QScrollArea() + scrollarea.setWidget(groupBox) + scrollarea.setMaximumHeight(600) + layout.addWidget(scrollarea) + + saveAndCloseBtn = QtWidgets.QPushButton('Save and Exit') + saveAndCloseBtn.clicked.connect(self.closeEvent) + layout.addWidget(saveAndCloseBtn) + + def closeEvent(self, event): + #read out LineEdits: + for index, key in enumerate(self.polymerEdits): + self.polymerEdits[key] = self.polymerLineEdits[index].text() + if len(self.additiveEdits) > 0: + self.additiveEdits[key] = self.additiveLineEdits[index].text() + + #copy values to parent Window + self.parent.manualPolymers, self.parent.manualAdditives = self.polymerEdits, self.additiveEdits + self.parent.runCalculations() + self.close() + + +if __name__ == "__main__": + + wd=getcwd() + chdir(wd) + + try: + del(app) + except: + pass + + app = QtWidgets.QApplication(sys.argv) + mainWin = LoadWITecResults(None) + mainWin.show() + app.exec_() + + diff --git a/dataset.py b/dataset.py index 4ac7cb0..6833a54 100644 --- a/dataset.py +++ b/dataset.py @@ -25,7 +25,7 @@ import cv2 from helperfunctions import cv2imread_fix, cv2imwrite_fix from copy import copy -currentversion = 1 +currentversion = 2 def loadData(fname): retds = None @@ -60,8 +60,11 @@ class DataSet(object): self.version = currentversion self.lastpos = None self.maxdim = None - self.pixelscale = None # µm / pixel - self.imagedim = None # width, height, angle + self.pixelscale_df = None # µm / pixel --> scale of DARK FIELD camera (used for image stitching) + self.pixelscale_bf = None # µm / pixel of DARK FIELD camera (set to same as bright field, if both use the same camera) + self.imagedim_bf = None # width, height, angle of BRIGHT FIELD camera + self.imagedim_df = None # width, height, angle of DARK FIELD camera (set to same as bright field, if both use the same camera) + self.imagescanMode = 'df' #was the fullimage acquired in dark- or brightfield? self.fitpoints = [] # manually adjusted positions aquired to define the specimen geometry self.fitindices = [] # which of the five positions in the ui are already known self.boundary = [] # scan boundary computed by a circle around the fitpoints + manual adjustments @@ -92,6 +95,18 @@ class DataSet(object): self.ramanscansortindex = None self.ramanscandone = False + self.results = {'polymers': None, + 'hqis': None, + 'additives': None, + 'additive_hqis': None} + + self.resultParams = {'minHQI': None, + 'compHQI': None} + + self.particles2spectra = None #links idParticle to corresponding idSpectra (i.e., first measured particle (ID=0) is linked to spectra indices 0 and 1) + self.colorSeed = 'default' + self.resultsUploadedToSQL = [] + self.readin = True # a value that is always set to True at loadData # and mark that the coordinate system might be changed in the meantime self.mode = "prepare" @@ -136,7 +151,7 @@ class DataSet(object): zvalimg = None Ngrid = len(self.grid) - width, height, rotationvalue = self.imagedim + width, height, rotationvalue = self.imagedim_df p0, p1 = self.maxdim[:2], self.maxdim[2:] for i in range(Ngrid): print(f"Processing image {i+1} of {Ngrid}") @@ -154,6 +169,23 @@ class DataSet(object): del self.particleimgs self.version = 1 + + + if self.version == 1: + print("Converting legacy version 1 to 2") + if hasattr(self, 'pixelscale'): + print('pixelscale was', self.pixelscale) + self.pixelscale_bf = self.pixelscale + self.pixelscale_df = self.pixelscale +# del self.pixelscale + + if hasattr(self, 'imagedim'): + self.imagedim_bf = self.imagedim + self.imagedim_df = self.imagedim +# del self.imagedim + + self.version = 2 + # add later conversion for higher version numbers here def getSubImage(self, img, index, draw=True): @@ -176,24 +208,42 @@ class DataSet(object): assert self.heightmap is not None return self.heightmap[0]*x + self.heightmap[1]*y + self.heightmap[2] - def mapToPixel(self, p, force=False): + def mapToPixel(self, p, mode='df', force=False): if not force: assert not self.readin p0 = copy(self.lastpos) - p0[0] -= self.imagedim[0]/2 - p0[1] += self.imagedim[1]/2 - return (p[0] - p0[0])/self.pixelscale, (p0[1] - p[1])/self.pixelscale + + if mode == 'df': + p0[0] -= self.imagedim_df[0]/2 + p0[1] += self.imagedim_df[1]/2 + return (p[0] - p0[0])/self.pixelscale_df, (p0[1] - p[1])/self.pixelscale_df + + elif mode == 'bf': + p0[0] -= self.imagedim_bf[0]/2 + p0[1] += self.imagedim_bf[1]/2 + return (p[0] - p0[0])/self.pixelscale_bf, (p0[1] - p[1])/self.pixelscale_bf + else: + print('mapToPixelMode not understood') + return - def mapToLength(self, pixelpos, force=False): + def mapToLength(self, pixelpos, mode='df', force=False): if not force: assert not self.readin p0 = copy(self.lastpos) - p0[0] -= self.imagedim[0]/2 - p0[1] += self.imagedim[1]/2 - return (pixelpos[0]*self.pixelscale + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale) + if mode == 'df': + p0[0] -= self.imagedim_df[0]/2 + p0[1] += self.imagedim_df[1]/2 + return (pixelpos[0]*self.pixelscale_df + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale_df) + elif mode == 'bf': + p0[0] -= self.imagedim_bf[0]/2 + p0[1] += self.imagedim_bf[1]/2 + return (pixelpos[0]*self.pixelscale_bf + p0[0]), (p0[1] - pixelpos[1]*self.pixelscale_bf) + else: + print('mapToRamanMode not understood') + return - def mapToLengthRaman(self, pixelpos, noz=False): - p0x, p0y = self.mapToLength(pixelpos) + def mapToLengthRaman(self, pixelpos, microscopeMode='df', noz=False): + p0x, p0y = self.mapToLength(pixelpos, mode = microscopeMode) x, y = p0x + self.pshift[0], p0y + self.pshift[1] z = None if not noz: @@ -223,8 +273,8 @@ class DataSet(object): self.name = os.path.splitext(os.path.basename(self.fname))[0] def getImageName(self): - return os.path.join(self.path, "fullimage.tif") - + return os.path.join(self.path, 'fullimage.tif') + def getZvalImageName(self): return os.path.join(self.path, "zvalues.tif") @@ -241,20 +291,38 @@ class DataSet(object): return os.path.join(self.path, "tmp.bmp") def saveParticleData(self): - if len(self.ramanscansortindex)>0: - data = [] - for i in self.ramanscansortindex: - data.append(list(self.ramanpoints[i])+list(self.particlestats[i])) - data = np.array(data) - data[:,0], data[:,1], z = self.mapToLengthRaman((data[:,0], data[:,1]), noz=True) - data[:,2:7] *= self.pixelscale - header = "x [µm], y [µm], length [µm], height [µm], length_ellipse [µm], height_ellipse [µm]" - if data.shape[1]>6: - header = header + ", area [µm^2]" - data[:,6] *= self.pixelscale - np.savetxt(os.path.join(self.path, "particledata.txt"), data, - header=header) + print('not saving ParticleData into text file..\nThe current output format might be wrong, if multiple spectra per particle are present...') +# if len(self.ramanscansortindex)>0: +# data = [] +# pixelscale = (self.pixelscale_df if self.imagescanMode == 'df' else self.pixelscale_bf) +# for i in self.ramanscansortindex: +# data.append(list(self.ramanpoints[i])+list(self.particlestats[i])) +# data = np.array(data) +# data[:,0], data[:,1], z = self.mapToLengthRaman((data[:,0], data[:,1]), microscopeMode=self.imagescanMode, noz=True) +# data[:,2:7] *= pixelscale +# header = "x [µm], y [µm], length [µm], height [µm], length_ellipse [µm], height_ellipse [µm]" +# if data.shape[1]>6: +# header = header + ", area [µm^2]" +# data[:,6] *= pixelscale +# np.savetxt(os.path.join(self.path, "particledata.txt"), data, +# header=header) def save(self): saveData(self, self.fname) - \ No newline at end of file + + def saveBackup(self): + backupNameNotFound = True + inc = 0 + while backupNameNotFound: + directory = os.path.dirname(self.fname) + filename = self.name + '_backup_' + str(inc) + '.pkl' + path = os.path.join(directory, filename) + if os.path.exists(path): + inc += 1 + else: + saveData(self, path) + backupNameNotFound = False + + +if __name__ == '__main__': + dset = loadData(r'D:\Projekte\Mikroplastik\Microcatch_BALT\Sampling Kampagne 1\MCI_2\MCI_2_all_kleiner500\MCI_2_ds1+2_all_kleiner500_10_1\MCI_2_ds1+2_all_kleiner500_10_1.pkl') diff --git a/detectionview.py b/detectionview.py index 1a0f550..9235836 100755 --- a/detectionview.py +++ b/detectionview.py @@ -267,7 +267,7 @@ class ImageView(QtWidgets.QLabel): painter.drawEllipse(p[0]-p[2], p[1]-p[2], 2*p[2], 2*p[2]) class ParticleDetectionView(QtWidgets.QWidget): - imageUpdate = QtCore.pyqtSignal(name='imageUpdate') + imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field) detectionFinished = QtCore.pyqtSignal(name='detectionFinished') def __init__(self, img, dataset, parent=None): @@ -277,6 +277,7 @@ class ParticleDetectionView(QtWidgets.QWidget): self.imgclip = 0,0,0,0 self.seg = Segmentation(self.dataset) self.thread = None + self.view = parent vbox = QtWidgets.QVBoxLayout() hbox = QtWidgets.QHBoxLayout() @@ -462,7 +463,7 @@ class ParticleDetectionView(QtWidgets.QWidget): def closeEvent(self, event): self.detectionFinished.emit() - self.destroy() +# self.destroy() def mousePressEvent(self, event): if event.button()==QtCore.Qt.RightButton: @@ -561,7 +562,7 @@ class ParticleDetectionView(QtWidgets.QWidget): self.dataset.ramanscandone = False self.dataset.mode = "opticalscan" self.dataset.save() - self.imageUpdate.emit() + self.imageUpdate.emit(self.view.microscopeMode) @QtCore.pyqtSlot() def cancelThread(self): @@ -602,7 +603,7 @@ class ParticleDetectionView(QtWidgets.QWidget): self.thread = None self.unBlockUI() self.pdetectall.setText("Detect all") - self.imageUpdate.emit() + self.imageUpdate.emit(self.view.microscopeMode) if self.dataset is not None: self.setWindowTitle(str(len(self.dataset.ramanpoints)) + " Particles") else: diff --git a/externalmodules/__init__.py b/external/__init__.py similarity index 100% rename from externalmodules/__init__.py rename to external/__init__.py diff --git a/externalmodules/setuptsp.py b/external/setuptsp.py old mode 100755 new mode 100644 similarity index 100% rename from externalmodules/setuptsp.py rename to external/setuptsp.py diff --git a/externalmodules/tsp.pyx b/external/tsp.pyx old mode 100755 new mode 100644 similarity index 100% rename from externalmodules/tsp.pyx rename to external/tsp.pyx diff --git a/gepard.py b/gepard.py index 2fdaf38..3f86cdc 100755 --- a/gepard.py +++ b/gepard.py @@ -21,7 +21,9 @@ If not, see . from PyQt5 import QtCore, QtWidgets, QtGui from sampleview import SampleView from scalebar import ScaleBar -from ramancontrol import defaultPath +from ramancom.ramancontrol import defaultPath +from ramancom.ramanSwitch import RamanSwitch +from analysis.analysisWidgets import Legend import os class MeasureParticleWindow(QtWidgets.QMainWindow): @@ -36,10 +38,17 @@ class MeasureParticleWindow(QtWidgets.QMainWindow): self.view.imparent = self self.view.ScalingChanged.connect(self.scalingChanged) self.scalebar = ScaleBar(self) + self.legend = Legend(self) + self.ramanSwitch = RamanSwitch(self) self.view.ScalingChanged.connect(self.scalebar.updateScale) mdiarea = QtWidgets.QMdiArea(self) mdiarea.addSubWindow(self.scalebar) + mdiarea.addSubWindow(self.legend) + mdiarea.addSubWindow(self.ramanSwitch) + self.legend.hide() + self.ramanSwitch.hide() + subview = mdiarea.addSubWindow(self.view) subview.showMaximized() subview.setWindowFlags(QtCore.Qt.FramelessWindowHint) @@ -53,7 +62,9 @@ class MeasureParticleWindow(QtWidgets.QMainWindow): self.updateModes() def resizeEvent(self, event): - self.scalebar.move(0,self.height()-self.scalebar.height()) + self.scalebar.move(0,self.height()-self.scalebar.height()-30) + self.legend.move(self.width()-self.legend.width()-130, 10) + self.ramanSwitch.move(5, 5) def closeEvent(self, event): self.view.closeEvent(event) @@ -152,13 +163,20 @@ class MeasureParticleWindow(QtWidgets.QMainWindow): self.ramanScanAct.setCheckable(True) self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("RamanScan"))) - self.particelAnalysisAct = QtWidgets.QAction("Particle analysis", self) self.particelAnalysisAct.setEnabled(False) self.particelAnalysisAct.setCheckable(True) self.particelAnalysisAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("ParticleAnalysis"))) - + self.snapshotAct = QtWidgets.QAction("Save Screenshot", self) + self.snapshotAct.triggered.connect(self.view.takeScreenshot) + self.snapshotAct.setDisabled(True) + + self.configRamanCtrlAct = QtWidgets.QAction("Configure Raman Control", self) + self.configRamanCtrlAct.triggered.connect(self.view.configureRamanControl) + if self.view.simulatedRaman: + self.configRamanCtrlAct.setDisabled(True) + def updateModes(self, active=None, maxenabled=None): ose, osc, pde, pdc, rse, rsc, pae, pac = [False]*8 if maxenabled=="OpticalScan": @@ -219,7 +237,7 @@ class MeasureParticleWindow(QtWidgets.QMainWindow): self.fileMenu.addAction(self.openAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) - + self.viewMenu = QtWidgets.QMenu("&View", self) self.viewMenu.addAction(self.zoomInAct) self.viewMenu.addAction(self.zoomOutAct) @@ -232,14 +250,20 @@ class MeasureParticleWindow(QtWidgets.QMainWindow): self.modeMenu.addAction(self.detectParticleAct) self.modeMenu.addAction(self.particelAnalysisAct) + self.toolsMenu = QtWidgets.QMenu("Tools") + self.toolsMenu.addAction(self.snapshotAct) + self.toolsMenu.addAction(self.configRamanCtrlAct) + self.helpMenu = QtWidgets.QMenu("&Help", self) self.helpMenu.addAction(self.aboutAct) - + self.menuBar().addMenu(self.fileMenu) self.menuBar().addMenu(self.viewMenu) self.menuBar().addMenu(self.modeMenu) + self.menuBar().addMenu(self.toolsMenu) self.menuBar().addMenu(self.helpMenu) - + + def createToolBar(self): self.toolbar = QtWidgets.QToolBar("Tools") self.toolbar.setIconSize(QtCore.QSize(100,50)) @@ -263,10 +287,10 @@ if __name__ == '__main__': import sys from time import localtime, strftime - logname = os.path.join(os.path.split(__file__)[0], os.path.join("logfile.txt")) - fp = open(logname, "a") - sys.stderr = fp - sys.stdout = fp +# logname = os.path.join(os.path.split(__file__)[0], os.path.join("logfile.txt")) +# fp = open(logname, "a") +# sys.stderr = fp +# sys.stdout = fp print("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime())) app = QtWidgets.QApplication(sys.argv) diff --git a/helperfunctions.py b/helperfunctions.py index 9472294..38bffa4 100644 --- a/helperfunctions.py +++ b/helperfunctions.py @@ -24,15 +24,11 @@ import cv2 import os def cv2imread_fix(fname, flags=cv2.IMREAD_COLOR): - if not os.path.exists(fname): #This seems to be a source of potential errors. Having these lines here probably aids finding errors for other groups? - print('Error, image file not found\Please check if the used save-Image command returns before the image was fully written to disk.') - return - - with open(fname, "rb") as fp: - cont = fp.read() - img = cv2.imdecode(np.fromstring(cont, dtype=np.uint8), flags) - return img - return None + with open(fname, "rb") as fp: + cont = fp.read() + img = cv2.imdecode(np.fromstring(cont, dtype=np.uint8), flags) + return img + return None def cv2imwrite_fix(fname, img, params=None): pathname, ext = os.path.splitext(fname) diff --git a/opticalscan.py b/opticalscan.py index 5dd9191..c6eae32 100755 --- a/opticalscan.py +++ b/opticalscan.py @@ -31,7 +31,10 @@ from time import sleep, time import datetime import sys +#def scan(path, sol, zpositions, grid, controlclass, connection, ishdr=False): def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, ishdr=False): + print('starting new optical scan') + sys.stdout.flush() if ishdr: merge_mertens = cv2.createMergeMertens() with open("scanlog.txt", "a") as fp: @@ -69,17 +72,24 @@ def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, ishdr= return dataqueue.put(i) ramanctrl.disconnect() + def loadAndPasteImage(srcnames, fullimage, fullzval, width, height, - rotationvalue, p0, p1, p): + rotationvalue, p0, p1, p, halfResolution = False): colimgs = [] for name in srcnames: colimgs.append(cv2.cvtColor(cv2imread_fix(name), cv2.COLOR_BGR2RGB)) img, zval = imageStacking(colimgs) + + if halfResolution: #halve resolution, if fullimage would become too large otherwise + img = cv2.resize(img, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) + zval= cv2.resize(zval, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) + x, y = p Nx, Ny = int((p1[0]-p0[0]+width)/width*img.shape[1]), int((p0[1]-p1[1]+height)/height*img.shape[0]) + 10 # + 10 because of rotation and hopefully it will be small c, s = np.cos(np.radians(rotationvalue)), np.sin(np.radians(rotationvalue)) dx, dy = (x-p0[0])/width*img.shape[1], (p0[1]-y)/height*img.shape[0] + M = np.float32([[c,s,dx],[-s,c,dy]]) if fullimage is not None: cv2.warpAffine(img, M, (Nx, Ny), fullimage, borderMode=cv2.BORDER_TRANSPARENT) @@ -196,23 +206,22 @@ class PointCoordinates(QtWidgets.QGridLayout): return points class OpticalScan(QtWidgets.QWidget): - imageUpdate = QtCore.pyqtSignal(name='imageUpdate') + imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field) boundaryUpdate = QtCore.pyqtSignal() def __init__(self, ramanctrl, dataset, parent=None): super().__init__(parent, QtCore.Qt.Window) self.view = parent vbox = QtWidgets.QVBoxLayout() - group = QtWidgets.QGroupBox("Point coordinates [µm]", self) + pointgroup = QtWidgets.QGroupBox("Point coordinates [µm]", self) self.ramanctrl = ramanctrl self.dataset = dataset self.positions = [] self.process = None self.points = PointCoordinates(5, self.ramanctrl, self) - group.setLayout(self.points) + pointgroup.setLayout(self.points) self.points.readPoint.connect(self.takePoint) - hbox = QtWidgets.QHBoxLayout() self.pareaselect = QtWidgets.QPushButton("Area select", self) label = QtWidgets.QLabel("Size increase:", self) self.radiusincreaseedit = QtWidgets.QDoubleSpinBox(self) @@ -220,16 +229,20 @@ class OpticalScan(QtWidgets.QWidget): self.radiusincreaseedit.setMaximum(1000) self.radiusincreaseedit.setDecimals(0) self.radiusincreaseedit.setSingleStep(20) + self.radiusincreaseedit.setMaximumWidth(100) + self.radiusincreaseedit.valueChanged.connect(self.areaSelect) label2 = QtWidgets.QLabel("Maximal focus height [µm]:", self) self.zmaxedit = QtWidgets.QDoubleSpinBox(self) self.zmaxedit.setMinimum(1) self.zmaxedit.setMaximum(1000) self.zmaxedit.setDecimals(0) self.zmaxedit.setValue(50) + self.zmaxedit.setMaximumWidth(100) label3 = QtWidgets.QLabel("Focus steps:", self) self.nzedit = QtWidgets.QSpinBox(self) self.nzedit.setRange(2,10) self.nzedit.setValue(3) + self.nzedit.setMaximumWidth(100) self.hdrcheck = QtWidgets.QCheckBox("High dynamic range", self) self.hdrcheck.setChecked(False) @@ -238,7 +251,6 @@ class OpticalScan(QtWidgets.QWidget): self.pareaselect.released.connect(self.areaSelect) self.prun.released.connect(self.run) self.pexit.released.connect(self.stopScan) - self.pareaselect.setEnabled(False) self.prun.setEnabled(False) self.timelabeltext = "Estimated time to finish: " @@ -247,34 +259,65 @@ class OpticalScan(QtWidgets.QWidget): self.progresstime.setEnabled(False) self.progressbar.setEnabled(False) - vboxradio = QtWidgets.QVBoxLayout() + radioGroup = QtWidgets.QGroupBox('Shape') + radioLayout = QtWidgets.QHBoxLayout() self.circlerad = QtWidgets.QRadioButton("Circle") + self.circlerad.clicked.connect(self.areaSelect) self.rectanglerad = QtWidgets.QRadioButton("Rectangle") - self.circlerad.setChecked(True) - vboxradio.addWidget(self.circlerad) - vboxradio.addWidget(self.rectanglerad) + self.rectanglerad.setChecked(True) + self.rectanglerad.clicked.connect(self.areaSelect) + radioLayout.addWidget(self.circlerad) + radioLayout.addWidget(self.rectanglerad) + radioGroup.setLayout(radioLayout) + + micModeGroup = QtWidgets.QGroupBox('Mode for Image Acquisition') + micModeLayout = QtWidgets.QHBoxLayout() + self.df_btn = QtWidgets.QRadioButton('Darkfield') + self.df_btn.setChecked(True) + self.df_btn.clicked.connect(self.areaSelect) + self.bf_btn = QtWidgets.QRadioButton('Brightfield') + self.bf_btn.clicked.connect(self.areaSelect) + micModeLayout.addWidget(self.df_btn) + micModeLayout.addWidget(self.bf_btn) + micModeGroup.setLayout(micModeLayout) - grid = QtWidgets.QGridLayout() - - grid.addLayout(vboxradio, 0, 0, QtCore.Qt.AlignLeft) - grid.addWidget(label, 0, 1, QtCore.Qt.AlignLeft) - grid.addWidget(self.radiusincreaseedit, 0, 2, QtCore.Qt.AlignRight) - grid.addWidget(self.pareaselect, 0, 3, QtCore.Qt.AlignRight) + self.halfResChecker = QtWidgets.QCheckBox('Half resolution') + self.halfResChecker.setChecked(False) + self.halfResChecker.setToolTip('Enable for very high resolution images.\nFull resolution slows down the scan too much..') + + self.deleteImgChecker = QtWidgets.QCheckBox('Delete image files after run') + self.deleteImgChecker.setChecked(True) - grid.addWidget(self.hdrcheck, 1, 0, QtCore.Qt.AlignLeft) - grid.addWidget(label2, 1, 1, QtCore.Qt.AlignLeft) - grid.addWidget(self.zmaxedit, 1, 2, QtCore.Qt.AlignRight) - grid.addWidget(label3, 1, 3, QtCore.Qt.AlignLeft) - grid.addWidget(self.nzedit, 1, 4, QtCore.Qt.AlignRight) + self.areaOptionsGroup = QtWidgets.QGroupBox('Area Select Options') + areaLayout = QtWidgets.QFormLayout() + areaLayout.addRow(radioGroup) + areaLayout.addRow(label, self.radiusincreaseedit) + areaLayout.addRow(micModeGroup) + if not self.view.ramanSwitchNeeded: + micModeGroup.setDisabled(True) + areaLayout.addRow(self.pareaselect) + self.areaOptionsGroup.setLayout(areaLayout) + self.areaOptionsGroup.setDisabled(True) - grid.addWidget(self.prun, 2, 3, QtCore.Qt.AlignLeft) - grid.addWidget(self.pexit, 2, 4, QtCore.Qt.AlignRight) + furtherOptionsGroup = QtWidgets.QGroupBox('Further Options') + furtherOptionsLayout = QtWidgets.QFormLayout() + furtherOptionsLayout.addRow(label2, self.zmaxedit) + furtherOptionsLayout.addRow(label3, self.nzedit) + furtherOptionsLayout.addRow(self.hdrcheck) + furtherOptionsLayout.addRow(self.deleteImgChecker) + furtherOptionsLayout.addRow(self.halfResChecker) + furtherOptionsGroup.setLayout(furtherOptionsLayout) - hbox.addStretch() - hbox.addLayout(grid) - vbox.addWidget(group) - vbox.addLayout(hbox) + btnLayout = QtWidgets.QHBoxLayout() + btnLayout.addWidget(self.prun) + btnLayout.addWidget(self.pexit) + btnLayout.addStretch() + + vbox.addWidget(pointgroup) + vbox.addWidget(self.areaOptionsGroup) + vbox.addWidget(furtherOptionsGroup) + vbox.addLayout(btnLayout) vbox.addWidget(self.progresstime) vbox.addWidget(self.progressbar) @@ -299,23 +342,24 @@ class OpticalScan(QtWidgets.QWidget): else: return self.close() - - + @QtCore.pyqtSlot() def areaSelect(self): + magn = self.ramanctrl.magn + if self.circlerad.isChecked() == True: xym, r = cv2.minEnclosingCircle(np.array([p[:2] for p in self.dataset.fitpoints], dtype=np.float32)) r += self.radiusincreaseedit.value() - phi = np.linspace(0, 2*np.pi, 20, endpoint=False) + phi = np.linspace(0, 2*np.pi, magn, endpoint=False) self.dataset.boundary = [[xym[0]+r*np.cos(phii), xym[1]+r*np.sin(phii)] for phii in phi] else: da = self.radiusincreaseedit.value() x0, x1 = self.dataset.fitpoints[:,0].min()-da, self.dataset.fitpoints[:,0].max()+da y0, y1 = self.dataset.fitpoints[:,1].min()-da, self.dataset.fitpoints[:,1].max()+da a = 2*(y1-y0 + x1-x0) - nx, ny = max(int(np.round((x1-x0)/a*20)),2), max(int(np.round((y1-y0)/a*20)),2) + nx, ny = max(int(np.round((x1-x0)/a*magn)),2), max(int(np.round((y1-y0)/a*magn)),2) x, dx = np.linspace(x0, x1, nx, endpoint=False, retstep=True) y, dy = np.linspace(y0, y1, ny, endpoint=False, retstep=True) self.dataset.boundary = [[xi, yi] for xi, yi in zip(x,y0*np.ones_like(x))] + \ @@ -331,7 +375,8 @@ class OpticalScan(QtWidgets.QWidget): self.dataset = ds self.points.createWidgets(5, list(zip(ds.fitindices,ds.fitpoints))) if len(self.dataset.fitindices)>1: - self.pareaselect.setEnabled(True) +# self.pareaselect.setEnabled(True) + self.areaOptionsGroup.setEnabled(True) softwarez = self.ramanctrl.getSoftwareZ() if abs(softwarez) >0.1: reply = QtWidgets.QMessageBox.critical(self, 'Software z position nonzero', @@ -356,12 +401,19 @@ class OpticalScan(QtWidgets.QWidget): if reply != QtWidgets.QMessageBox.Yes: return self.ramanctrl.saveImage(self.dataset.getTmpImageName()) - width, height, rotationvalue = self.ramanctrl.getImageDimensions() + width, height, rotationvalue = self.ramanctrl.getImageDimensions(self.view.microscopeMode) pshift = self.ramanctrl.getRamanPositionShift() self.dataset.pshift = pshift img = cv2.cvtColor(cv2imread_fix(self.dataset.getTmpImageName()), cv2.COLOR_BGR2RGB) - self.dataset.pixelscale = width/img.shape[1] - self.dataset.imagedim = width, height, rotationvalue + if self.halfResChecker.isChecked(): + img = cv2.resize(img, None, fx = 0.5, fy = 0.5, interpolation = cv2.INTER_CUBIC) + + self.dataset.imagedim_bf = self.ramanctrl.getImageDimensions('bf') + self.dataset.pixelscale_bf = self.dataset.imagedim_bf[0]/img.shape[1] #=imagedim_width/shape[1] + self.dataset.imagedim_df = self.ramanctrl.getImageDimensions('df') + self.dataset.pixelscale_df = self.dataset.imagedim_df[0]/img.shape[1] #=imagedim_width/shape[1] + + points = self.points.getPoints() ind = np.isfinite(points[:,0]) @@ -369,7 +421,8 @@ class OpticalScan(QtWidgets.QWidget): points = points[ind,:].copy() self.dataset.fitpoints = points if len(points)>1: - self.pareaselect.setEnabled(True) + self.areaOptionsGroup.setEnabled(True) + points = np.concatenate(([[x,y,z]], points), axis=0) p0 = [points[:,0].min(), points[:,1].max()] p1 = [points[:,0].max(), points[:,1].min()] @@ -387,14 +440,18 @@ class OpticalScan(QtWidgets.QWidget): dx, dy = (lp[0]-p0[0])/width*img.shape[1], (p0[1]-lp[1])/height*img.shape[0] full = self.view.imgdata M = np.float32([[1,0,dx],[0,1,dy]]) - full = cv2.warpAffine(full, M, (Nx, Ny)) - dst = cv2.max(full, dst) + try: + full = cv2.warpAffine(full, M, (Nx, Ny)) #fails, if image dimensions are >32767x32767px... + dst = cv2.max(full, dst) + except: + QtWidgets.QMessageBox.critical(self, 'Error', 'Image is too large\nPlease repeat with "scale image" checked.') + return self.view.imgdata = dst self.dataset.lastpos = p0 self.dataset.maxdim = p0 + p1 self.dataset.readin = False - self.imageUpdate.emit() + self.imageUpdate.emit(self.view.microscopeMode) @QtCore.pyqtSlot() def run(self): @@ -414,6 +471,19 @@ class OpticalScan(QtWidgets.QWidget): self.dataset.readin = False else: return + + self.view.imparent.ramanSwitch.df_btn.setChecked(self.df_btn.isChecked()) + self.view.imparent.ramanSwitch.setDisabled(True) + + if self.df_btn.isChecked(): + self.view.dataset.imagescanMode = 'df' + else: + self.view.dataset.imagescanMode = 'bf' + + #TODO: + #DISABLE OPTION GROUPS when scanning, reactivate upon cancelling + + points = np.float32(self.dataset.fitpoints) # convert z to software z, which is relative to current user z softwarez = self.ramanctrl.getSoftwareZ() # get current software z @@ -425,7 +495,7 @@ class OpticalScan(QtWidgets.QWidget): self.dataset.zpositions = np.array([0.0]) else: self.dataset.zpositions = np.linspace(0, zmaxstack, Nz) - width, height, rotationvalue = self.dataset.imagedim + width, height, rotationvalue = self.dataset.imagedim_df print("Width, height, rotation:", width, height, rotationvalue) print("Points x:", points[:,0].min(), points[:,0].max()) print("Points y:", points[:,1].min(), points[:,1].max()) @@ -460,6 +530,11 @@ class OpticalScan(QtWidgets.QWidget): QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: +# if self.halfResChecker.isChecked(): +# #reset pixelscales!!! +# self.dataset.pixelscale_df /= 2 +# self.dataset.pixelscale_bf /= 2 + self.prun.setEnabled(False) self.ramanctrl.disconnect() self.processstopevent = Event() @@ -495,28 +570,38 @@ class OpticalScan(QtWidgets.QWidget): i = -1 if i>=0: + Ngrid = len(self.dataset.grid) names = [] for k in range(len(self.dataset.zpositions)): names.append(os.path.join(self.dataset.getScanPath(),f"image_{i}_{k}.bmp")) - width, height, rotationvalue = self.dataset.imagedim + width, height, rotationvalue = (self.dataset.imagedim_df if self.view.imparent.ramanSwitch.df_btn.isChecked() else self.dataset.imagedim_bf) p = self.dataset.grid[i] p0, p1 = self.dataset.maxdim[:2], self.dataset.maxdim[2:] self.view.imgdata, self.dataset.zvalimg = loadAndPasteImage(names, self.view.imgdata, self.dataset.zvalimg, width, height, - rotationvalue, p0, p1, p) + rotationvalue, p0, p1, p, halfResolution = self.halfResChecker.isChecked()) self.progressbar.setValue(i+1) if i>3: timerunning = time()-self.starttime ttot = timerunning*Ngrid/(i+1) time2go = ttot - timerunning self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) - self.imageUpdate.emit() + self.imageUpdate.emit(self.view.microscopeMode) + if i==Ngrid-1: cv2imwrite_fix(self.dataset.getImageName(), cv2.cvtColor(self.view.imgdata, cv2.COLOR_RGB2BGR)) self.dataset.saveZvalImg() self.process.join() self.dataqueue.close() self.dataqueue.join_thread() + + if self.deleteImgChecker.isChecked(): + path = self.dataset.getScanPath() + files = os.listdir(path) + for file in files: + if file.startswith('image_') and (file.endswith('.bmp') or file.endswith('.tiff')): + os.remove(os.path.join(path, file)) + self.ramanctrl.connect() self.view.saveDataSet() self.view.unblockUI() @@ -526,12 +611,12 @@ class OpticalScan(QtWidgets.QWidget): self.progresstime.setEnabled(False) self.close() return - self.timer.start(100.) + self.timer.start(100.) if __name__ == "__main__": - from WITecCOM import WITecCOM + from ramancom.simulatedraman import SimulatedRaman app = QtWidgets.QApplication(sys.argv) - optscan = OpticalScan(WITecCOM()) + optscan = OpticalScan(SimulatedRaman()) optscan.show() sys.exit(app.exec_()) \ No newline at end of file diff --git a/WITecCOM.py b/ramancom/WITecCOM.py similarity index 91% rename from WITecCOM.py rename to ramancom/WITecCOM.py index 92f113a..19f0596 100644 --- a/WITecCOM.py +++ b/ramancom/WITecCOM.py @@ -28,12 +28,25 @@ except ImportError: os.environ["NO_WITEC_CONTROL"] = "True" from time import sleep, time -from ramanbase import RamanBase +try: #when running the witectesting, the paths have to be differently, as it is in the same directory as the other raman com modules + from ramancom.ramanbase import RamanBase + from ramancom.configRaman import RamanSettingParam +except: + from ramanbase import RamanBase + from configRaman import RamanSettingParam from socket import gethostname +from PyQt5 import QtWidgets + class WITecCOM(RamanBase): CLSID = "{C45E77CE-3D66-489A-B5E2-159F443BD1AA}" + + magn = 20 + + ramanParameters = [RamanSettingParam('IntegrationTime (s)', 'double', default=0.5, minVal=0.01, maxVal=100), + RamanSettingParam('Accumulations', 'int', default=5, minVal=1, maxVal=100)] + def __init__(self, hostname=None): super().__init__() if hostname is None: @@ -193,7 +206,7 @@ class WITecCOM(RamanBase): z = self.PosZCurUserFloatMan.GetSingleValueAsDouble()[1] return z - def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011, debugReturn=False, measurementRunning=False): assert self.connected initpos = self.getPosition() # move only if new position is really different; repeat if new position is ignored (happens some times) @@ -208,9 +221,8 @@ class WITecCOM(RamanBase): while distance > epsxy:# and (lastpos is None or lastpos!=curpos): curpos = self.getPosition() distance = max(abs(curpos[0]-x), abs(curpos[1]-y)) -# if ((time()-t0>0.5) and max(abs(curpos[0]-initpos[0]), abs(curpos[1]-initpos[1]))10.): - if ((time()-t0>2) and max(abs(curpos[0]-initpos[0]), abs(curpos[1]-initpos[1]))10.): - print("WARNING: signal ignored; time: {} s, x, y: {}, {}, curPos: {}, initPos: {}".format((time()-t0), x, y, curpos, initpos)) + if ((time()-t0>0.5) and max(abs(curpos[0]-initpos[0]), abs(curpos[1]-initpos[1]))10.): + print("WARNING: signal ignored:", time()-t0, x, y, curpos, initpos) sys.stdout.flush() break sleep(.01) @@ -244,7 +256,7 @@ class WITecCOM(RamanBase): self.ImageSaveMan.OperateTrigger() sleep(.1) - def getImageDimensions(self): + def getImageDimensions(self, mode = 'df'): """ Get the image width and height in um and the orientation angle in degrees. """ assert self.connected @@ -262,16 +274,17 @@ class WITecCOM(RamanBase): if not Busy: break - def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + def initiateMeasurement(self, ramanSettings): assert self.connected - self.timeseries = numberofscans - self.TimeSeriesSlowNameMan.SetValue(label) - self.TimeSeriesSlowNumMeasurementsMan.SetValue(numberofscans) - self.TimeSeriesSlowNumAccumulationsMan.SetValue(accumulations) - self.TimeSeriesSlowIntTimeMan.SetValue(integrtime) + self.timeseries = ramanSettings['numPoints'] + self.TimeSeriesSlowNameMan.SetValue(ramanSettings['filename']) + self.TimeSeriesSlowNumMeasurementsMan.SetValue(ramanSettings['numPoints']) + self.TimeSeriesSlowNumAccumulationsMan.SetValue(ramanSettings['Accumulations']) + self.TimeSeriesSlowIntTimeMan.SetValue(ramanSettings['IntegrationTime (s)']) self.TimeSeriesSlowModeMan.SetValueNumeric(0) self.TimeSeriesSlowNumMan.SetValue(0) self.TimeSeriesSlowStartMan.OperateTrigger() + sleep(0.1) t1 = time() while True: @@ -286,7 +299,7 @@ class WITecCOM(RamanBase): t1 = time() - def nextTimeSeriesScan(self, num): + def triggerMeasurement(self, num): assert self.timeseries self.TimeSeriesSlowNextMan.OperateTrigger() # Wait until sequencer has finished @@ -313,4 +326,5 @@ class WITecCOM(RamanBase): if num==self.timeseries-1: self.timeseries = False - \ No newline at end of file + def finishMeasurement(self): + pass \ No newline at end of file diff --git a/ramancom/__init__.py b/ramancom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ramancom/configRaman.py b/ramancom/configRaman.py new file mode 100644 index 0000000..39028b0 --- /dev/null +++ b/ramancom/configRaman.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Sep 21 12:23:20 2018 + +@author: brandt +""" + + +from PyQt5 import QtWidgets + +class RamanConfigWin(QtWidgets.QWidget): + def __init__(self, parent=None): + super(RamanConfigWin, self).__init__() + self.parent = parent + if parent is not None: + self.ramancontrol = self.parent.ramanctrl + + self.setGeometry(200, 200, 400, 300) + self.setWindowTitle('Configure Raman Control') + layout = QtWidgets.QGridLayout() + self.setLayout(layout) + + generalGroup = QtWidgets.QGroupBox('General settings') + witecGroup = QtWidgets.QGroupBox('WITec settings') + renishawGroup = QtWidgets.QGroupBox('Renishaw settings') + + generalLayout = QtWidgets.QFormLayout() + self.magnSpinBox = QtWidgets.QSpinBox() + if parent is not None: + self.magnSpinBox.setValue(self.ramancontrol.magn) + else: + self.magnSpinBox.setValue(20) + generalLayout.addRow(QtWidgets.QLabel('Magnification (x)'), self.magnSpinBox) + generalGroup.setLayout(generalLayout) + + witecLayout = QtWidgets.QFormLayout() + witecLayout.addRow(QtWidgets.QLabel('Nothing yet implemented...')) + witecGroup.setLayout(witecLayout) + + renishawLayout = QtWidgets.QFormLayout() + self.ren_BFwidth = QtWidgets.QDoubleSpinBox() + self.ren_BFheight = QtWidgets.QDoubleSpinBox() + self.ren_DFwidth = QtWidgets.QDoubleSpinBox() + self.ren_DFheight = QtWidgets.QDoubleSpinBox() + for spinbox in [self.ren_BFheight, self.ren_BFwidth, self.ren_DFheight, self.ren_DFwidth]: + spinbox.setMinimum(0) + spinbox.setMaximum(1e7) + + if self.parent is not None: + self.ren_BFwidth.setValue(self.ramancontrol.cam_bf_dims[0]) + self.ren_BFheight.setValue(self.ramancontrol.cam_bf_dims[1]) + self.ren_DFwidth.setValue(self.ramancontrol.cam_df_dims[0]) + self.ren_DFheight.setValue(self.ramancontrol.cam_df_dims[1]) + + self.getBFImgDimBtn = QtWidgets.QPushButton('Read dimensions from File') + self.getBFImgDimBtn.released.connect(self.makeReadImgDimsLambda(self.ren_BFwidth, self.ren_BFheight)) + self.getDFImgDimBtn = QtWidgets.QPushButton('Read dimensions from File') + self.getDFImgDimBtn.released.connect(self.makeReadImgDimsLambda(self.ren_DFwidth, self.ren_DFheight)) + + renishawLayout.addRow(QtWidgets.QLabel('Bright Field Camera Settings')) + renishawLayout.addRow(QtWidgets.QLabel('Image widht (um)'), self.ren_BFwidth) + renishawLayout.addRow(QtWidgets.QLabel('Image height (um)'), self.ren_BFheight) + renishawLayout.addRow(self.getBFImgDimBtn) + renishawLayout.addRow(QtWidgets.QLabel()) + renishawLayout.addRow(QtWidgets.QLabel('Dark Field Camera Settings')) + renishawLayout.addRow(QtWidgets.QLabel('Image widht (um)'), self.ren_DFwidth) + renishawLayout.addRow(QtWidgets.QLabel('Image height (um)'), self.ren_DFheight) + renishawLayout.addRow(self.getDFImgDimBtn) + renishawGroup.setLayout(renishawLayout) + + layout.addWidget(generalGroup, 0, 0, 1, 2) + layout.addWidget(witecGroup, 1, 0) + layout.addWidget(renishawGroup, 1, 1) + + closeBtn = QtWidgets.QPushButton('Save and Exit') + closeBtn.released.connect(self.close) + layout.addWidget(closeBtn, 2, 0) + + def makeReadImgDimsLambda(self, width_spinbox, height_spinbox): + return lambda: self.readImgDims(width_spinbox, height_spinbox) + + def readImgDims(self, width_spinbox, height_spinbox): + try: + import exifread + except: + QtWidgets.QMessageBox.critical(self, 'Error', 'Failed to load exifread module!\nPlease install exifread module to use this function') + return + + magn, ok = QtWidgets.QInputDialog.getInt(self, 'Enter Magnification', 'Magnification = ', 20) + if not ok: + return + + fname = QtWidgets.QFileDialog.getOpenFileName(self, 'Select jpeg file', 'C://', 'Image file (*.jpg *.jpeg)')[0] + if len(fname) == 0: + return + + + def stringDivision(string): + a, b = string.split('/') + a, b = float(a), float(b) + return a/b + + try: + file = open(fname, 'rb') + tags = exifread.process_file(file) + for index, key in enumerate(tags): + if index == 3: + startVals = tags[key].printable + if index == 4: + ranges = tags[key].printable + + startVals = startVals.replace('[', '').replace(']', '').replace(' ', '') #remove brackets and space + ranges = ranges.replace('[', '').replace(']', '').replace(' ', '') #remove brackets and space + + startX, startY = startVals.split(',') + rangeX, rangeY = ranges.split(',') + + startX, startY = stringDivision(startX), stringDivision(startY) + rangeX, rangeY = stringDivision(rangeX), stringDivision(rangeY) + + width_spinbox.setValue(rangeX*magn) + height_spinbox.setValue(rangeY*magn) + + except: + QtWidgets.QMessageBox.critical(self, 'Error', 'Reading dimensions from jpeg failed') + + def closeEvent(self, event): + ret = QtWidgets.QMessageBox.question(self, '', 'Save Data to RamanControl?', QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + if ret == QtWidgets.QMessageBox.Yes: + + if self.parent is not None: + if self.ramancontrol.hostname == 'AN-Raman-Renishaw': + self.ramancontrol.cam_bf_dims = [self.ren_BFwidth.value(), self.ren_BFheight.value()] + self.ramancontrol.cam_df_dims = [self.ren_DFwidth.value(), self.ren_DFheight.value()] + self.ramancontrol.magn = self.magnSpinBox.value() + else: + print('fake saved') + + +class RamanSettingParam(object): + def __init__(self, name, dtype, default=None, minVal=None, maxVal=None, valList=None, openFileType=None): + self.name = name + self.dtype = dtype + self.value = default + self.minVal = minVal + self.maxVal = maxVal + self.valList = valList + self.openFileType = openFileType + + if not self.hasValidType(): + print('erroreneous type in setting parameter:', self.dtype) + + def hasValidType(self): + if self.dtype in ['int', 'double', 'checkBox', 'combobox', 'selectBtn']: + return True + else: + return False + + def value_of(self, obj): + if self.dtype in ['int', 'double']: + return obj.value() + elif self.dtype == 'checkBox': + return obj.isChecked() + elif self.dtype == 'combobox': + return obj.currentText() + elif self.dtype == 'selectBtn': + return obj.text() + +if __name__ == '__main__': + import sys + app = QtWidgets.QApplication(sys.argv) + setWin = RamanConfigWin() + setWin.show() + ret = app.exec_() \ No newline at end of file diff --git a/ramancom/ramanSwitch.py b/ramancom/ramanSwitch.py new file mode 100644 index 0000000..77081a5 --- /dev/null +++ b/ramancom/ramanSwitch.py @@ -0,0 +1,180 @@ +import sys, os +from PyQt5 import QtWidgets, QtCore + +class RamanSwitch(QtWidgets.QMdiSubWindow): + def __init__(self, parent=None): + super(RamanSwitch, self).__init__() + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.FramelessWindowHint) + self.setFixedSize(220, 60) + + self.parent = parent + self.sampleview = None + layout = self.layout() + + self.df_btn = QtWidgets.QRadioButton('Darkfield') + self.bf_btn = QtWidgets.QRadioButton('Brightfield') + self.bf_btn.setChecked(True) + self.df_btn.clicked.connect(self.updateMode) + self.bf_btn.clicked.connect(self.updateMode) + + groupLayout = QtWidgets.QHBoxLayout() + group = QtWidgets.QGroupBox('Micropscope Mode') + groupLayout.addWidget(self.bf_btn) + groupLayout.addWidget(self.df_btn) + group.setLayout(groupLayout) + layout.addWidget(group) + + def connectToSampleView(self): + if self.parent is not None: + self.sampleview = self.parent.view + if os.path.exists(self.sampleview.dataset.getImageName()): + mode = self.sampleview.dataset.imagescanMode + print('fullimage was acquired in', mode) + if mode == 'df': + self.df_btn.setChecked(True) + else: + self.df_btn.setChecked(False) + self.setDisabled(True) + + def updateMode(self): + if self.sampleview is not None: + if self.df_btn.isChecked(): + self.sampleview.microscopeMode = 'df' + self.sampleview.announceScaling() + else: + self.sampleview.microscopeMode = 'bf' + self.sampleview.announceScaling() + print('mic-mode is now', self.sampleview.microscopeMode) + +#class QSlideSwitchPrivate(QtCore.QObject): +# +# def __init__(self, q): +# super(QSlideSwitchPrivate, self).__init__() +# +# self._position = 0 +# self._sliderShape = QtCore.QRectF() +# self._gradient = QtGui.QLinearGradient() +# self._gradient.setSpread(QtGui.QGradient.PadSpread) +# self._qPointer = q +# +# self.animation = QtCore.QPropertyAnimation(self) +# self.animation.setTargetObject(self) +# self.animation.setPropertyName(b"position") +# self.animation.setStartValue(0) +# self.animation.setEndValue(1) +# self.animation.setDuration(300) +# self.animation.setEasingCurve(QtCore.QEasingCurve.InOutExpo) +# +# def __del__(self): +# del self.animation +# +# @QtCore.pyqtProperty(float) +# def position(self): +# return self._position +# +# @position.setter +# def position(self, value): +# self._position = value +# self._qPointer.repaint() +# +# def drawSlider(self, painter): +# margin = 3 +# r = self._qPointer.rect().adjusted(0,0,-1,-1) +# dx = (r.width() - self._sliderShape.width()) * self._position +# sliderRect = self._sliderShape.translated(dx, 0) +# painter.setPen(QtCore.Qt.NoPen) +# +# # basic settings +# shadow = self._qPointer.palette().color(QtGui.QPalette.Dark) +# light = self._qPointer.palette().color(QtGui.QPalette.Light) +# button = self._qPointer.palette().color(QtGui.QPalette.Button) +# +# # draw background +# # draw outer background +# self._gradient.setColorAt(0, shadow.darker(130)) +# self._gradient.setColorAt(1, light.darker(130)) +# self._gradient.setStart(0, r.height()) +# self._gradient.setFinalStop(0, 0) +# painter.setBrush(self._gradient) +# painter.drawRoundedRect(r, 15, 15) +# +# # draw background +# # draw inner background +# self._gradient.setColorAt(0, shadow.darker(140)) +# self._gradient.setColorAt(1, light.darker(160)) +# self._gradient.setStart(0, 0) +# self._gradient.setFinalStop(0, r.height()) +# painter.setBrush(self._gradient) +# painter.drawRoundedRect(r.adjusted(margin, margin, -margin, -margin), 15, 15) +# +# # draw slider +# self._gradient.setColorAt(0, button.darker(130)) +# self._gradient.setColorAt(1, button) +# +# # draw outer slider +# self._gradient.setStart(0, r.height()) +# self._gradient.setFinalStop(0, 0) +# painter.setBrush(self._gradient) +# painter.drawRoundedRect(sliderRect.adjusted(margin, margin, -margin, -margin), 10, 15) +# +# # draw inner slider +# self._gradient.setStart(0, 0) +# self._gradient.setFinalStop(0, r.height()) +# painter.setBrush(self._gradient) +# painter.drawRoundedRect(sliderRect.adjusted(2.5 * margin, 2.5 * margin, -2.5 * margin, - 2.5 * margin), 5, 15) +# +# font = self._qPointer.font() +# self._gradient.setColorAt(0, light) +# self._gradient.setColorAt(1, shadow) +# self._gradient.setStart(0, r.height() / 2.0 + font.pointSizeF()) +# self._gradient.setFinalStop(0, r.height() / 2.0 - font.pointSizeF()) +# painter.setFont(font) +# painter.setPen(QtCore.Qt.black) +# painter.drawText(0, 0, r.width() / 2, r.height()-1, QtCore.Qt.AlignCenter, "Brightfield") +# painter.drawText( r.width() / 2, 0, r.width() / 2, r.height() - 1, QtCore.Qt.AlignCenter, "Darkfield") +# +# def updateSliderRect(self, size): +# self._sliderShape.setWidth(size.width() / 2.0) +# self._sliderShape.setHeight(size.height() - 1.0) +# +# @QtCore.pyqtSlot(bool, name='animate') +# def animate(self, checked): +# self.animation.setDirection(QtCore.QPropertyAnimation.Forward if checked else QtCore.QPropertyAnimation.Backward) +# self.animation.start() +# +# +#class QSlideSwitch(QtWidgets.QAbstractButton): +# def __init__(self, parent = None): +# super(QSlideSwitch, self).__init__(parent) +# +# self.d_ptr = QSlideSwitchPrivate( self ) +# self.clicked.connect( self.d_ptr.animate ) +# self.d_ptr.animation.finished.connect( self.update ) +# +# def __del__(self): +# del self.d_ptr +# +# def sizeHint(self): +# return QtCore.QSize(48, 28) +# +# def hitButton(self, point): +# return self.rect().contains(point) +# +# def paintEvent(self, event): +# painter = QtGui.QPainter(self) +# painter.setRenderHint(QtGui.QPainter.Antialiasing) +# self.d_ptr.drawSlider(painter) +# +# def resizeEvent(self, event): +# self.d_ptr.updateSliderRect(event.size()) +# self.repaint() + + +if __name__ == '__main__': + + import sys + + app = QtWidgets.QApplication(sys.argv) + win = RamanSwitch() + win.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ramanbase.py b/ramancom/ramanbase.py similarity index 86% rename from ramanbase.py rename to ramancom/ramanbase.py index f0d474f..28ccccf 100644 --- a/ramanbase.py +++ b/ramancom/ramanbase.py @@ -44,7 +44,7 @@ class RamanBase(object): def getUserZ(self): raise NotImplementedError - def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011, debugReturn=False, measurementRunning=False): raise NotImplementedError def moveZto(self, z, epsz=0.011): @@ -53,7 +53,7 @@ class RamanBase(object): def saveImage(self, fname): raise NotImplementedError - def getImageDimensions(self): + def getImageDimensions(self, mode = 'df'): """ Get the image width and height in um and the orientation angle in degrees. """ raise NotImplementedError @@ -61,8 +61,11 @@ class RamanBase(object): def startSinglePointScan(self): raise NotImplementedError - def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + def initiateMeasurement(self, ramanSettings): raise NotImplementedError - def nextTimeSeriesScan(self, num): + def triggerMeasurement(self, num): + raise NotImplementedError + + def finishMeasurement(self): raise NotImplementedError \ No newline at end of file diff --git a/ramancontrol.py b/ramancom/ramancontrol.py similarity index 55% rename from ramancontrol.py rename to ramancom/ramancontrol.py index e9bbf1e..7d0174e 100644 --- a/ramancontrol.py +++ b/ramancom/ramancontrol.py @@ -23,12 +23,14 @@ import configparser __all__ = ["RamanControl", "defaultPath", "simulatedRaman"] -defaultPath = os.path.split(__file__)[0] + +defaultPath = os.path.dirname(os.path.split(__file__)[0]) config = configparser.ConfigParser() config.read(os.path.join(defaultPath, 'gepard.cfg')) -interface = "SIMULATED_RAMAN_CONTROL" +interface = "RENISHAW_CONTROL" + try: defaultPath = config["Defaults"]["file_path"] @@ -41,15 +43,33 @@ except KeyError: pass if interface == "SIMULATED_RAMAN_CONTROL": - from simulatedraman import SimulatedRaman + from ramancom.simulatedraman import SimulatedRaman RamanControl = SimulatedRaman print("WARNING: using only simulated raman control!") simulatedRaman = True + elif interface == "WITEC_CONTROL": - from WITecCOM import WITecCOM + from ramancom.WITecCOM import WITecCOM RamanControl = WITecCOM + RamanControl.magn = int(config["General Microscope Setup"]["magnification"]) # not yet implemented in WITecCOM, but would probably be a good idea... simulatedRaman = False + elif interface == "RENISHAW_CONTROL": - raise NotImplementedError + from ramancom.renishawcom import RenishawCOM + RamanControl = RenishawCOM + RamanControl.magn = int(config["General Microscope Setup"]["magnification"]) + try: + bf_dims = config["Renishaw"]["img_size_BF"].split('*') + df_dims = config["Renishaw"]["img_size_DF"].split('*') + RamanControl.cam_bf_dims = [float(bf_dims[0]), float(bf_dims[1])] + RamanControl.cam_df_dims = [float(df_dims[0]), float(df_dims[1])] + except: + print('Invalid image dimensions in config file!') +# RamanControl.defaultMeasTemplate = config["Renishaw"]["defaultMeasTemplate"] + RamanControl.measTemplatePath = config["Renishaw"]["measTemplatePath"] + print(RamanControl.measTemplatePath) + RamanControl.ramanParameters = RamanControl.updateRamanParameters(RamanControl, RamanControl.measTemplatePath) + simulatedRaman = False - \ No newline at end of file + + diff --git a/simulatedraman.py b/ramancom/simulatedraman.py similarity index 90% rename from simulatedraman.py rename to ramancom/simulatedraman.py index a85b1ff..8bea518 100644 --- a/simulatedraman.py +++ b/ramancom/simulatedraman.py @@ -24,11 +24,12 @@ Simualted Raman interface module for testing without actual raman system connect from time import sleep import numpy as np from shutil import copyfile - -from ramanbase import RamanBase +from ramancom.ramanbase import RamanBase class SimulatedRaman(RamanBase): + magn = 20 + ramanParameters = {} def __init__(self): super().__init__() self.currentpos = None, 0., 0. @@ -73,13 +74,13 @@ class SimulatedRaman(RamanBase): else: return self.currentZ - def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011, debugReturn=False, measurementRunning=False): assert self.connected if z is None: self.currentpos = x, y, self.currentpos[2] else: self.currentpos = x, y, z - sleep(1.) + sleep(0.1) def moveZto(self, z, epsz=0.011): assert self.connected @@ -91,7 +92,7 @@ class SimulatedRaman(RamanBase): self.imageindex = (self.imageindex+1)%(self.znum*self.gridnum) sleep(.01) - def getImageDimensions(self): + def getImageDimensions(self, mode = 'df'): """ Get the image width and height in um and the orientation angle in degrees. """ assert self.connected @@ -103,16 +104,16 @@ class SimulatedRaman(RamanBase): print("Fake scan") sleep(.3) - def initiateTimeSeriesScan(self, label, numberofscans, accumulations, integrtime): + def initiateMeasurement(self, label, numberofscans, accumulations, integrtime): assert self.connected print("Scanning ",numberofscans, "particle positions") self.timeseries = numberofscans - sleep(.3) + sleep(.1) - def nextTimeSeriesScan(self, num): + def triggerMeasurement(self, num): assert self.timeseries print("Scan number:", num) - sleep(.3) + sleep(.1) if num==self.timeseries-1: self.timeseries = False \ No newline at end of file diff --git a/witectesting.py b/ramancom/witectesting.py similarity index 100% rename from witectesting.py rename to ramancom/witectesting.py diff --git a/ramanscanui.py b/ramanscanui.py index a9ccaa1..ed9c27d 100644 --- a/ramanscanui.py +++ b/ramanscanui.py @@ -23,9 +23,9 @@ from PyQt5 import QtCore, QtWidgets import numpy as np from multiprocessing import Process, Queue, Event import queue -from time import sleep, time, localtime, strftime +from time import sleep, time +from external import tsp import datetime -from externalmodules import tsp import sys def reorder(points, N=20): @@ -46,32 +46,38 @@ def reorder(points, N=20): assert np.unique(newind).shape[0]==allind.shape[0] return newind -def scan(name, accu, inttime, positions, controlclass, dataqueue, stopevent): - +def scan(ramanSettings, positions, controlclass, dataqueue, stopevent): with open("ramanscanlog.txt", "a") as fp: sys.stderr = fp sys.stdout = fp ramanctrl = controlclass() ramanctrl.connect() - ramanctrl.initiateTimeSeriesScan(name, len(positions), accu, inttime) - print("starting Raman Scan at: " + strftime("%d %b %Y %H:%M:%S", localtime())) + print("connected:", time()) + ramanctrl.initiateMeasurement(ramanSettings) for i, p in enumerate(positions): x, y, z = p - - print('Measuring particle index {} at location {}, time = {}'.format(i, (x, y, z), strftime("%H:%M:%S", localtime()))) - sys.stdout.flush() #remove after testing + print("time:", time()) + print("position:", x, y, z) + sys.stdout.flush() #remove this line after testing ramanctrl.moveToAbsolutePosition(x, y, z) - ramanctrl.nextTimeSeriesScan(i) - + print("move done") + sys.stdout.flush() + ramanctrl.triggerMeasurement(i) + print("trigger done") + sys.stdout.flush() + if stopevent.is_set(): ramanctrl.disconnect() return + dataqueue.put(i) + ramanctrl.disconnect() + class RamanScanUI(QtWidgets.QWidget): - imageUpdate = QtCore.pyqtSignal(name='imageUpdate') + imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field) ramanscanUpdate = QtCore.pyqtSignal() def __init__(self, ramanctrl, dataset, parent=None): @@ -84,32 +90,37 @@ class RamanScanUI(QtWidgets.QWidget): vbox = QtWidgets.QVBoxLayout() hbox = QtWidgets.QHBoxLayout() - labelaccu = QtWidgets.QLabel("Accumulations:", self) - self.accumulationsedit = QtWidgets.QSpinBox(self) - self.accumulationsedit.setMinimum(1) - self.accumulationsedit.setMaximum(1000) - self.accumulationsedit.setValue(10) - self.accumulationsedit.setMinimumWidth(70) + self.params = [] + for param in self.ramanctrl.ramanParameters: +# if param.dtype == 'selectBtn': +# self.params.append(QtWidgets.QPushButton(str(param.value))) +# self.params[-1].released.connect(self.makeGetFnameLambda('Select template file', self.ramanctrl.measTemplatePath, param.openFileType, self.params[-1])) + if param.dtype == 'int': + self.params.append(QtWidgets.QSpinBox()) + self.params[-1].setMinimum(param.minVal) + self.params[-1].setMaximum(param.maxVal) + self.params[-1].setValue(param.value) + if param.dtype == 'double': + self.params.append(QtWidgets.QDoubleSpinBox()) + self.params[-1].setMinimum(param.minVal) + self.params[-1].setMaximum(param.maxVal) + self.params[-1].setValue(param.value) + if param.dtype == 'combobox': + self.params.append(QtWidgets.QComboBox()) + self.params[-1].addItems([str(i) for i in param.valList]) + - labelinttime = QtWidgets.QLabel("Integration time:", self) - self.inttimeedit = QtWidgets.QDoubleSpinBox(self) - self.inttimeedit.setMinimum(0.1) - self.inttimeedit.setMaximum(100.0) - self.inttimeedit.setDecimals(1) - self.inttimeedit.setValue(0.1) - self.inttimeedit.setSingleStep(0.1) - self.inttimeedit.setMinimumWidth(70) + self.group2 = QtWidgets.QGroupBox("Raman settings", self) + grid2 = QtWidgets.QFormLayout() - group2 = QtWidgets.QGroupBox("Raman settings", self) - grid2 = QtWidgets.QGridLayout() - grid2.addWidget(labelaccu, 0, 0, QtCore.Qt.AlignLeft) - grid2.addWidget(self.accumulationsedit, 0, 1, QtCore.Qt.AlignRight) - grid2.addWidget(labelinttime, 1, 0, QtCore.Qt.AlignLeft) - grid2.addWidget(self.inttimeedit, 1, 1, QtCore.Qt.AlignRight) + for index, param in enumerate(self.params): + param.setMinimumWidth(70) + grid2.addRow(QtWidgets.QLabel(self.ramanctrl.ramanParameters[index].name), param) + self.prun = QtWidgets.QPushButton("Raman scan", self) self.prun.released.connect(self.run) - grid2.addWidget(self.prun, 1, 2, QtCore.Qt.AlignRight) - group2.setLayout(grid2) + grid2.addRow(self.prun) + self.group2.setLayout(grid2) self.pexit = QtWidgets.QPushButton("Cancel", self) self.pexit.released.connect(self.stopScan) @@ -123,16 +134,25 @@ class RamanScanUI(QtWidgets.QWidget): hbox.addStretch() hbox.addWidget(self.pexit) - vbox.addWidget(group2) + vbox.addWidget(self.group2) vbox.addLayout(hbox) vbox.addWidget(self.progresstime) vbox.addWidget(self.progressbar) self.setLayout(vbox) - self.setWindowTitle("Particle Detection") + self.setWindowTitle("Raman Scan") #self.show() self.setVisible(False) - + + def makeGetFnameLambda(self, msg, path, fileType, btn): + return lambda : self.getFName(msg, path, fileType, btn) + + def getFName(self, msg, path, filetype, btn): + fname = QtWidgets.QFileDialog.getOpenFileName(self, msg, path, filetype)[0] + btn.setText(fname.split('\\')[-1]) + btn.setMinimumSize(btn.sizeHint()) + + def resetDataset(self, ds): self.dataset = ds if len(self.dataset.ramanpoints)>0: @@ -148,7 +168,6 @@ class RamanScanUI(QtWidgets.QWidget): QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.timer.stop() -# self.connection.send("stop") self.processstopevent.set() self.process.join() self.dataqueue.close() @@ -169,9 +188,15 @@ class RamanScanUI(QtWidgets.QWidget): self.dataset.readin = False else: return - accu = self.accumulationsedit.value() - inttime = self.inttimeedit.value() + self.view.imparent.ramanSwitch.hide() points = np.asarray(self.dataset.ramanpoints) + ramanSettings = {'filename': self.dataset.name, + 'numPoints': len(points), + '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: print(param) + lmin = None for i in range(20,41): c = reorder(points, i) @@ -182,7 +207,7 @@ class RamanScanUI(QtWidgets.QWidget): if len(points)<20000: cmin, T = tsp.tspcomp(np.double(points), np.int32(cmin)) assert np.all(np.sort(cmin)==np.arange(len(points), dtype=np.int32)) - scanpoints = np.array([self.dataset.mapToLengthRaman(p) for p in points[cmin,:]]) + scanpoints = np.array([self.dataset.mapToLengthRaman(p, microscopeMode=self.view.microscopeMode) for p in points[cmin,:]]) zmin, zmax = scanpoints[:,2].min(), scanpoints[:,2].max() softwarez = self.ramanctrl.getSoftwareZ() # get current software z zmin -= softwarez @@ -202,7 +227,7 @@ class RamanScanUI(QtWidgets.QWidget): self.view.scaleImage(2.0) self.view.highLightRamanIndex(0) self.view.blockUI() - self.prun.setEnabled(False) + self.group2.setEnabled(False) self.progresstime.setEnabled(True) self.progressbar.setEnabled(True) self.progressbar.setRange(0, len(scanpoints)) @@ -210,13 +235,13 @@ class RamanScanUI(QtWidgets.QWidget): self.ramanctrl.disconnect() self.processstopevent = Event() self.dataqueue = Queue() - self.process = Process(target=scan, args=(self.dataset.name, accu, inttime, scanpoints, self.ramanctrl.__class__, self.dataqueue, self.processstopevent)) + self.process = Process(target=scan, args=(ramanSettings, scanpoints, self.ramanctrl.__class__, self.dataqueue, self.processstopevent)) self.process.start() self.starttime = time() self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.checkOnScan) self.timer.setSingleShot(True) - self.timer.start(100.) + self.timer.start(10000.) @QtCore.pyqtSlot() def checkOnScan(self): @@ -224,7 +249,9 @@ class RamanScanUI(QtWidgets.QWidget): i = self.dataqueue.get_nowait() except queue.Empty: i = -1 - if i >= 0: + + if i>=0: + self.progressbar.setValue(i+1) self.view.highLightRamanIndex(i+1) Npoints = len(self.dataset.ramanpoints) @@ -246,5 +273,5 @@ class RamanScanUI(QtWidgets.QWidget): self.progresstime.setEnabled(False) self.close() return - self.timer.start(100.) + self.timer.start(100.) \ No newline at end of file diff --git a/sample_gepard.cfg b/sample_gepard.cfg index 2d227ba..1563666 100644 --- a/sample_gepard.cfg +++ b/sample_gepard.cfg @@ -6,4 +6,18 @@ file_path = . [Interface] # possible values: SIMULATED_RAMAN_CONTROL, WITEC_CONTROL, RENISHAW_CONTROL -raman_interface = SIMULATED_RAMAN_CONTROL \ No newline at end of file +raman_interface = SIMULATED_RAMAN_CONTROL + + +[General Microscope Setup] +magnification = 20 + +[Renishaw] +#information specific for renishaw control +#image mikrometer sizes for bright and dark field camera, format: width*height +#please insert, as it would represent an image at 1x magnification +#if unavailable, export an image from the Renishaw Camera viewer. Export as jpeg (without axes) and, within Gepard, go to Tools->ConfigureRamanControl. +img_size_BF = 9016.0*5748.0 +img_size_DF = 4524.0*2874.0 +#Path to measuring templates (*.wxm) +measTemplatePath = C:\RamanData \ No newline at end of file diff --git a/sampleview.py b/sampleview.py index e8fa994..e0e9909 100644 --- a/sampleview.py +++ b/sampleview.py @@ -22,13 +22,15 @@ from PyQt5 import QtCore, QtGui, QtWidgets import numpy as np import os from dataset import DataSet, loadData -from ramancontrol import RamanControl, simulatedRaman +from ramancom.ramancontrol import RamanControl, simulatedRaman from opticalscan import OpticalScan from ramanscanui import RamanScanUI from detectionview import ParticleDetectionView -from viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegementationContours +from analysis.analysisview import ParticleAnalysis +from viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContours from helperfunctions import polygoncovering, cv2imread_fix import cv2 +from ramancom.configRaman import RamanConfigWin class SampleView(QtWidgets.QGraphicsView): ScalingChanged = QtCore.pyqtSignal(float) @@ -51,6 +53,18 @@ class SampleView(QtWidgets.QGraphicsView): self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) self.ramanctrl = RamanControl() + self.simulatedRaman = simulatedRaman + #determine, if ramanSwitch is needed: + self.ramanctrl.connect() + if not self.ramanctrl.connected: + QtWidgets.QMessageBox.warning(self, 'Error', 'Please enable Raman Connection') + return + if self.ramanctrl.getImageDimensions(mode='bf')[0] == self.ramanctrl.getImageDimensions(mode='df')[0]: + self.ramanSwitchNeeded = False + else: + self.ramanSwitchNeeded = True + self.ramanctrl.disconnect() + self.drag = None self.mode = None self.dataset = None @@ -60,7 +74,7 @@ class SampleView(QtWidgets.QGraphicsView): self.ramanscanitems = [] self.imgdata = None self.isblocked = False - self.contouritem = SegementationContours() + self.contouritem = SegmentationContours(self) scene.addItem(self.contouritem) self.detectionwidget = None self.ramanwidget = RamanScanUI(self.ramanctrl, None, self) @@ -68,8 +82,37 @@ class SampleView(QtWidgets.QGraphicsView): self.oscanwidget = OpticalScan(self.ramanctrl, None, self) self.oscanwidget.imageUpdate.connect(self.loadPixmap) self.oscanwidget.boundaryUpdate.connect(self.resetBoundary) + self.analysiswidget = None self.setMinimumSize(600,600) + self.darkenPixmap = False + self.microscopeMode = None + + def takeScreenshot(self): + #TODO: + #LIMIT SCREENSHOT TO ACTUAL VIEWSIZE OF LOADED IMAGE... + #hide scrollbars + self.setHorizontalScrollBarPolicy(1) + self.setVerticalScrollBarPolicy(1) + #capture screen + screen = QtWidgets.QApplication.primaryScreen() + self.repaint() + screenshot = screen.grabWindow(self.winId()) + #unhide scrollbars + self.setHorizontalScrollBarPolicy(0) + self.setVerticalScrollBarPolicy(0) + fname = self.dataset.path + '/screenshot.png' + validFileName = False + incr = 1 + while not validFileName: + if not os.path.exists(fname): + validFileName = True + else: + fname = self.dataset.path + '/screenshot ({}).png'.format(incr) + incr += 1 + screenshot.save(fname , 'png') + QtWidgets.QMessageBox.about(self, 'Message', 'Saved as {} to project directory.'.format(fname.split('/')[-1])) + def closeEvent(self, event): reply = QtWidgets.QMessageBox.question(self, 'Message', "Are you sure to quit?", QtWidgets.QMessageBox.Yes | @@ -82,10 +125,16 @@ class SampleView(QtWidgets.QGraphicsView): self.oscanwidget.close() if self.detectionwidget is not None: self.detectionwidget.close() + if self.analysiswidget is not None: + self.analysiswidget.close() self.ramanwidget.close() else: event.ignore() - + + def configureRamanControl(self): + self.configWin = RamanConfigWin(self) + self.configWin.show() + def saveDataSet(self): if self.dataset is not None: self.dataset.save() @@ -106,7 +155,7 @@ class SampleView(QtWidgets.QGraphicsView): @QtCore.pyqtSlot() def fitToWindow(self): - print("fitting") + print("fitting to Window") brect = self.item.sceneBoundingRect() self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) self.scaleFactor = self.transform().m11() @@ -123,39 +172,69 @@ class SampleView(QtWidgets.QGraphicsView): self.ramanwidget.setVisible(False) self.contouritem.resetContours([]) self.mode = mode - self.loadPixmap() + self.loadPixmap(self.microscopeMode) if mode == "OpticalScan": self.oscanwidget.setVisible(True) self.oscanwidget.resetDataset(self.dataset) + elif mode == "ParticleDetection": - self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) - self.detectionwidget.show() - self.detectionwidget.imageUpdate.connect(self.detectionUpdate) - self.detectionwidget.detectionFinished.connect(self.activateMaxMode) + if self.detectionwidget is None: + self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) + self.detectionwidget.show() + self.detectionwidget.imageUpdate.connect(self.detectionUpdate) + self.detectionwidget.detectionFinished.connect(self.activateMaxMode) + elif mode == "RamanScan": self.ramanwidget.resetDataset(self.dataset) self.ramanwidget.setVisible(True) + + elif mode == "ParticleAnalysis": + if self.ramanwidget.isVisible(): + self.ramanwidget.setVisible(False) + if self.analysiswidget is None: + self.analysiswidget = ParticleAnalysis(self) + self.analysiswidget.showMaximized() + else: + self.analysiswidget.showMaximized() + +# self.ramanwidget.setVisible(False) + if self.detectionwidget is not None: + self.detectionwidget.setVisible(False) + + #show legend: + self.imparent.legend.show() + if loadnew: self.fitToWindow() self.imparent.updateModes(mode, self.getMaxMode()) - def open(self, fname): + def open(self, fname): self.saveDataSet() - if self.dataset is not None: + if self.dataset is not None: #####TODO: properly close and delete all widget instances for avoiding that data from a currently opened dataset is saved into the new... self.dataset.save() self.dataset = loadData(fname) + self.setMicroscopeMode() self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) self.imgdata = None self.activateMaxMode(loadnew=True) + self.imparent.snapshotAct.setEnabled(True) def new(self, fname): self.saveDataSet() if self.dataset is not None: self.dataset.save() self.dataset = DataSet(fname, newProject=True) + self.setMicroscopeMode() self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) self.imgdata = None self.activateMaxMode(loadnew=True) + self.imparent.snapshotAct.setEnabled(True) + + def setMicroscopeMode(self): + if self.ramanSwitchNeeded: + self.imparent.ramanSwitch.connectToSampleView() + self.imparent.ramanSwitch.show() + self.microscopeMode = ('df' if self.imparent.ramanSwitch.df_btn.isChecked() else 'bf') @QtCore.pyqtSlot() def activateMaxMode(self, loadnew=False): @@ -184,12 +263,13 @@ class SampleView(QtWidgets.QGraphicsView): maxmode = "ParticleDetection" if len(self.dataset.ramanpoints)>0: maxmode = "RamanScan" - if self.dataset.ramanscandone: + if self.dataset.ramanscandone: #uncomment!! maxmode = "ParticleAnalysis" return maxmode def mousePressEvent(self, event): - if event.button()==QtCore.Qt.RightButton: +# if event.button()==QtCore.Qt.RightButton: + if event.button()==QtCore.Qt.MiddleButton: self.drag = event.pos() elif event.button()==QtCore.Qt.LeftButton and self.mode in ["OpticalScan", "RamanScan"] \ and event.modifiers()==QtCore.Qt.ControlModifier: @@ -204,13 +284,18 @@ class SampleView(QtWidgets.QGraphicsView): self.dataset.readin = False else: return - x, y, z = self.dataset.mapToLengthRaman([p0.x(), p0.y()], noz=(False if self.mode=="RamanScan" else True)) + x, y, z = self.dataset.mapToLengthRaman([p0.x(), p0.y()], + microscopeMode=self.microscopeMode, + noz=(False if self.mode=="RamanScan" else True)) if z is not None: assert z>-100. self.ramanctrl.moveToAbsolutePosition(x, y, z) elif event.button()==QtCore.Qt.LeftButton and self.mode=="ParticleDetection": p0 = self.mapToScene(event.pos()) self.detectionwidget.setImageCenter([p0.x(), p0.y()]) +# elif event.button()==QtCore.Qt.LeftButton and self.mode=="ParticleAnalysis": +# p0 = self.mapToScene(event.pos()) + else: p0 = self.mapToScene(event.pos()) super(SampleView, self).mousePressEvent(event) @@ -221,6 +306,7 @@ class SampleView(QtWidgets.QGraphicsView): move = self.drag-p0 self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) + self.drag = p0 else: super(SampleView, self).mouseMoveEvent(event) @@ -243,10 +329,11 @@ class SampleView(QtWidgets.QGraphicsView): self.announceScaling() def announceScaling(self): - if self.dataset is None or self.dataset.pixelscale is None: + pixelscale = (self.dataset.pixelscale_df if self.microscopeMode == 'df' else self.dataset.pixelscale_bf) + if self.dataset is None or pixelscale is None: self.ScalingChanged.emit(-1.0) else: - self.ScalingChanged.emit(self.dataset.pixelscale/self.scaleFactor) + self.ScalingChanged.emit(pixelscale/self.scaleFactor) ##CURRENTLY ONLY DARKFIELD!!! FIX NEEDED!!! def connectRaman(self): if not self.ramanctrl.connect(): @@ -262,14 +349,14 @@ class SampleView(QtWidgets.QGraphicsView): self.ramanctrl.disconnect() self.imparent.updateConnected(self.ramanctrl.connected) - @QtCore.pyqtSlot() + @QtCore.pyqtSlot(str) def detectionUpdate(self): self.contouritem.resetContours(self.dataset.particlecontours) self.prepareAnalysis() self.update() - @QtCore.pyqtSlot() - def loadPixmap(self): + @QtCore.pyqtSlot(str) + def loadPixmap(self, microscope_mode='df'): self.clearItems() if self.dataset is None: self.item.setPixmap(QtGui.QPixmap()) @@ -282,17 +369,28 @@ class SampleView(QtWidgets.QGraphicsView): data = cv2.cvtColor(cv2imread_fix(fname), cv2.COLOR_BGR2RGB) self.imgdata = data if data is not None: + height, width, channel = data.shape bytesPerLine = 3 * width pix = QtGui.QPixmap() pix.convertFromImage(QtGui.QImage(data.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) + self.item.setPixmap(pix) + + if self.darkenPixmap: + self.scene().setBackgroundBrush(QtGui.QColor(5, 5, 5)) + self.item.setOpacity(0.2) + else: + self.scene().setBackgroundBrush(QtCore.Qt.darkGray) + self.item.setOpacity(1) + + else: self.item.setPixmap(QtGui.QPixmap()) if self.mode == "OpticalScan": for i, p in zip(self.dataset.fitindices, self.dataset.fitpoints): - p = self.dataset.mapToPixel(p, force=True) + p = self.dataset.mapToPixel(p, mode=microscope_mode, force=True) fititem = FitPosIndicator(i+1, pos=p) self.scene().addItem(fititem) self.fititems.append(fititem) @@ -303,17 +401,22 @@ class SampleView(QtWidgets.QGraphicsView): @QtCore.pyqtSlot() def resetScanPositions(self): + micMode = ('df' if self.oscanwidget.df_btn.isChecked() else 'bf') for item in self.scanitems: self.scene().removeItem(item) edges, nodes = self.boundaryitems boundary = [] for n in nodes: p = n.pos().x(), n.pos().y() - boundary.append(self.dataset.mapToLength(p, force=True)) + boundary.append(self.dataset.mapToLength(p, self.microscopeMode, force=True)) boundary = np.array(boundary) - print(boundary) +# print(boundary) self.dataset.boundary = boundary - width, height, angle = self.dataset.imagedim + if micMode == 'df': + width, height, angle = self.dataset.imagedim_df + else: + width, height, angle = self.dataset.imagedim_bf + margin = min(width, height)*0.02 wx, wy = width-margin, height-margin print(wx,wy) @@ -324,16 +427,21 @@ class SampleView(QtWidgets.QGraphicsView): if len(p2)0: data = [] for i in self.dataset.ramanscansortindex: - data.append(list(self.dataset.ramanpoints[i])+list(self.dataset.particlestats[i])) +# data.append(list(self.dataset.ramanpoints[i])+list(self.dataset.particlestats[i])) + data.append(list(self.dataset.ramanpoints[i])) #particlestats are not needed here. Plus, they dont need to match spectra indices anymore.. for i in range(len(data)): item = RamanScanIndicator(self, i+1, 20, (data[i][0],data[i][1])) self.scene().addItem(item) @@ -372,9 +517,17 @@ class SampleView(QtWidgets.QGraphicsView): for item in self.ramanscanitems: item.setHighLight(False) self.ramanscanitems[index].setHighLight(True) - self.ensureVisible(self.ramanscanitems[index]) - # alternatively use self.centerOn(self.ramanscanitems[index]) + def centerOnRamanIndex(self, index, centerOn=True, highlightContour=True): + if centerOn: + self.centerOn(self.ramanscanitems[index]) + +# if highlightContour: +# self.contouritem.selectedContours = [] +# else: +# self.ensureVisible(self.ramanscanitems[index]) + + def clearItems(self): for item in self.fititems: self.scene().removeItem(item) @@ -390,4 +543,5 @@ class SampleView(QtWidgets.QGraphicsView): self.boundaryitems = [], [] for item in self.ramanscanitems: self.scene().removeItem(item) - self.ramanscanitems = [] \ No newline at end of file + self.ramanscanitems = [] + \ No newline at end of file diff --git a/scalebar.py b/scalebar.py index 8e7d8e2..c3d7b2f 100755 --- a/scalebar.py +++ b/scalebar.py @@ -78,16 +78,16 @@ class ScaleBar(QtWidgets.QMdiSubWindow): qp.drawRect(0,0,WX,WY) qp.setPen(QtGui.QColor(0,0,0)) qp.setBrush(QtGui.QColor(0,0,100)) - y0, dy = 40, 35 + y0, dy = 50, 25 if self.divisor is None: qp.drawRect(10,y0,WX-20,dy) else: font = qp.font() font.setPointSize(30) qp.setFont(font) - qp.drawText((WX-self.wscale)//2,5,self.wscale,dy, - QtCore.Qt.AlignCenter, str(int(self.divisor))+" µm") qp.drawRect((WX-self.wscale)//2,y0,self.wscale,dy) + qp.drawText((WX-self.wscale)//2,5,self.wscale,dy+20, + QtCore.Qt.AlignCenter, str(int(self.divisor))+" µm") qp.end() diff --git a/segmentation.py b/segmentation.py index 17dbab7..158d696 100644 --- a/segmentation.py +++ b/segmentation.py @@ -55,7 +55,7 @@ class Segmentation(object): 'minparticlearea': 20, 'minparticledistance': 20, 'measurefrac': 1, - 'compactness': 0.1, + 'compactness': 0., 'seedRad': 3} self.initialParameters() @@ -345,20 +345,18 @@ class Segmentation(object): sure_bg = self.closeHoles(sure_bg) # modify sure_fg and sure_bg with seedpoints and deletepoints - if len(deletepoints)>0: h, w = sure_fg.shape[:2] mask = np.zeros((h+2, w+2), np.uint8) for p in np.int32(deletepoints): if 0 < p[0] < h and 0 < p[1] < w: #point has to be within image, otherwise the floodFill fails - cv2.floodFill(sure_fg, mask, tuple([p[0], p[1]]), 0) + cv2.floodFill(sure_fg, mask, tuple([p[0], p[1]]), 0) for p in np.int32(seedpoints): cv2.circle(sure_fg, tuple([p[0], p[1]]), int(p[2]), 1, -1) for p in np.int32(deletepoints): cv2.circle(sure_fg, tuple([p[0], p[1]]), int(p[2]), 0, -1) cv2.circle(sure_bg, tuple([p[0], p[1]]), int(p[2]), 0, -1) - print("sure_fg, sure_bg") if self.cancelcomputation: diff --git a/viewitems.py b/viewitems.py index 80d87ec..d37de3e 100644 --- a/viewitems.py +++ b/viewitems.py @@ -21,13 +21,17 @@ If not, see . import numpy as np from PyQt5 import QtCore, QtWidgets, QtGui -class SegementationContours(QtWidgets.QGraphicsItem): - def __init__(self, contours=[], pos=(0,0)): +class SegmentationContours(QtWidgets.QGraphicsItem): + def __init__(self, parent=None, contours=[], pos=(0,0)): super().__init__() + self.parent = parent self.setPos(pos[0], pos[1]) - self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) + self.setAcceptedMouseButtons(QtCore.Qt.AllButtons) self.brect = QtCore.QRectF(0,0,1,1) self.resetContours(contours) + self.colorList = [] + self.selectedContours = [] def boundingRect(self): return self.brect @@ -57,12 +61,96 @@ class SegementationContours(QtWidgets.QGraphicsItem): self.contours = cp self.update() - def paint(self, painter, option, widget): + def paint(self, painter, option, widget): painter.setPen(QtCore.Qt.green) - for c in self.contours: - painter.drawPolygon(c) + for index, c in enumerate(self.contours): + if index not in self.selectedContours: + color = self.colorList[index] + painter.setBrush(color) + painter.setPen(QtGui.QColor(int(color.red()*0.7), int(color.green()*0.7), int(color.blue()*0.7), color.alpha())) + painter.drawPolygon(c) + else: + alpha = self.colorList[index].alpha() + painter.setBrush(QtGui.QColor(200, 200, 200, alpha)) + painter.setPen(QtCore.Qt.white) + painter.drawPolygon(c) + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.LeftButton: + p = event.pos() + p = QtCore.QPointF(p.x(), p.y()) + for index, cnt in enumerate(self.contours): + if cnt.containsPoint(p, QtCore.Qt.OddEvenFill): + + if event.modifiers()==QtCore.Qt.ShiftModifier: + self.selectedContours.append(index) + else: + self.selectedContours = [index] + + self.parent.selectContour(index, centerOn=False) + + self.update() + return + self.selectedContours = [] #reset selection, if nothing was hit... + self.update() + + def contextMenuEvent(self, event): + contextMenu = QtWidgets.QMenu("Particle options") + + combineMenu = QtWidgets.QMenu("Combine Particles into") + + combineActs = [] + assignments = [] + for index in self.selectedContours: +# partIndex = int(np.where(self.parent.dataset.ramanscansortindex == index)[0]) + partIndex = index + assignments.append(self.parent.analysiswidget.particleResults[partIndex]) + assignments.append("other") + for assignment in np.unique(np.array(assignments)): + combineActs.append(combineMenu.addAction(assignment)) + + reassignActs = [] + reassignMenu = QtWidgets.QMenu("Reassign particle(s) into") + for polymer in self.parent.analysiswidget.uniquePolymers: + reassignActs.append(reassignMenu.addAction(polymer)) + reassignActs.append(reassignMenu.addAction("other")) + + contextMenu.addMenu(combineMenu) + contextMenu.addMenu(reassignMenu) + deleteAct = contextMenu.addAction("Delete Particle(s)") + + numParticles = len(self.selectedContours) + if numParticles == 0: + reassignMenu.setDisabled(True) + combineMenu.setDisabled(True) + deleteAct.setDisabled(True) + elif numParticles == 1: + combineMenu.setDisabled(True) + + + action = contextMenu.exec_(event.screenPos()) + + if action == deleteAct: + print('deleting') + elif action in combineActs: + newAssignment = action.text() + if newAssignment == "other": + QtWidgets.QMessageBox.about(self.parent, "Not yet implemented", "we are getting there...") + return + + self.parent.analysiswidget.editor.combineParticles(self.selectedContours, newAssignment) + + elif action in reassignActs: + newAssignment = action.text() + if newAssignment == "other": + QtWidgets.QMessageBox.about(self.parent, "Not yet implemented", "we are getting there...") + return + + self.parent.analysiswidget.editor.reassignParticles(self.selectedContours, newAssignment) + + class FitPosIndicator(QtWidgets.QGraphicsItem): indicatorSize = 80 def __init__(self, number, pos=(0,0)): @@ -76,15 +164,15 @@ class FitPosIndicator(QtWidgets.QGraphicsItem): 2*self.indicatorSize+2,2*self.indicatorSize+2) def paint(self, painter, option, widget): - painter.setPen(QtCore.Qt.green) - painter.setBrush(QtGui.QColor(250,250,0,150)) - rect = QtCore.QRectF(-self.indicatorSize,-self.indicatorSize, - 2*self.indicatorSize,2*self.indicatorSize) - font = painter.font() - font.setPointSize(40) - painter.setFont(font) - painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) - painter.drawEllipse(rect) + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtGui.QColor(250,250,0,150)) + rect = QtCore.QRectF(-self.indicatorSize,-self.indicatorSize, + 2*self.indicatorSize,2*self.indicatorSize) + font = painter.font() + font.setPointSize(40) + painter.setFont(font) + painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) + painter.drawEllipse(rect) def shape(self): path = QtGui.QPainterPath() @@ -94,11 +182,13 @@ class FitPosIndicator(QtWidgets.QGraphicsItem): class RamanScanIndicator(QtWidgets.QGraphicsItem): def __init__(self, view, number, radius, pos=(0,0)): super().__init__() - self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) +# self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) self.view = view self.number = number self.radius = radius self.highlight = False + self.hidden = False self.setPos(pos[0], pos[1]) def setHighLight(self, highlight): @@ -116,27 +206,27 @@ class RamanScanIndicator(QtWidgets.QGraphicsItem): return path def paint(self, painter, option, widget): - if self.highlight: - painter.setPen(QtCore.Qt.red) - painter.setBrush(QtGui.QColor(100,250,100,150)) - else: - painter.setPen(QtCore.Qt.green) - painter.setBrush(QtGui.QColor(50,50,250,150)) - rect = QtCore.QRectF(-self.radius, -self.radius, 2*self.radius, 2*self.radius) - painter.drawEllipse(rect) - font = painter.font() - font.setPointSize(10) - painter.setFont(font) - painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) + if not self.hidden: + if self.highlight: + painter.setPen(QtCore.Qt.red) + painter.setBrush(QtGui.QColor(100,250,100,150)) + else: + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtGui.QColor(50,50,250,150)) + rect = QtCore.QRectF(-self.radius, -self.radius, 2*self.radius, 2*self.radius) + painter.drawEllipse(rect) + font = painter.font() + font.setPointSize(10) + painter.setFont(font) + painter.drawText(rect, QtCore.Qt.AlignCenter, str(self.number)) - def mousePressEvent(self, event): - p = event.pos() - x, y = p.x(), p.y() - r = np.sqrt(x**2+y**2) - print(self.number, r) - if r