particleeditor.py 10 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# -*- 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. <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
27

28 29
import numpy as np
import cv2
JosefBrandt's avatar
 
JosefBrandt committed
30
from PyQt5 import QtWidgets, QtCore
31

JosefBrandt's avatar
 
JosefBrandt committed
32 33
from analysis.particlePainter import ParticlePainter

JosefBrandt's avatar
 
JosefBrandt committed
34 35 36
class ParticleContextMenu(QtWidgets.QMenu):
    combineParticlesSignal = QtCore.pyqtSignal(list, str)
    reassignParticlesSignal = QtCore.pyqtSignal(list, str)
JosefBrandt's avatar
 
JosefBrandt committed
37
    paintParticlesSignal = QtCore.pyqtSignal(list, str)
JosefBrandt's avatar
 
JosefBrandt committed
38
    def __init__(self, viewparent):
JosefBrandt's avatar
 
JosefBrandt committed
39
        super(ParticleContextMenu, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
40 41 42
        self.viewparent = viewparent
        self.selectedParticleIndices = self.viewparent.selectedParticleIndices
        self.particleContainer = self.viewparent.dataset.particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
43 44 45 46
        
    def executeAtScreenPos(self, screenPos):
        self.combineActs = []
        self.combineMenu = QtWidgets.QMenu("Combine Particles into")
JosefBrandt's avatar
 
JosefBrandt committed
47 48 49 50 51

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

        selctedAssignments = []
JosefBrandt's avatar
 
JosefBrandt committed
52 53

        for particleIndex in self.selectedParticleIndices:
JosefBrandt's avatar
 
JosefBrandt committed
54 55 56 57
            try:
                assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex)
            except:
                return
JosefBrandt's avatar
 
JosefBrandt committed
58
            selctedAssignments.append(assignment)
JosefBrandt's avatar
 
JosefBrandt committed
59
        
JosefBrandt's avatar
 
JosefBrandt committed
60
        for assignment in np.unique(selctedAssignments):
JosefBrandt's avatar
 
JosefBrandt committed
61
            self.combineActs.append(self.combineMenu.addAction(assignment))
JosefBrandt's avatar
 
JosefBrandt committed
62
            self.paintActs.append(self.paintMenu.addAction(assignment))
JosefBrandt's avatar
 
JosefBrandt committed
63
        self.combineActs.append(self.combineMenu.addAction("other"))
JosefBrandt's avatar
 
JosefBrandt committed
64 65
        self.paintActs.append(self.paintMenu.addAction("other"))
        
JosefBrandt's avatar
 
JosefBrandt committed
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
        
        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.addMenu(self.combineMenu)        
        self.addMenu(self.reassignMenu)
JosefBrandt's avatar
 
JosefBrandt committed
82
        self.addMenu(self.paintMenu)
JosefBrandt's avatar
 
JosefBrandt committed
83 84 85
        
        action = self.exec_(screenPos)

JosefBrandt's avatar
 
JosefBrandt committed
86
        if action:
87
            newAssignment = self.validifyAssignment(action.text())
JosefBrandt's avatar
 
JosefBrandt committed
88 89 90 91 92 93 94

            if action in self.combineActs:
                self.combineParticlesSignal.emit(self.selectedParticleIndices, newAssignment)
            elif action in self.reassignActs:
                self.reassignParticlesSignal.emit(self.selectedParticleIndices, newAssignment)
            elif action in self.paintActs:
                self.paintParticlesSignal.emit(self.selectedParticleIndices, newAssignment)
95 96 97 98 99 100 101
    
    def validifyAssignment(self, assignment):
        if assignment == "other":
            assignment = self.getNewEntry()
        return assignment
    
    def getNewEntry(self):
JosefBrandt's avatar
 
JosefBrandt committed
102
        text, okClicked = QtWidgets.QInputDialog.getText(self.viewparent, "Custom assignment", "Enter new assignment")
103 104
        if okClicked and text != '':
            return text
JosefBrandt's avatar
 
JosefBrandt committed
105 106 107


class ParticleEditor(QtCore.QObject):
108
#    particleContoursChanged = QtCore.pyqtSignal()
JosefBrandt's avatar
 
JosefBrandt committed
109
    particleAssignmentChanged = QtCore.pyqtSignal()
JosefBrandt's avatar
 
JosefBrandt committed
110
    def __init__(self, viewparent, particleContainer):
JosefBrandt's avatar
 
