Commit 3a47533b authored by JosefBrandt's avatar JosefBrandt
Browse files

More robust detection of invalid particles

currently only fully implemented in legacy conversion, has to be adapted during particle detection as well!
parent a79c38ed
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -83,7 +83,6 @@ class TypeHistogramView(QtWidgets.QScrollArea):
        self.widgets = []
        
    def updateTypeHistogram(self, types):
#        print("Updating polymer type view", flush=True)
        for pi in self.widgets:
            self.indicatorbox.removeWidget(pi)
            pi.setParent(None)
@@ -97,7 +96,6 @@ class TypeHistogramView(QtWidgets.QScrollArea):
        
        for index, entry in enumerate(types):
            num, text, color = entry
#            print("num, text, color:", num, text, color, flush=True)
            pi = ParticleIndicator(num, numtotal, color, text)
            self.indicatorbox.addWidget(pi)
            pi.clicked.connect(getIndexFunction(index))
@@ -129,7 +127,7 @@ class SpectraPlot(QtWidgets.QGroupBox):
        self.spectra = self.dataset.particleContainer.getSpectraFromDisk()
        self.canvas.draw()
        
    def updateParticleSpectrum(self, specIndex, assignment, particleSize, hqi, color):
    def updateParticleSpectrum(self, specIndex, assignment, hqi):
        #draw Sample Spectrum
        self.spec_axis.axis("on")
        self.spec_axis.clear()
@@ -138,9 +136,11 @@ class SpectraPlot(QtWidgets.QGroupBox):
        self.spec_axis.set_ylabel('Counts', fontsize = 15)
        
        if self.spectra is not None:
            specInfo = f'ScanPoint Number {specIndex+1}, with assignment {assignment} (hqi = {hqi})'
            self.spec_axis.plot(self.spectra[:, 0], self.spectra[:, specIndex+1])
            self.spec_axis.set_title('{}, ScanPoint Number {}, Size = {} µm, HQI = {}, color = {}'.format(assignment, specIndex+1, particleSize, hqi, color))
            self.spec_axis.set_title(specInfo, fontsize=13)
            self.spec_axis.set_xbound(100, (3400 if self.spectra[-1, 0] > 3400 else self.spectra[-1, 0]))
            
            wavenumber_diff = list(self.spectra[:, 0]-100)
            y_start = wavenumber_diff.index(min(wavenumber_diff))
            y_min = min(self.spectra[y_start:, specIndex+1])
+27 −61
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ except:
class ParticleAnalysis(QtWidgets.QMainWindow):
    def __init__(self, dataset, viewparent=None):
        super(ParticleAnalysis, self).__init__(viewparent)
        self.resize(1680, 1050)
#        self.resize(1680, 1050)
        self.setWindowTitle('Results of polymer analysis')
        self.layout = QtWidgets.QHBoxLayout()
        self.widget = QtWidgets.QWidget()
@@ -203,8 +203,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
        self.layout.addLayout(self.menuLayout)
        self.layout.addLayout(viewLayout)
         

         
        self.createActions()
        self.createMenus()
        self.applyHQIThresholdToResults()
