particleeditor.py 7.93 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 34
class ParticleContextMenu(QtWidgets.QMenu):
    combineParticlesSignal = QtCore.pyqtSignal(list, str)
    reassignParticlesSignal = QtCore.pyqtSignal(list, str)
JosefBrandt's avatar
 
JosefBrandt committed
35
    def __init__(self, viewparent):
JosefBrandt's avatar
 
JosefBrandt committed
36
        super(ParticleContextMenu, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
37 38 39
        self.viewparent = viewparent
        self.selectedParticleIndices = self.viewparent.selectedParticleIndices
        self.particleContainer = self.viewparent.dataset.particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
40 41 42 43 44 45 46
        
    def executeAtScreenPos(self, screenPos):
        self.combineActs = []
        self.combineMenu = QtWidgets.QMenu("Combine Particles into")
        assignments = []

        for particleIndex in self.selectedParticleIndices:
JosefBrandt's avatar
 
JosefBrandt committed
47 48 49 50
            try:
                assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex)
            except:
                return
JosefBrandt's avatar
 
JosefBrandt committed
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
            assignments.append(assignment)
        
        for assignment in np.unique(assignments):
            self.combineActs.append(self.combineMenu.addAction(assignment))
        self.combineActs.append(self.combineMenu.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.addMenu(self.combineMenu)        
        self.addMenu(self.reassignMenu)
        
        action = self.exec_(screenPos)

        if action in self.combineActs:
76
            newAssignment = self.validifyAssignment(action.text())
JosefBrandt's avatar
 
JosefBrandt committed
77 78
            self.combineParticlesSignal.emit(self.selectedParticleIndices, newAssignment)
        elif action in self.reassignActs:
79
            newAssignment = self.validifyAssignment(action.text())
JosefBrandt's avatar
 
JosefBrandt committed
80
            self.reassignParticlesSignal.emit(self.selectedParticleIndices, newAssignment)
81 82 83 84 85 86 87 88 89 90
    
    def validifyAssignment(self, assignment):
        if assignment == "other":
            assignment = self.getNewEntry()
        return assignment
    
    def getNewEntry(self):
        text, okClicked = QtWidgets.QInputDialog.getText(QtWidgets.QWidget(), "Custom assignment", "Enter new assignment")
        if okClicked and text != '':
            return text
JosefBrandt's avatar
 
JosefBrandt committed
91 92 93


class ParticleEditor(QtCore.QObject):
JosefBrandt's avatar
 
JosefBrandt committed
94 95 96
    particleContoursChanged = QtCore.pyqtSignal()
    particleAssignmentChanged = QtCore.pyqtSignal()
    def __init__(self, analysisparent, particleContainer):
JosefBrandt's avatar
 
JosefBrandt committed
97
        super(ParticleEditor, self).__init__()
JosefBrandt's avatar
 
JosefBrandt committed
98
        self.particleContainer = particleContainer
JosefBrandt's avatar
 
JosefBrandt committed
99
        self.analysisparent = analysisparent #the assigned analysis widget
Hackmet's avatar
Hackmet committed
100 101 102
        self.backupFreq = 3     #save a backup every n actions
        self.neverBackedUp = True
        self.actionCounter = 0
JosefBrandt's avatar
 
JosefBrandt committed
103 104 105 106
    
    def connectToSignals(self, contextMenu):
        contextMenu.combineParticlesSignal.connect(self.combineParticles)
        contextMenu.reassignParticlesSignal.connect(self.reassignParticles)
JosefBrandt's avatar
 
JosefBrandt committed
107
    
108
    def createSafetyBackup(self):
Hackmet's avatar
Hackmet committed
109 110
        self.actionCounter += 1
        if self.actionCounter == self.backupFreq-1 or self.neverBackedUp:            
JosefBrandt's avatar
 
JosefBrandt committed
111
            backupname = self.analysisparent.dataset.saveBackup()
Hackmet's avatar
Hackmet committed
112
            print('backing up as', backupname)
Hackmet's avatar
Hackmet committed
113
            self.neverBackedUp = False
JosefBrandt's avatar
 
JosefBrandt committed
114
            self.actionCounter = 0        
Hackmet's avatar
Hackmet committed
115
    
JosefBrandt's avatar
 
JosefBrandt committed
116
    @QtCore.pyqtSlot(list, str)
117 118 119 120 121 122 123 124
    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
125
        self.particleContoursChanged.emit()
JosefBrandt's avatar
 
JosefBrandt committed
126 127 128 129 130 131 132
#        #save data
#        minHQI = self.parent.hqiSpinBox.value()
#        compHQI = self.parent.compHqiSpinBox.value()
#        if not self.datastats.saveAnalysisResults(minHQI, compHQI):
#            QtWidgets.QMessageBox.warning(self.parent, 'Error!',
#                    'Data inconsistency after saving!', QtWidgets.QMessageBox.Ok, 
#                    QtWidgets.QMessageBox.Ok)
133
    
JosefBrandt's avatar
 
JosefBrandt committed
134
    @QtCore.pyqtSlot(list, str)
135
    def reassignParticles(self, contourindices, newAssignment):
136
        self.createSafetyBackup()
137
        print(f'reassigning indices {contourindices} into {newAssignment}')
138
        for partIndex in contourindices:
139
            self.particleContainer.reassignParticleToAssignment(partIndex, newAssignment)
140

JosefBrandt's avatar
 
JosefBrandt committed
141
        self.particleAssignmentChanged.emit()
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    
    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
169
            QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!', 
170 171 172 173 174 175 176
                                           '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
177
            
178 179 180
        return newContour
        
        
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    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
197 198

        
Hackmet's avatar
Hackmet committed
199

200
    
Hackmet's avatar
Hackmet committed
201