# -*- coding: utf-8 -*- """ GEPARD - Gepard-Enabled PARticle Detection Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Polymerforschung Dresden e. V. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ from PyQt5 import QtCore, QtGui, QtWidgets import numpy as np import os import cv2 import time from .dataset import DataSet, loadData from .ramancom.ramancontrol import RamanControl, simulatedRaman from .opticalscan import OpticalScan from .ramanscanui import RamanScanUI from .detectionview import ParticleDetectionView from .analysis.analysisview import ParticleAnalysis from .zeissimporter import ZeissImporter from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo from .helperfunctions import polygoncovering, cv2imread_fix from .analysis.colorlegend import getColorFromNameWithSeed from .analysis.particleEditor import ParticleEditor from .ramancom.configRaman import RamanConfigWin class SampleView(QtWidgets.QGraphicsView): ScalingChanged = QtCore.pyqtSignal(float) ParticleOfIndexSelected = QtCore.pyqtSignal(int) def __init__(self, logpath): super(SampleView, self).__init__() self.logpath = logpath self.item = QtWidgets.QGraphicsPixmapItem() self.item.setPos(0, 0) self.item.setAcceptedMouseButtons(QtCore.Qt.NoButton) self.scaleFactor = 1.0 scene = QtWidgets.QGraphicsScene(self) scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) scene.addItem(self.item) scene.setBackgroundBrush(QtCore.Qt.darkGray) self.setScene(scene) self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) self.setRenderHint(QtGui.QPainter.Antialiasing) self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) self.ramanctrl = RamanControl() self.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 self.particleEditor = None self.fititems = [] self.boundaryitems = [[],[]] self.scanitems = [] self.ramanscanitems = [] self.particleInfoBox = None self.imgdata = None self.isblocked = False self.contourItems = [] self.selectedParticleIndices = [] self.particlePainter = None self.detectionwidget = None self.ramanwidget = RamanScanUI(self.ramanctrl, None, self.logpath, self) self.ramanwidget.imageUpdate.connect(self.loadPixmap) self.oscanwidget = OpticalScan(self.ramanctrl, None, self.logpath, 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 self.update() 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', "Do you really want to quit?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.disconnectRaman() self.saveDataSet() event.accept() self.oscanwidget.close() if self.detectionwidget is not None: self.detectionwidget.close() if self.analysiswidget is not None: self.analysiswidget.close() self.ramanwidget.close() else: event.ignore() def configureRamanControl(self): """ Launches a window for updating Raman instrument configuration. :return: """ self.configWin = RamanConfigWin(self) self.configWin.show() def saveDataSet(self): if self.dataset is not None: self.dataset.save() @QtCore.pyqtSlot() def zoomIn(self): self.zoomDisplay(1.25) @QtCore.pyqtSlot() def zoomOut(self): self.zoomDisplay(0.8) @QtCore.pyqtSlot() def normalSize(self): self.scaleFactor = 1.0 self.setTransform(QtGui.QTransform.fromScale(1., 1.)) self.announceScaling() @QtCore.pyqtSlot() def fitToWindow(self): """ Fits the window to show the entire sample. :return: """ brect = self.item.sceneBoundingRect() self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) self.scaleFactor = self.transform().m11() self.announceScaling() def switchMode(self, mode, loadnew=False): """ Switch the gepard to another module :return: """ if mode is None: return assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] print("switching to mode:", mode, flush=True) self.oscanwidget.setVisible(False) if self.detectionwidget is not None: self.detectionwidget.close() self.detectionwidget.destroy() self.detectionwidget = None self.ramanwidget.setVisible(False) self.mode = mode self.loadPixmap(self.microscopeMode) if mode == "OpticalScan": self.oscanwidget.setVisible(True) self.oscanwidget.resetDataset(self.dataset) elif mode == "ParticleDetection": if self.detectionwidget is None: self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) self.detectionwidget.imageUpdate.connect(self.detectionUpdate) self.detectionwidget.detectionFinished.connect(self.activateMaxMode) self.detectionwidget.show() 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: print('creating new analysiswidget') self.analysiswidget = ParticleAnalysis(self.dataset, self) self.analysiswidget.showMaximized() self.setupParticleEditor() else: print('show maximized already exisiting analysiswidget') self.analysiswidget.showMaximized() 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): self.saveDataSet() #close all widgets for widget in [self.detectionwidget, self.ramanwidget, self.oscanwidget, self.analysiswidget]: if widget is not None: widget.close() widget.destroy() del widget self.dataset = loadData(fname) self.setupParticleEditor() 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 importProject(self, fname): zimp = ZeissImporter(fname, self.ramanctrl, self) if zimp.validimport: zimp.exec() if zimp.result() == QtWidgets.QDialog.Accepted: self.open(zimp.gepardname) def new(self, fname): self.saveDataSet() if self.dataset is not None: self.dataset.save() self.dataset = DataSet(fname, newProject=True) self.setupParticleEditor() 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 setupParticleEditor(self): """ Setting up the particle editor for editing properties or contours of particles. It needs some connections to sampleview and analysisview. """ def tryDisconnectingSignal(signal): try: signal.disconnect() except TypeError: pass if self.particleEditor is None: self.particleEditor = ParticleEditor(self, self.dataset.particleContainer) tryDisconnectingSignal(self.particleEditor.particleAssignmentChanged) if self.analysiswidget is not None: self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updatePlotsAndContours) self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updateWidgetContents) def setMicroscopeMode(self): """ The opical microscope can be in either Brightfield (bf) or Darkfield (df) mode. In the case of the Renishaw instrument this mode affects the current image size in µm. Hence, gepard needs to be aware of the current mode :return: """ 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): """ Activates the "highest" available mode of gepard. The modes follow gepard's operation workflow: OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis :return: """ mode = self.getMaxMode() self.imparent.updateModes(self.mode, self.getMaxMode()) self.switchMode(mode, loadnew=loadnew) def getMaxMode(self): """ Determines the "highest" available mode of gepard. The modes follow gepard's operation workflow: OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis :return: """ if self.dataset is None: return None if not self.ramanctrl.connected: self.connectRaman() if not self.ramanctrl.connected: return None maxmode = "OpticalScan" if os.path.exists(self.dataset.getImageName()): maxmode = "ParticleDetection" if self.dataset.particleDetectionDone: maxmode = "RamanScan" if self.dataset.ramanscandone: maxmode = "ParticleAnalysis" return maxmode def blockUI(self): self.isblocked = True self.imparent.blockUI() def unblockUI(self): self.isblocked = False self.imparent.unblockUI(self.ramanctrl.connected) self.imparent.updateModes(self.mode, self.getMaxMode()) def mousePressEvent(self, event): if event.button()==QtCore.Qt.MiddleButton: self.drag = event.pos() if self.particlePainter is None: if event.button()==QtCore.Qt.LeftButton: if self.analysiswidget is not None: self.checkForContourSelection(event) if self.mode in ["OpticalScan", "RamanScan"] and event.modifiers()==QtCore.Qt.ControlModifier: p0 = self.mapToScene(event.pos()) self.moveStageToPosition(p0) elif self.mode=="ParticleDetection": p0 = self.mapToScene(event.pos()) self.detectionwidget.setImageCenter([p0.x(), p0.y()]) else: p0 = self.mapToScene(event.pos()) super(SampleView, self).mousePressEvent(event) else: self.particlePainter.mousePressEvent(event) def mouseMoveEvent(self, event): if self.drag is not None: p0 = event.pos() move = self.drag-p0 self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) self.drag = p0 elif self.particlePainter is None: p0 = self.mapToScene(event.pos()) super(SampleView, self).mouseMoveEvent(event) else: self.particlePainter.mouseMoveEvent(event) def mouseReleaseEvent(self, event): self.drag = None if self.particlePainter is None: super(SampleView, self).mouseReleaseEvent(event) else: self.particlePainter.mouseReleaseEvent(event) def wheelEvent(self, event): if self.particlePainter is None: factor = 1.01**(event.angleDelta().y()/8) self.zoomDisplay(factor) else: self.particlePainter.wheelEvent(event) def keyPressEvent(self, event): if self.particlePainter is not None: self.particlePainter.keyPressEvent(event) def moveStageToPosition(self, pos): """ Sends a command to the ramancontrol to move the microscope stage to the position of the click event. :return: """ if self.dataset is not None and self.dataset.pshift is not None: if self.dataset.readin: reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!', "Coordinate systems might have changed since. Do you want to continue with saved coordinates?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.dataset.readin = False else: return noz = (self.mode in ['OpticalScan', 'RamanScan']) x, y, z = self.dataset.mapToLengthRaman([pos.x(), pos.y()], microscopeMode=self.microscopeMode, noz=noz) if z is not None: assert z>-100. self.ramanctrl.moveToAbsolutePosition(x, y, z) def checkForContourSelection(self, event): """ Checks, if at the given event position any contour is selected. :return: """ def acceptSelection(cnt): self.ParticleOfIndexSelected.emit(cnt.particleIndex) cnt.isSelected = True cnt.update() if cnt.particleIndex not in self.selectedParticleIndices: self.selectedParticleIndices.append(cnt.particleIndex) def removeContourFromSelection(cnt): cnt.isSelected = False cnt.update() self.selectedParticleIndices.remove(cnt.particleIndex) p = self.mapToScene(event.pos()) p = QtCore.QPointF(p.x(), p.y()) for index, cnt in enumerate(self.contourItems): if cnt.polygon.containsPoint(p, QtCore.Qt.OddEvenFill): #clicked on particle if not event.modifiers()==QtCore.Qt.ShiftModifier: acceptSelection(cnt) else: if cnt.particleIndex not in self.selectedParticleIndices: acceptSelection(cnt) elif cnt.particleIndex in self.selectedParticleIndices: removeContourFromSelection(cnt) else: #not clicked on particle if event.modifiers()!=QtCore.Qt.ShiftModifier: cnt.isSelected = False cnt.update() if cnt.particleIndex in self.selectedParticleIndices: self.selectedParticleIndices.remove(cnt.particleIndex) if len(self.selectedParticleIndices) > 0: self.updateParticleInfoBox(self.selectedParticleIndices[-1]) else: self.removeParticleInfoBox() self.update() def zoomDisplay(self, factor): """ Zooms the GraphicsView in or out, according to the given factor. :return: """ if factor<1 and not self.imparent.zoomOutAct.isEnabled(): return if factor>1 and not self.imparent.zoomInAct.isEnabled(): return self.scaleFactor *= factor self.scale(factor, factor) self.announceScaling() def announceScaling(self): """ Processes a new scaling of the GraphicsView. :return: """ pixelscale = self.dataset.getPixelScale(self.microscopeMode) if self.dataset is None or pixelscale is None: self.ScalingChanged.emit(-1.0) else: self.ScalingChanged.emit(pixelscale/self.scaleFactor) ##CURRENTLY ONLY DARKFIELD!!! FIX NEEDED!!! def connectRaman(self): if not self.ramanctrl.connect(): msg = QtWidgets.QMessageBox() msg.setText("Connection failed! Please enable remote control.") msg.exec() else: mode = self.getMaxMode() self.switchMode(mode) self.imparent.updateConnected(self.ramanctrl.connected) def disconnectRaman(self): self.ramanctrl.disconnect() self.imparent.updateConnected(self.ramanctrl.connected) @QtCore.pyqtSlot(str) def detectionUpdate(self): """ Is connected to particle detection. When a new segmentation result was obtained, the sampleview has to update here. :return: """ self.resetParticleContours() self.prepareAnalysis() self.update() @QtCore.pyqtSlot(str) def loadPixmap(self, microscope_mode='df'): """ Loads the pixmap image as background for the GraphicsView. It is stored in self.imgdata :return: """ self.clearItems() if self.dataset is None: self.item.setPixmap(QtGui.QPixmap()) else: data = self.imgdata fname = self.dataset.getImageName() if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": self.resetParticleContours() if data is None and os.path.exists(fname): # data = cv2.cvtColor(cv2imread_fix(fname), cv2.COLOR_BGR2RGB) ##With this line the B and R channel are swapped, which leads to a wrong presentation of the image in gepard..... Why was this line here? data = cv2imread_fix(fname) 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, mode=microscope_mode, force=True) fititem = FitPosIndicator(i+1, pos=p) self.scene().addItem(fititem) self.fititems.append(fititem) if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": self.prepareAnalysis() else: self.fitToWindow() def prepareAnalysis(self): """ Is called after final particle detection. Raman Scan Indicators are added. """ self.clearItems() if self.dataset.particleDetectionDone: for meas in self.dataset.particleContainer.measurements: if meas.ramanScanIndex is not None: number = meas.ramanScanIndex+1 item = RamanScanIndicator(self, number, 20, (meas.pixelcoord_x, meas.pixelcoord_y)) self.scene().addItem(item) self.ramanscanitems.append(item) def clearItems(self): """ Removes items from the GraphicsScene. ContourItems remain, however.. :return: """ for itemList in [self.fititems, self.scanitems, self.ramanscanitems]: for item in itemList: self.scene().removeItem(item) itemList = [] edges, nodes = self.boundaryitems for item in edges: self.scene().removeItem(item) for item in nodes: self.scene().removeItem(item) self.boundaryitems = [], [] @QtCore.pyqtSlot() def resetScanPositions(self): """ Calculates position of scan tiles for the optical scan, according to the boundary and the polygoncovering method. The corresponding graphic items are added to the scene. :return: """ 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, self.microscopeMode, force=True)) boundary = np.array(boundary) self.dataset.boundary = boundary 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) p1 = polygoncovering(boundary, wx, wy) b2 = boundary.copy() b2 = b2[:,[1,0]] p2 = polygoncovering(b2, wy, wx) if len(p2)