@@ -220,39 +218,44 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
        self.loadSpectraAct = QtWidgets.QAction("Load &Spectra", self)
        self.loadSpectraAct.triggered.connect(self.initializeSpecPlot)
        
        self.noOverlayAct = QtWidgets.QAction("&No Overlay", self)
        self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self)
        self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self)
        self.databaseAct = QtWidgets.QAction("&ManageDatabase", self)
        self.databaseAct.triggered.connect(self.launchDBManager)
        
        self.transpAct = QtWidgets.QAction("&Transparent Overlay", self)
        self.expExcelAct= QtWidgets.QAction("Export &Excel List", self)
        self.expExcelAct.setDisabled(True)
        self.expExcelAct.triggered.connect(self.exportToExcel)
        
        self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self)
        self.expSQLAct.setDisabled(True)
        self.expSQLAct.triggered.connect(self.exportToSQL)
        
        self.getAndActivateActionsFromGepardMain()
    
    def getAndActivateActionsFromGepardMain(self):
        gepard = self.viewparent.imparent
        
        self.noOverlayAct = gepard.noOverlayAct
        self.selOverlayAct = gepard.selOverlayAct
        self.fullOverlayAct = gepard.fullOverlayAct
        
        self.transpAct = gepard.transpAct
        self.transpAct.triggered.connect(self.updateContourColors)
            
        self.hideLabelAct = QtWidgets.QAction('&Hide Polymer Numbers', self)
        self.hideLabelAct = gepard.hideLabelAct
        self.hideLabelAct.triggered.connect(self.show_hide_labels)
        
        self.darkenAct = QtWidgets.QAction("&Darken Image", self)
        self.darkenAct = gepard.darkenAct
        self.darkenAct.triggered.connect(self.darkenBackground)
        
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct]:
            act.setCheckable(True)
        self.fullOverlayAct.setChecked(True)
        
        self.seedAct = QtWidgets.QAction("&Set Color Seed", self)
        self.seedAct = gepard.seedAct
        self.seedAct.triggered.connect(self.updateColorSeed)
        
        self.removeTinyParticlesAct = QtWidgets.QAction("&Remove not unknown Particles < 1 µm")
        self.removeTinyParticlesAct.triggered.connect(self.removeTinyParticles)

        self.databaseAct = QtWidgets.QAction("&ManageDatabase", self)
        self.databaseAct.triggered.connect(self.launchDBManager)
        
        self.expExcelAct= QtWidgets.QAction("Export &Excel List", self)
        self.expExcelAct.setDisabled(True)
        self.expExcelAct.triggered.connect(self.exportToExcel)
        
        self.expSQLAct = QtWidgets.QAction("Export to &SQL Database", self)
        self.expSQLAct.setDisabled(True)
        self.expSQLAct.triggered.connect(self.exportToSQL)
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]:
            act.setDisabled(False)

    def createMenus(self):
        self.importMenu = QtWidgets.QMenu("&Import Spectra and Results")
@@ -272,7 +275,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
        self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct])
        
        self.toolMenu = QtWidgets.QMenu("&Tools")
        self.toolMenu.addAction(self.removeTinyParticlesAct)
        self.toolMenu.addAction(self.databaseAct)
        
        self.exportMenu = QtWidgets.QMenu("&Export", self)
@@ -308,7 +310,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
            del self.importWindow
        
        self.importWindow = LoadTrueMatchResults(self.particleContainer, self)
#        self.importWindow.exec()
        
    @QtCore.pyqtSlot()
    def applyHQIThresholdToResults(self):
@@ -452,11 +453,9 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
            self.sizeHist_ax.figure.canvas.draw()
    
    def updateSpecPlot(self):
        particleSize = np.round(self.particleContainer.getSizeOfParticleByIndex(self.currentParticleIndex))
        hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex)
        assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex)
        color = self.particleContainer.getParticleColorByIndex(self.currentParticleIndex)
        self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, particleSize, hqi, color)
        self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, hqi)
            
        if self.refSelector.isEnabled() and self.refSelector.currentText() != '':
            refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText())
@@ -574,39 +573,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
            self.dataset.colorSeed = text
            self.updateHistogramsAndContours()
    
    def removeTinyParticles(self):
        indices = []
        for particle in self.particleContainer.particles:
            if particle.getParticleAssignment() != 'unknown' and particle.getParticleSize() < 1:
                indices.append(particle.index)
        
        indices = sorted(indices, reverse=True)
        numIndices = len(indices)
        
        for index, partIndex in enumerate(indices):
            self.setWidgetsToNewParticleIndex(partIndex)
            assignment = self.particleContainer.getParticleAssignmentByIndex(partIndex)
            specIndices = self.particleContainer.getSpectraIndicesOfParticle(partIndex)
            self.viewparent.highLightContour(partIndex)
            self.viewparent.centerOnRamanIndex(specIndices[0])
            reply = QtWidgets.QMessageBox.question(self, f'Particle {index+1} of {numIndices}',
                                f"Do you want to remove that particle? Type = {assignment}",
                                QtWidgets.QMessageBox.Yes | 
                                QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.No)
            
            if reply == QtWidgets.QMessageBox.Yes:
                self.viewparent.removeParticleContour(partIndex)
                self.particleContainer.removeParticles([partIndex])
                
            elif reply == QtWidgets.QMessageBox.Cancel:
                self.particleContainer.resetParticleIndices()
                self.viewparent.resetContourIndices()
                return
        
        self.particleContainer.resetParticleIndices()
        self.viewparent.resetContourIndices()
        self.updateHistogramsAndContours()
    
    def show_hide_labels(self):
        hidden = self.hideLabelAct.isChecked()
        for scanIndicator in self.viewparent.ramanscanitems:
