particleEditor.py 12.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018  Lars Bittrich and Josef Brandt, Leibniz-Institut für 
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>    

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 <https://www.gnu.org/licenses/>.
"""
Lars Bittrich's avatar
Lars Bittrich committed
21

22
import numpy as np
JosefBrandt's avatar
 
JosefBrandt committed
23
from PyQt5 import QtWidgets, QtCore
24

JosefBrandt's avatar
JosefBrandt committed
25
from .particlePainter import ParticlePainter
26
import analysis.particleCharacterization as pc
27
from errors import NotConnectedContoursError
JosefBrandt's avatar
 
JosefBrandt committed
28

JosefBrandt's avatar
 
JosefBrandt committed
29 30 31
class ParticleContextMenu(QtWidgets.QMenu):
    combineParticlesSignal = QtCore.pyqtSignal(list, str)
    reassignParticlesSignal = QtCore.pyqtSignal(list, str)
JosefBrandt's avatar
 
JosefBrandt committed
32
    paintParticlesSignal = QtCore.pyqtSignal(list, str)
33
    changeParticleColorSignal = QtCore.pyqtSignal(list, str)
34
    changeParticleShapeSignal = QtCore.pyqtSignal(list, str)
JosefBrandt's avatar
JosefBrandt committed
35 36
    deleteParticlesSignal = QtCore.pyqtSignal(list)
    
JosefBrandt's avatar
 
JosefBrandt committed
37
    def __init__(self, viewparent):
JosefBrandt's avatar
 
JosefBrandt committed
38
        super(ParticleContextMenu, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
39 40 41
        self.viewparent = viewparent
        self.selectedParticleIndices = self.viewparent.selectedParticleIndices
        self.particleContainer = self.viewparent.dataset.particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
42 43
        
    def executeAtScreenPos(self, screenPos):
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
        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):
JosefBrandt's avatar
 
JosefBrandt committed
65 66
        self.combineActs = []
        self.combineMenu = QtWidgets.QMenu("Combine Particles into")
JosefBrandt's avatar
 
JosefBrandt committed
67 68 69 70 71

        self.paintActs = []
        self.paintMenu = QtWidgets.QMenu("Paint Mode, merge into")

        selctedAssignments = []
JosefBrandt's avatar
 
JosefBrandt committed
72 73

        for particleIndex in self.selectedParticleIndices:
JosefBrandt's avatar
 
JosefBrandt committed
74 75 76 77
            try:
                assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex)
            except:
                return
JosefBrandt's avatar
 
JosefBrandt committed
78
            selctedAssignments.append(assignment)
JosefBrandt's avatar
 
JosefBrandt committed
79
        
JosefBrandt's avatar
 
JosefBrandt committed
80
        for assignment in np.unique(selctedAssignments):
JosefBrandt's avatar
 
JosefBrandt committed
81
            self.combineActs.append(self.combineMenu.addAction(assignment))
JosefBrandt's avatar
 
JosefBrandt committed
82
            self.paintActs.append(self.paintMenu.addAction(assignment))
JosefBrandt's avatar
 
JosefBrandt committed
83
        self.combineActs.append(self.combineMenu.addAction("other"))
JosefBrandt's avatar
 
JosefBrandt committed
84 85
        self.paintActs.append(self.paintMenu.addAction("other"))
        
JosefBrandt's avatar
 
JosefBrandt committed
86 87 88 89 90 91 92 93 94 95 96 97 98 99
        
        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)
            
100 101 102 103 104
            
        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))
105 106 107 108 109
            
        self.shapeMenu = QtWidgets.QMenu("Set Particle Shape To")
        self.shapeActs = []
        for shape in ['fibre', 'spherule', 'irregular', 'flake']:
            self.shapeActs.append(self.shapeMenu.addAction(shape))
110 111 112
        
        infoAct = self.addAction(f'selected {numParticles} particles')
        infoAct.setDisabled(True)
JosefBrandt's avatar
 
JosefBrandt committed
113 114
        self.addMenu(self.combineMenu)        
        self.addMenu(self.reassignMenu)
JosefBrandt's avatar
 
JosefBrandt committed
115
        self.addMenu(self.paintMenu)
116
        self.addSeparator()
117
        self.addMenu(self.colorMenu)
118 119
        self.addMenu(self.shapeMenu)
        self.addSeparator()
JosefBrandt's avatar
JosefBrandt committed
120
        self.deleteAct = self.addAction("Delete particle(s)")
121
    
122
    def validifyText(self, assignment):
123 124 125 126 127
        if assignment == "other":
            assignment = self.getNewEntry()
        return assignment
    
    def getNewEntry(self):
JosefBrandt's avatar
 
JosefBrandt committed
128
        text, okClicked = QtWidgets.QInputDialog.getText(self.viewparent, "Custom assignment", "Enter new assignment")
129 130
        if okClicked and text != '':
            return text
JosefBrandt's avatar
 
JosefBrandt committed
131 132 133


class ParticleEditor(QtCore.QObject):
JosefBrandt's avatar
 
JosefBrandt committed
134
    particleAssignmentChanged = QtCore.pyqtSignal()
JosefBrandt's avatar
 
JosefBrandt committed
135
    def __init__(self, viewparent, particleContainer):
JosefBrandt's avatar
 
JosefBrandt committed
136
        super(ParticleEditor, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
137
        self.particleContainer = particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
138
        self.viewparent = viewparent #the assigned analysis widget
Hackmet's avatar
Hackmet committed
139 140 141
        self.backupFreq = 3     #save a backup every n actions
        self.neverBackedUp = True
        self.actionCounter = 0
JosefBrandt's avatar
 
JosefBrandt committed
142 143 144
        
        self.storedIndices = []
        self.storedAssignmend = None
JosefBrandt's avatar
 
JosefBrandt committed
145 146 147 148
    
    def connectToSignals(self, contextMenu):
        contextMenu.combineParticlesSignal.connect(self.combineParticles)
        contextMenu.reassignParticlesSignal.connect(self.reassignParticles)
JosefBrandt's avatar
 
JosefBrandt committed
149
        contextMenu.paintParticlesSignal.connect(self.paintParticles)
150
        contextMenu.changeParticleColorSignal.connect(self.changeParticleColors)
151
        contextMenu.changeParticleShapeSignal.connect(self.changeParticleShapes)
JosefBrandt's avatar
JosefBrandt committed
152
        contextMenu.deleteParticlesSignal.connect(self.deleteParticles)
JosefBrandt's avatar
 
JosefBrandt committed
153
    
154
    def createSafetyBackup(self):
Hackmet's avatar
Hackmet committed
155 156
        self.actionCounter += 1
        if self.actionCounter == self.backupFreq-1 or self.neverBackedUp:            
JosefBrandt's avatar
 
JosefBrandt committed
157
            backupname = self.viewparent.dataset.saveBackup()
Hackmet's avatar
Hackmet committed
158
            print('backing up as', backupname)
Hackmet's avatar
Hackmet committed
159
            self.neverBackedUp = False
JosefBrandt's avatar
 
JosefBrandt committed
160
            self.actionCounter = 0        
Hackmet's avatar
Hackmet committed
161
    
JosefBrandt's avatar
 
JosefBrandt committed
162
    @QtCore.pyqtSlot(list, str)
163 164 165 166
    def combineParticles(self, contourIndices, newAssignment):     
        self.createSafetyBackup()
        print(f'Combining particles {contourIndices} into {newAssignment}')
        contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
JosefBrandt's avatar
JosefBrandt committed
167
        try:
168
            newContour = pc.mergeContours(contours) 
169
        except NotConnectedContoursError:
170
            QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 
171
                                       'Particle contours are not connected.\nThat is currently not supported!')
JosefBrandt's avatar
JosefBrandt committed
172
            return
173

JosefBrandt's avatar
JosefBrandt committed
174
        self.mergeParticlesInParticleContainerAndSampleView(contourIndices, newContour, newAssignment)
175
    
JosefBrandt's avatar
 
JosefBrandt committed
176
    @QtCore.pyqtSlot(list, str)
177
    def reassignParticles(self, contourindices, newAssignment):
178
        self.createSafetyBackup()
179
        print(f'reassigning indices {contourindices} into {newAssignment}')
180
        for partIndex in contourindices:
181
            self.particleContainer.reassignParticleToAssignment(partIndex, newAssignment)
JosefBrandt's avatar
JosefBrandt committed
182 183
        
        self.viewparent.updateParticleInfoBox(partIndex)
JosefBrandt's avatar
 
JosefBrandt committed
184
        self.particleAssignmentChanged.emit()
JosefBrandt's avatar
 
JosefBrandt committed
185 186 187
     
    @QtCore.pyqtSlot(list, str)
    def paintParticles(self, contourIndices, newAssignment):
JosefBrandt's avatar
JosefBrandt committed
188 189
        print(f'painting indices {contourIndices} into {newAssignment}')
        self.createSafetyBackup()
JosefBrandt's avatar
JosefBrandt committed
190
        self.viewparent.removeParticleInfoBox()
JosefBrandt's avatar
 
JosefBrandt committed
191 192
        self.storedIndices = contourIndices
        self.storedAssignmend = newAssignment
Josef Brandt's avatar
Josef Brandt committed
193
        
JosefBrandt's avatar
 
JosefBrandt committed
194
        contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
195
        img, xmin, ymin, self.padding = pc.contoursToImg(contours, padding=0)
196
        topLeft = [ymin, xmin]
Josef Brandt's avatar
Josef Brandt committed
197 198
        self.particlePainter = ParticlePainter(self, img, topLeft)
        
JosefBrandt's avatar
 
JosefBrandt committed
199 200 201 202 203 204
        self.viewparent.normalSize()
        self.viewparent.particlePainter = self.particlePainter
        self.viewparent.scene().addItem(self.particlePainter)
        self.viewparent.update()
        
    def acceptPaintedResult(self):
JosefBrandt's avatar
JosefBrandt committed
205
        try:
206 207 208
            img = self.particlePainter.img
            xmin = self.particlePainter.topLeft[1]
            ymin = self.particlePainter.topLeft[0]
209
            newContour = pc.imgToCnt(img, xmin, ymin, 0)
210
        except NotConnectedContoursError:
211
            QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 
212
                                       'Particle contours are not connected.\nThat is currently not supported!')
JosefBrandt's avatar
JosefBrandt committed
213
            self.viewparent.updateParticleInfoBox(self.storedIndices[-1])
JosefBrandt's avatar
JosefBrandt committed
214 215 216 217
            self.storedIndices = []
            self.storedAssignmend = None
            self.destroyParticlePainter()
            return
JosefBrandt's avatar
 
JosefBrandt committed
218
        
219
        self.mergeParticlesInParticleContainerAndSampleView(self.storedIndices, newContour, self.storedAssignmend)
JosefBrandt's avatar
 
JosefBrandt committed
220 221 222 223 224 225 226 227 228 229 230
        
        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
231
    
232
    def mergeParticlesInParticleContainerAndSampleView(self, indices, newContour, assignment):
233
        stats = pc.getParticleStatsWithPixelScale(newContour, self.viewparent.imgdata, self.viewparent.dataset)
234
        
JosefBrandt's avatar
JosefBrandt committed
235 236
        self.viewparent.addParticleContourToIndex(newContour, len(self.viewparent.contourItems)-1)
        self.particleContainer.addMergedParticle(indices, newContour, stats, newAssignment=assignment)
237
        
JosefBrandt's avatar
JosefBrandt committed
238 239 240
        for ind in sorted(indices, reverse=True):
            self.viewparent.removeParticleContour(ind)
            self.particleContainer.removeParticle(ind)
241
        
JosefBrandt's avatar
JosefBrandt committed
242 243
        self.viewparent.resetContourIndices()
        self.particleContainer.resetParticleIndices()
JosefBrandt's avatar
JosefBrandt committed
244
        self.viewparent.updateParticleInfoBox(self.particleContainer.getNumberOfParticles()-1)
JosefBrandt's avatar
JosefBrandt committed
245
        self.particleAssignmentChanged.emit()
246
    
247 248 249 250 251
    @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)
JosefBrandt's avatar
JosefBrandt committed
252
        self.viewparent.updateParticleInfoBox(partIndex)
253 254 255 256 257 258
            
    @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)
JosefBrandt's avatar
JosefBrandt committed
259
        self.viewparent.updateParticleInfoBox(partIndex)
260
    
JosefBrandt's avatar
JosefBrandt committed
261 262 263 264 265 266
    @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:
Hackmet's avatar
Hackmet committed
267

JosefBrandt's avatar
JosefBrandt committed
268 269 270 271 272 273 274
            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()
JosefBrandt's avatar
JosefBrandt committed
275 276


277

JosefBrandt's avatar
JosefBrandt committed
278
        
Hackmet's avatar
Hackmet committed
279