# -*- 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 from analysis.colorlegend import getColorFromNameWithSeed from helperfunctions import polygoncovering, cv2imread_fix from ramancom.configRaman import RamanConfigWin from analysis.particleeditor import ParticleEditor class SampleView(QtWidgets.QGraphicsView): ScalingChanged = QtCore.pyqtSignal(float) 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.disableSelection = False 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.imgdata = None self.isblocked = False self.contourItems = [] self.selectedParticleIndices = [] 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 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 | 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): 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.scaleImage(1.25) @QtCore.pyqtSlot() def zoomOut(self): self.scaleImage(0.8) @QtCore.pyqtSlot() def normalSize(self): self.scaleFactor = 1.0 self.setTransform(QtGui.QTransform.fromScale(1., 1.)) self.announceScaling() @QtCore.pyqtSlot() def fitToWindow(self): brect = self.item.sceneBoundingRect() self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) self.scaleFactor = self.transform().m11() self.announceScaling() def switchMode(self, mode, loadnew=False): if mode is None: return assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] self.oscanwidget.setVisible(False) if self.detectionwidget is not None: self.detectionwidget.close() self.detectionwidget.destroy() 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.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: 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() # 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): 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): def tryDisconnectingSignal(signal): try: signal.disconnect() except TypeError: pass if self.particleEditor is None: self.particleEditor = ParticleEditor(self, self.dataset.particleContainer) #try disconnecting the signals. If they are connected multiple times, the functions will run accordingly... tryDisconnectingSignal(self.particleEditor.particleContoursChanged) tryDisconnectingSignal(self.particleEditor.particleAssignmentChanged) self.particleEditor.particleContoursChanged.connect(self.resetParticleContours) if self.analysiswidget is not None: self.particleEditor.particleContoursChanged.connect(self.analysiswidget.updateDisplays) self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updateDisplays) 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): mode = self.getMaxMode() self.imparent.updateModes(self.mode, self.getMaxMode()) self.switchMode(mode, loadnew=loadnew) def blockUI(self): self.isblocked = True self.imparent.blockUI() def unblockUI(self): self.isblocked = False self.imparent.unblockUI(self.ramanctrl.connected) self.imparent.updateModes(self.mode, self.getMaxMode()) def getMaxMode(self): if self.dataset is None: return None if not self.ramanctrl.connected: self.connectRaman() if not self.ramanctrl.connected: return None maxmode = "OpticalScan" if os.path.exists(self.dataset.getImageName()): maxmode = "ParticleDetection" if len(self.dataset.ramanpoints)>0: maxmode = "RamanScan" if self.dataset.ramanscandone: maxmode = "ParticleAnalysis" return maxmode def mousePressEvent(self, event): if not self.disableSelection: if event.button()==QtCore.Qt.MiddleButton: self.drag = event.pos() elif event.button()==QtCore.Qt.LeftButton: self.checkForContourSelection(event) event.ignore() elif event.button()==QtCore.Qt.LeftButton and self.mode in ["OpticalScan", "RamanScan"] \ and event.modifiers()==QtCore.Qt.ControlModifier: p0 = self.mapToScene(event.pos()) if self.dataset is not None and self.dataset.pshift is not None: if self.dataset.readin: reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!', "Coordinate systems might have changed since. Do you want to continue with saved coordinates?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self.dataset.readin = False else: return # x, y, z = self.dataset.mapToLengthRaman([p0.x(), p0.y()], # microscopeMode=self.microscopeMode, # noz=(False if self.mode=="RamanScan" else True)) x, y, z = self.dataset.mapToLengthRaman([p0.x(), p0.y()], microscopeMode=self.microscopeMode, noz=False) if z is not None: assert z>-100. self.ramanctrl.moveToAbsolutePosition(x, y, z) elif event.button()==QtCore.Qt.LeftButton and self.mode=="ParticleDetection": p0 = self.mapToScene(event.pos()) self.detectionwidget.setImageCenter([p0.x(), p0.y()]) else: p0 = self.mapToScene(event.pos()) super(SampleView, self).mousePressEvent(event) def mouseMoveEvent(self, event): if self.drag is not None: p0 = event.pos() move = self.drag-p0 self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) self.drag = p0 else: super(SampleView, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): self.drag = None super(SampleView, self).mouseReleaseEvent(event) def wheelEvent(self, event): factor = 1.01**(event.angleDelta().y()/8) self.scaleImage(factor) def checkForContourSelection(self, event): ''' Iam not yet happy how this selection is handled. I would prefer having it in the viewitems themselves, but somehow the sampleview has to check for already selected indices and also check when nothing is hit... ''' def addContourToSelection(cnt): cnt.isSelected = True cnt.update() 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: addContourToSelection(cnt) self.analysiswidget.selectParticleIndex(cnt.particleIndex, centerOn=False) else: if cnt.particleIndex not in self.selectedParticleIndices: addContourToSelection(cnt) self.analysiswidget.selectParticleIndex(cnt.particleIndex, centerOn=False) elif cnt.particleIndex in self.selectedParticleIndices: removeContourFromSelection(cnt) else: #not clicked on particle if event.modifiers()!=QtCore.Qt.ShiftModifier and cnt.particleIndex in self.selectedParticleIndices: removeContourFromSelection(cnt) self.update() def scaleImage(self, factor): if factor<1 and not self.imparent.zoomOutAct.isEnabled(): return if factor>1 and not self.imparent.zoomInAct.isEnabled(): return self.scaleFactor *= factor self.scale(factor, factor) self.announceScaling() def announceScaling(self): 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): self.resetParticleContours() self.prepareAnalysis() self.update() @QtCore.pyqtSlot(str) def loadPixmap(self, microscope_mode='df'): 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) 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() @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, 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)0: data = [] for i in self.dataset.ramanscansortindex: data.append(list(self.dataset.ramanpoints[i])) for i in range(len(data)): item = RamanScanIndicator(self, i+1, 20, (data[i][0],data[i][1])) self.scene().addItem(item) self.ramanscanitems.append(item) def resetParticleContours(self): t0 = time.time() for cnt in self.contourItems: self.scene().removeItem(cnt) self.contourItems = [] if self.dataset is not None: for particleIndex, contour in enumerate(self.dataset.particleContainer.getParticleContours()): newCnt = SegmentationContour(self, contour) newCnt.setIndex(particleIndex) assignment = self.dataset.particleContainer.getParticleAssignmentByIndex(particleIndex) color = getColorFromNameWithSeed(assignment, self.dataset.colorSeed) newCnt.setColor(QtGui.QColor(color[0], color[1], color[2], 255)) self.contourItems.append(newCnt) self.scene().addItem(newCnt) self.update() print('resetted contours:', round((time.time()-t0)*1000)) def updateLegend(self, legendItems): self.imparent.legend.setTextColorItems(legendItems) def highLightRamanIndex(self, index): if index < len(self.ramanscanitems): for item in self.ramanscanitems: item.setHighLight(False) self.ramanscanitems[index].setHighLight(True) def centerOnRamanIndex(self, index, centerOn=True, highlightContour=True): if centerOn: self.centerOn(self.ramanscanitems[index]) if highlightContour: self.highLightRamanIndex(index) def clearItems(self): for item in self.fititems: self.scene().removeItem(item) self.fititems = [] for item in self.scanitems: self.scene().removeItem(item) edges, nodes = self.boundaryitems for item in edges: self.scene().removeItem(item) for item in nodes: self.scene().removeItem(item) self.scanitems = [] self.boundaryitems = [], [] for item in self.ramanscanitems: self.scene().removeItem(item) self.ramanscanitems = []