+1 −0
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ class LoadTrueMatchResults(QtWidgets.QWidget):
    
    def show3FlagsReview(self):
        self.editEntryWindow = ModifyManualEdits(self, self.manualPolymers, self.manualAdditives)        
        self.setDisabled(True)
        self.editEntryWindow.show()

    def getImportFiles(self):
+10 −24
Original line number Diff line number Diff line
@@ -19,17 +19,14 @@ along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import numpy as np
from viewitems import SegmentationContour, RamanScanIndicator
from .particleCharacterization import getParticleColor

class Particle(object):
    def __init__(self):
        super(Particle, self).__init__()
        self.index = None
        self.longSize_ellipse = np.nan
        self.shortSize_ellipse = np.nan
        self.longSize_box = np.nan
        self.shortSize_box = np.nan
        self.longSize = np.nan
        self.shortSize = np.nan
        self.height = None
        self.area = None
        self.contour = None
        self.measurements = []
@@ -78,26 +75,18 @@ class Particle(object):
        return assignments[indexOfHighestHQI]
    
    def getParticleSize(self):
        if not np.isnan(self.longSize_ellipse):
            size = self.longSize_ellipse
        elif not np.isnan(self.longSize_box):
            size = self.longSize_box
        else:
        if np.isnan(self.longSize):
            print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}')
            raise ValueError
        assert size is not None, f'Error, size of particle {self.index} is None'
        return size
        else:
            return self.longSize
    
    def getShortParticleSize(self):
        if not np.isnan(self.shortSize_ellipse):
            size = self.shortSize_ellipse
        elif not np.isnan(self.shortSize_box):
            size = self.shortSize_box
        else:
        if np.isnan(self.shortSize):
            print(f'Error, particle size requested, but not yet set.\nParticle Index is {self.index}')
            raise ValueError
        assert size is not None, f'Error, short size of particle {self.index} is None'
        return size
        else:
            return self.shortSize
    
    def getNumberOfMeasurements(self):
        return len(self.measurements)
@@ -120,9 +109,6 @@ class Particle(object):
        for measurement in self.measurements:
            measurement.applyHQIThreshold(minHQI)

    def recreateViewItem(self):
        self.viewItem = SegmentationContour()
            
    
class Measurement(object):
    def __init__(self):
+117 −100
Original line number Diff line number Diff line
@@ -24,66 +24,44 @@ import numpy as np
import cv2
from copy import deepcopy

def getContourStatsWithPixelScale(cnt, pixelscale):
        long, short, longellipse, shortellipse, area = getContourStats(cnt)
        return long*pixelscale, short*pixelscale, longellipse*pixelscale, shortellipse*pixelscale, area*pixelscale**2

def getContourStats(cnt):
    ##characterize particle
        longellipse, shortellipse = np.nan, np.nan
        
        if cnt.shape[0] >= 5:       ##at least 5 points required for ellipse fitting...
            ellipse = cv2.fitEllipse(cnt)
            shortellipse, longellipse = ellipse[1]
from .particleClassification.colorClassification import ColorClassifier
from .particleClassification.shapeClassification import ShapeClassifier
from segmentation import closeHolesOfSubImage
from errors import NotConnectedContoursError, InvalidParticleError

class ParticleStats(object):
    longSize = None
    shortSize = None
    height = None
    area = None
    shape = None
    color = None

