# -*- coding: utf-8 -*- """ GEPARD - Gepard-Enabled PARticle Detection Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Polymerforschung Dresden e. V. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ import numpy as np from PyQt5 import QtWidgets, QtCore from .particlePainter import ParticlePainter import analysis.particleCharacterization as pc from errors import NotConnectedContoursError class ParticleContextMenu(QtWidgets.QMenu): combineParticlesSignal = QtCore.pyqtSignal(list, str) reassignParticlesSignal = QtCore.pyqtSignal(list, str) paintParticlesSignal = QtCore.pyqtSignal(list, str) changeParticleColorSignal = QtCore.pyqtSignal(list, str) changeParticleShapeSignal = QtCore.pyqtSignal(list, str) deleteParticlesSignal = QtCore.pyqtSignal(list) def __init__(self, viewparent): super(ParticleContextMenu, self).__init__() self.viewparent = viewparent self.selectedParticleIndices = self.viewparent.selectedParticleIndices self.particleContainer = self.viewparent.dataset.particleContainer def executeAtScreenPos(self, screenPos): self.createActsAndMenus() action = self.exec_(screenPos) if action: actionText = self.validifyText(action.text()) if action in self.combineActs: self.combineParticlesSignal.emit(self.selectedParticleIndices, actionText) elif action in self.reassignActs: self.reassignParticlesSignal.emit(self.selectedParticleIndices, actionText) elif action in self.paintActs: self.paintParticlesSignal.emit(self.selectedParticleIndices, actionText) elif action in self.colorActs: self.changeParticleColorSignal.emit(self.selectedParticleIndices, actionText) elif action in self.shapeActs: self.changeParticleShapeSignal.emit(self.selectedParticleIndices, actionText) elif action == self.deleteAct: self.deleteParticlesSignal.emit(self.selectedParticleIndices) def createActsAndMenus(self): self.combineActs = [] self.combineMenu = QtWidgets.QMenu("Combine Particles into") self.paintActs = [] self.paintMenu = QtWidgets.QMenu("Paint Mode, merge into") selctedAssignments = [] for particleIndex in self.selectedParticleIndices: try: assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex) except: return selctedAssignments.append(assignment) for assignment in np.unique(selctedAssignments): self.combineActs.append(self.combineMenu.addAction(assignment)) self.paintActs.append(self.paintMenu.addAction(assignment)) self.combineActs.append(self.combineMenu.addAction("other")) self.paintActs.append(self.paintMenu.addAction("other")) self.reassignActs = [] self.reassignMenu = QtWidgets.QMenu("Reassign particle(s) into") for polymType in self.particleContainer.getUniquePolymers(): self.reassignActs.append(self.reassignMenu.addAction(polymType)) self.reassignActs.append(self.reassignMenu.addAction("other")) numParticles = len(self.selectedParticleIndices) if numParticles == 0: self.reassignMenu.setDisabled(True) self.combineMenu.setDisabled(True) elif numParticles == 1: self.combineMenu.setDisabled(True) self.colorMenu = QtWidgets.QMenu("Set Particle Color To") self.colorActs = [] for color in ['white', 'black', 'blue', 'brown', 'green', 'grey', 'non-determinable', 'red', 'transparent', 'yellow']: self.colorActs.append(self.colorMenu.addAction(color)) self.shapeMenu = QtWidgets.QMenu("Set Particle Shape To") self.shapeActs = [] for shape in ['fibre', 'spherule', 'irregular', 'flake']: self.shapeActs.append(self.shapeMenu.addAction(shape)) infoAct = self.addAction(f'selected {numParticles} particles') infoAct.setDisabled(True) self.addMenu(self.combineMenu) self.addMenu(self.reassignMenu) self.addMenu(self.paintMenu) self.addSeparator() self.addMenu(self.colorMenu) self.addMenu(self.shapeMenu) self.addSeparator() self.deleteAct = self.addAction("Delete particle(s)") def validifyText(self, assignment): if assignment == "other": assignment = self.getNewEntry() return assignment def getNewEntry(self): text, okClicked = QtWidgets.QInputDialog.getText(self.viewparent, "Custom assignment", "Enter new assignment") if okClicked and text != '': return text class ParticleEditor(QtCore.QObject): particleAssignmentChanged = QtCore.pyqtSignal() def __init__(self, viewparent, particleContainer): super(ParticleEditor, self).__init__() self.particleContainer = particleContainer self.viewparent = viewparent #the assigned analysis widget self.backupFreq = 3 #save a backup every n actions self.neverBackedUp = True self.actionCounter = 0 self.storedIndices = [] self.storedAssignmend = None def connectToSignals(self, contextMenu): contextMenu.combineParticlesSignal.connect(self.combineParticles) contextMenu.reassignParticlesSignal.connect(self.reassignParticles) contextMenu.paintParticlesSignal.connect(self.paintParticles) contextMenu.changeParticleColorSignal.connect(self.changeParticleColors) contextMenu.changeParticleShapeSignal.connect(self.changeParticleShapes) contextMenu.deleteParticlesSignal.connect(self.deleteParticles) def createSafetyBackup(self): self.actionCounter += 1 if self.actionCounter == self.backupFreq-1 or self.neverBackedUp: backupname = self.viewparent.dataset.saveBackup() print('backing up as', backupname) self.neverBackedUp = False self.actionCounter = 0 @QtCore.pyqtSlot(list, str) def combineParticles(self, contourIndices, newAssignment): self.createSafetyBackup() print(f'Combining particles {contourIndices} into {newAssignment}') contours = self.particleContainer.getParticleContoursByIndex(contourIndices) try: newContour = pc.mergeContours(contours) except NotConnectedContoursError: QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 'Particle contours are not connected.\nThat is currently not supported!') return self.mergeParticlesInParticleContainerAndSampleView(contourIndices, newContour, newAssignment) @QtCore.pyqtSlot(list, str) def reassignParticles(self, contourindices, newAssignment): self.createSafetyBackup() print(f'reassigning indices {contourindices} into {newAssignment}') for partIndex in contourindices: self.particleContainer.reassignParticleToAssignment(partIndex, newAssignment) self.viewparent.updateParticleInfoBox(partIndex) self.particleAssignmentChanged.emit() @QtCore.pyqtSlot(list, str) def paintParticles(self, contourIndices, newAssignment): print(f'painting indices {contourIndices} into {newAssignment}') self.createSafetyBackup() self.viewparent.removeParticleInfoBox() self.storedIndices = contourIndices self.storedAssignmend = newAssignment contours = self.particleContainer.getParticleContoursByIndex(contourIndices) img, xmin, ymin, self.padding = pc.contoursToImg(contours, padding=0) topLeft = [ymin, xmin] self.particlePainter = ParticlePainter(self, img, topLeft) self.viewparent.normalSize() self.viewparent.particlePainter = self.particlePainter self.viewparent.scene().addItem(self.particlePainter) self.viewparent.update() def acceptPaintedResult(self): try: img = self.particlePainter.img xmin = self.particlePainter.topLeft[1] ymin = self.particlePainter.topLeft[0] newContour = pc.imgToCnt(img, xmin, ymin, 0) except NotConnectedContoursError: QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 'Particle contours are not connected.\nThat is currently not supported!') self.viewparent.updateParticleInfoBox(self.storedIndices[-1]) self.storedIndices = [] self.storedAssignmend = None self.destroyParticlePainter() return self.mergeParticlesInParticleContainerAndSampleView(self.storedIndices, newContour, self.storedAssignmend) self.storedIndices = [] self.storedAssignmend = None self.destroyParticlePainter() def destroyParticlePainter(self): if self.particlePainter is not None: self.viewparent.particlePainter = None self.viewparent.scene().removeItem(self.particlePainter) self.viewparent.update() self.particlePainter = None def mergeParticlesInParticleContainerAndSampleView(self, indices, newContour, assignment): stats = pc.getParticleStatsWithPixelScale(newContour, self.viewparent.imgdata, self.viewparent.dataset) self.viewparent.addParticleContourToIndex(newContour, len(self.viewparent.contourItems)-1) self.particleContainer.addMergedParticle(indices, newContour, stats, newAssignment=assignment) for ind in sorted(indices, reverse=True): self.viewparent.removeParticleContour(ind) self.particleContainer.removeParticle(ind) self.viewparent.resetContourIndices() self.particleContainer.resetParticleIndices() self.viewparent.updateParticleInfoBox(self.particleContainer.getNumberOfParticles()-1) self.particleAssignmentChanged.emit() @QtCore.pyqtSlot(list, str) def changeParticleColors(self, contourIndices, newColor): print(f'changing color of particles {contourIndices} into {newColor}') for partIndex in contourIndices: self.particleContainer.changeParticleColor(partIndex, newColor) self.viewparent.updateParticleInfoBox(partIndex) @QtCore.pyqtSlot(list, str) def changeParticleShapes(self, contourIndices, newShape): print(f'changing shape of particles {contourIndices} into {newShape}') for partIndex in contourIndices: self.particleContainer.changeParticleShape(partIndex, newShape) self.viewparent.updateParticleInfoBox(partIndex) @QtCore.pyqtSlot(list) def deleteParticles(self, contourIndices): reply = QtWidgets.QMessageBox.question(self, f'About to delete {len(contourIndices)} particles.', "Are you sure to permanantly delete these particles?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: for partIndex in sorted(contourIndices, reverse=True): self.viewparent.removeParticleContour(partIndex) self.viewparent.dataset.particleContainer.removeParticles([partIndex]) self.viewparent.dataset.particleContainer.resetParticleIndices() self.viewparent.resetContourIndices() self.viewparent.analysiswidget.updateHistogramsAndContours()