JosefBrandt committed
111
        super(ParticleEditor, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
112
        self.particleContainer = particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
113
        self.viewparent = viewparent #the assigned analysis widget
Hackmet's avatar
Hackmet committed
114 115 116
        self.backupFreq = 3     #save a backup every n actions
        self.neverBackedUp = True
        self.actionCounter = 0
JosefBrandt's avatar
 
JosefBrandt committed
117 118 119
        
        self.storedIndices = []
        self.storedAssignmend = None
JosefBrandt's avatar
 
JosefBrandt committed
120 121 122 123
    
    def connectToSignals(self, contextMenu):
        contextMenu.combineParticlesSignal.connect(self.combineParticles)
        contextMenu.reassignParticlesSignal.connect(self.reassignParticles)
JosefBrandt's avatar
 
JosefBrandt committed
124
        contextMenu.paintParticlesSignal.connect(self.paintParticles)
JosefBrandt's avatar
 
JosefBrandt committed
125
    
126
    def createSafetyBackup(self):
Hackmet's avatar
Hackmet committed
127 128
        self.actionCounter += 1
        if self.actionCounter == self.backupFreq-1 or self.neverBackedUp:            
JosefBrandt's avatar
 
JosefBrandt committed
129
            backupname = self.viewparent.dataset.saveBackup()
Hackmet's avatar
Hackmet committed
130
            print('backing up as', backupname)
Hackmet's avatar
Hackmet committed
131
            self.neverBackedUp = False
JosefBrandt's avatar
 
JosefBrandt committed
132
            self.actionCounter = 0        
Hackmet's avatar
Hackmet committed
133
    
JosefBrandt's avatar
 
JosefBrandt committed
134
    @QtCore.pyqtSlot(list, str)
135 136 137 138 139 140 141 142
    def combineParticles(self, contourIndices, newAssignment):     
        self.createSafetyBackup()
        print(f'Combining particles {contourIndices} into {newAssignment}')
        contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
        newContour = self.mergeContours(contours.copy())        
        stats = self.characterizeParticle(newContour)

        self.particleContainer.mergeParticles(contourIndices, newContour, stats, newAssignment=newAssignment)
JosefBrandt's avatar
 
JosefBrandt committed
143 144 145 146 147 148
        for ind in contourIndices:
            self.viewparent.removeParticleContour(ind)
        
        self.viewparent.resetContourIndices()
        self.viewparent.addParticleContour(newContour, len(self.viewparent.contourItems))
        
149
        self.particleAssignmentChanged.emit()
JosefBrandt's avatar
 
JosefBrandt committed
150
        #TODO: INCLUDE SANITY CHECK!!!!!!!!!
151
    
JosefBrandt's avatar
 
JosefBrandt committed
152
    @QtCore.pyqtSlot(list, str)
153
    def reassignParticles(self, contourindices, newAssignment):
154
        self.createSafetyBackup()
155
        print(f'reassigning indices {contourindices} into {newAssignment}')
156
        for partIndex in contourindices:
157
            self.particleContainer.reassignParticleToAssignment(partIndex, newAssignment)
158

JosefBrandt's avatar
 
JosefBrandt committed
159
        self.particleAssignmentChanged.emit()
JosefBrandt's avatar
 
JosefBrandt committed
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
     
    @QtCore.pyqtSlot(list, str)
    def paintParticles(self, contourIndices, newAssignment):
        self.createSafetyBackup
        self.storedIndices = contourIndices
        self.storedAssignmend = newAssignment
        contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
        self.particlePainter = ParticlePainter(self, contours)
        self.viewparent.normalSize()
        self.viewparent.particlePainter = self.particlePainter
        self.viewparent.scene().addItem(self.particlePainter)
        self.viewparent.update()
        
    def acceptPaintedResult(self):
        newContour = self.mergeContours(self.particlePainter.contours.copy())
        stats = self.characterizeParticle(newContour)
        
        self.particleContainer.mergeParticles(self.storedIndices, newContour, stats, newAssignment=self.storedAssignmend)
        for ind in self.storedIndices:
            self.viewparent.removeParticleContour(ind)
        
181
        self.particleAssignmentChanged.emit()
JosefBrandt's avatar
 
JosefBrandt committed
182 183 184 185 186 187 188 189 190 191 192 193 194 195
        self.viewparent.resetContourIndices()
        self.viewparent.addParticleContour(newContour, len(self.viewparent.contourItems))
        
        #TODO: INCLUDE SANITY CHECK!!!!!!!!!
        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
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
    
    def mergeContours(self, contours):
        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))

        img = np.zeros((rangey, rangex))
        for curCnt in contours:
            for i in range(len(curCnt)):
                curCnt[i][0][0] -= xmin-padding
                curCnt[i][0][1] -= ymin-padding
                
            cv2.drawContours(img, [curCnt], -1, 1, -1)
            cv2.drawContours(img, [curCnt], -1, 1, 1)
        
        img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3))))
        
        if cv2.__version__ > '3.5':
            contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
        else:
            temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

        if len(contours)>1:
JosefBrandt's avatar
 
JosefBrandt committed
223
            QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 
224 225 226 227 228 229 230
                                           'Particle contours are not connected and cannot be combined!')
            return
        
        newContour = contours[0]
        for i in range(len(newContour)):
            newContour[i][0][0] += xmin-padding
            newContour[i][0][1] += ymin-padding
231
            
232 233
        return newContour
        
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
    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)
JosefBrandt's avatar
 
JosefBrandt committed
250 251

        
Hackmet's avatar
Hackmet committed
252

253
    
Hackmet's avatar
Hackmet committed
254