def particleIsValid(particle):
    if particle.longSize == 0 or particle.shortSize == 0:
        return False
    
        rect = cv2.minAreaRect(cnt)
        long, short = rect[1]
        if short>long:
            long, short = short, long
    if cv2.contourArea(particle.contour) == 0:
        return False
    return True
    
        area = cv2.contourArea(cnt)

        return long, short, longellipse, shortellipse, area
def getParticleStatsWithPixelScale(cnt, pixelscale, fullimage, dataset):
    newStats = ParticleStats()
    
class ColorRangeHSV(object):
    def __init__(self, name, hue, hue_tolerance, min_sat, max_sat):
        self.name = name
        self.minHue = hue-hue_tolerance/2
        self.maxHue = hue+hue_tolerance/2
        self.minSat = min_sat
        self.maxSat = max_sat
    newStats.longSize, newStats.shortSize, newStats.area = getContourStats(cnt)
    newStats.longSize *= pixelscale
    newStats.shortSize *= pixelscale
    newStats.area *= (pixelscale**2)

    def containsHSV(self, hsv):
        hue = hsv[0]
        sat = hsv[1]
    newStats.height = getParticleHeight(cnt, dataset)    
    print('newHeight =', newStats.height)
    newStats.shape = getParticleShape(cnt, newStats.height)
    
        if self.minHue <= hue <= self.maxHue and self.minSat <= sat <= self.maxSat:
            return True
        else:
            if self.name != 'white':
                return False
            else:
                if sat < 128 and hsv[2] > 70:
                    return True
    partImg = getParticleImageFromFullimage(cnt, fullimage)
    newStats.color = getParticleColor(partImg)
    
class ColorClassifier(object):
    def __init__(self):
        hue_tolerance = 30
        self.colors = [ColorRangeHSV('yellow', 30, hue_tolerance, 30, 255),
                  ColorRangeHSV('blue', 120, hue_tolerance, 80, 255),
                  ColorRangeHSV('red', 180, hue_tolerance, 50, 255),
                  ColorRangeHSV('red', 0, hue_tolerance, 50, 255),
                  ColorRangeHSV('green', 70, hue_tolerance, 50, 255),
                  ColorRangeHSV('white', 128, 256, 0, 50)]
    
    def classifyColor(self, meanHSV):
        result = 'non-determinable'
        for color in self.colors:
            if color.containsHSV(meanHSV):
                result = color.name
                break
        
        return result
    return newStats

def getParticleColor(imgRGB, colorClassifier=None):
    img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL)
@@ -93,6 +71,44 @@ def getParticleColor(imgRGB, colorClassifier=None):
    color = colorClassifier.classifyColor(meanHSV)
    return color

def getParticleShape(contour, particleHeight, shapeClassifier=None):
    if shapeClassifier is None:
        shapeClassifier = ShapeClassifier()
    try:
        shape = shapeClassifier.classifyShape(contour, particleHeight)
    except InvalidParticleError:
        raise
    return shape

def getParticleHeight(contour, dataset):
    zimg = getParticleImageFromFullimage(contour, dataset.getZvalImg())
    if zimg.shape[0] == 0 or zimg.shape[1] == 0:
        raise InvalidParticleError
        
    zimg = cv2.medianBlur(zimg, 5)
    avg_ZValue = np.mean(zimg[zimg > 0])
    if np.isnan(avg_ZValue):  #i.e., only zeros in zimg
        avg_ZValue = 0
    z0, z1 = dataset.zpositions.min(), dataset.zpositions.max()
    height = avg_ZValue/255.*(z1-z0) + z0
    return height

def getContourStats(cnt):
    ##characterize particle
        if cnt.shape[0] >= 5:       ##at least 5 points required for ellipse fitting...
            ellipse = cv2.fitEllipse(cnt)
            short, long = ellipse[1]
        else:
            rect = cv2.minAreaRect(cnt)
            long, short = rect[1]
            
        if short>long:
            long, short = short, long
        
        area = cv2.contourArea(cnt)
    
        return long, short, area

def mergeContours(contours):
    img, xmin, ymin, padding = contoursToImg(contours)
    return imgToCnt(img, xmin, ymin, padding)
@@ -115,13 +131,15 @@ def getParticleImageFromFullimage(contour, fullimage):
    img = np.array(img, dtype = np.uint8)
    return img
    
def contoursToImg(contours, padding=2):
def contoursToImg(contours, padding=0):
    contourCopy = deepcopy(contours)
    xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
    
    padding = padding   #pixel in each direction
    rangex = int(np.round((xmax-xmin)+2*padding))
    rangey = int(np.round((ymax-ymin)+2*padding))
    if rangex == 0 or rangey == 0:
        raise InvalidParticleError

    img = np.zeros((rangey, rangex))
    for curCnt in contourCopy:
@@ -130,42 +148,44 @@ def contoursToImg(contours, padding=2):
            curCnt[i][0][1] -= ymin-padding
        cv2.drawContours(img, [curCnt], -1, 255, -1)
        cv2.drawContours(img, [curCnt], -1, 255, 1)
    
    img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3))))
    return img, xmin, ymin, padding

def imgToCnt(img, xmin, ymin, padding):
    def getSimpleContour(img):
def imgToCnt(img, xmin, ymin, padding=0):
    def getContour(img, flag):
        if cv2.__version__ > '3.5':
            contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
            contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag)
        else:
            temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
        
        if len(contour)>1:
            raise NotConnectedContoursError
        return contour
            temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag)
        
    def getFullContour(img):
        if cv2.__version__ > '3.5':
            contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
        else:
            temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
        if len(contours) == 0:   #i.e., no contour found
            raise InvalidParticleError
        elif len(contours) == 1:   #i.e., exactly one contour found
            contour = contours[0]
        else:       #i.e., multiple contours found
            contour = getLargestContour(contours)
            
        if len(contour)>1:
            raise NotConnectedContoursError
        return contour
    
    contour = getSimpleContour(img)
    def getLargestContour(contours):
        areas = []
        for contour in contours:
            areas.append(cv2.contourArea(contour))
        maxIndex = areas.index(max(areas))
        print(f'{len(contours)} contours found, getting the largest one. Areas are: {areas}, taking contour at index {maxIndex}')
        return contours[maxIndex]

    if len(contour[0]) < 5:
        contour = getFullContour(img)
    img = closeHolesOfSubImage(img)
    contour = getContour(img, flag=cv2.CHAIN_APPROX_SIMPLE)

    newContour = contour[0]
    for i in range(len(newContour )):
        newContour [i][0][0] += xmin-padding
        newContour [i][0][1] += ymin-padding
    if len(contour) < 5:
        contour = getContour(img, flag=cv2.CHAIN_APPROX_NONE)
    
    return newContour 
    for i in range(len(contour)):
        contour [i][0][0] += xmin-padding
        contour [i][0][1] += ymin-padding
        
    return contour

def getContourExtrema(contours):
    try:
@@ -175,21 +195,18 @@ def getContourExtrema(contours):
    except IndexError: #i.e., not a list of contours was passed, but an individual contour. Hence, the above indexing does not work
        xmin, xmax = cnt[:, 0].min(), cnt[:, 0].max()
        ymin, ymax = cnt[:, 1].min(), cnt[:, 1].max()
    return xmin, xmax, ymin, ymax
        
class NotConnectedContoursError(Exception):
    pass

if __name__ == '__main__':
    colors = {'white': (41, 25, 66),
              "red": (128, 121, 57),
              "red2": (23, 88, 49), 
              "yellow": (25, 121, 91),
              "pink": (11, 79, 51),
              "brown": (32, 38, 64),
              "green": (54, 99, 53)}
    classifier= ColorClassifier()
#    print(classifier.hsv)
    for name, mean in colors.items():
        print(name, classifier.classifyColor(mean))
    xmin, xmax = int(round(xmin)), int(round(xmax))
    ymin, ymax = int(round(ymin)), int(round(ymax))
    
    return xmin, xmax, ymin, ymax

def getParticleCenterPoint(contour):
    img, xmin, ymin, padding = contoursToImg(contour)
    dist = cv2.distanceTransform(img, cv2.DIST_L2, 3)
    ind = np.argmax(dist)
    y = ind//dist.shape[1]-1
    x = ind%dist.shape[1]-1
    x += xmin
    y += ymin
    return x, y
Loading