shapeClassification.py 4.79 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
#!/usr/bin/env python3
# -*- 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/>.
"""
import cv2
from errors import InvalidParticleError

class BaseShape(object):
    def __init__(self):
        self.name = None
        self.contour = None
                
        self.longSize = None
        self.shortSize = None
        self.height = None        
        
        self.aspectRatio = None
        self.aspectRatioRange = None
        
        self.solidity = None
        self.solidityRange = None

        self.height2AverageLength = None
        self.height2AverageLengthRange = None
    
    def getParticleCharacteristics(self):
        assert self.contour is not None, 'not able to get contour characteristics of NONE contour'
        area = cv2.contourArea(self.contour)
        hull = cv2.convexHull(self.contour)
        hull_area = cv2.contourArea(hull)
        if area == 0 or hull_area == 0:
            raise InvalidParticleError
            
        self.solidity = area/hull_area
        
JosefBrandt's avatar
JosefBrandt committed
53
        long, short = self.getEllipseOrBoxLongAndShortSize()
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
        self.aspectRatio = long/short
        
        avgLength = (long+short)/2
        self.height2AverageLength = self.height/avgLength
    
    def getEllipseOrBoxLongAndShortSize(self):
        if self.contour.shape[0] >= 5:       ##at least 5 points required for ellipse fitting...
            ellipse = cv2.fitEllipse(self.contour)
            short, long = ellipse[1]
        else:
            rect = cv2.minAreaRect(self.contour)
            long, short = rect[1]
            
        if short>long:
            long, short = short, long
        
        if short == 0.0:
            raise InvalidParticleError
        
        return long, short
    
    def fitsOtherShapeCriteria(self, otherShape):
        assert self.aspectRatio is not None
        assert self.solidity is not None
        
        numFittingCriteria = 0
        if otherShape.aspectRatioRange[0] <= self.aspectRatio <= otherShape.aspectRatioRange[1]:
            numFittingCriteria += 1
            
        if otherShape.solidityRange[0] <= self.solidity <= otherShape.solidityRange[1]:
            numFittingCriteria += 1
            
        if otherShape.height2AverageLengthRange[0] <= self.height2AverageLength <= otherShape.height2AverageLengthRange[1]:
            numFittingCriteria += 1
        
        return numFittingCriteria


class Spherule(BaseShape):
    def __init__(self):
        super(Spherule, self).__init__()
        self.name = 'spherule'
        self.aspectRatioRange = [1.0, 1.2]
        self.solidityRange = [0.95, 1.]
        self.height2AverageLengthRange = [0.4, 1.5]


class Irregular(BaseShape):
    def __init__(self):
        super(Irregular, self).__init__()
        self.name = 'irregular'
        
        self.aspectRatioRange = [1.0, 3]
        self.solidityRange = [0.9, 1.]
        self.height2AverageLengthRange = [0.4, 10]


class Flake(BaseShape):
    def __init__(self):
        super(Flake, self).__init__()
        self.name = 'flake'
        
        self.aspectRatioRange = [1.0, 3]
        self.solidityRange = [0.7, 1.]
        self.height2AverageLengthRange = [0.0, 0.4]


class Fibre(BaseShape):
    def __init__(self):
        super(Fibre, self).__init__()
        self.name = 'fibre'
        self.aspectRatioRange = [3, 1000]
        self.solidityRange = [0.0, 0.4]
        self.height2AverageLengthRange = [0, 1000]


class ShapeClassifier(object):
    def __init__(self):
        self.shapeClasses = [Spherule(), Fibre(), Irregular()]
    
    def classifyShape(self, contour, particleHeight):
        newShape = BaseShape()
        newShape.contour = contour
        newShape.height = particleHeight
JosefBrandt's avatar
JosefBrandt committed
138
        newShape.getParticleCharacteristics()
139 140 141 142 143 144 145 146 147 148 149
        
        mostFittingCriteria = 0
        bestFittingShape = 'unknown'
        
        for testShape in self.shapeClasses:
            numFits = newShape.fitsOtherShapeCriteria(testShape)
            if numFits > mostFittingCriteria:
                mostFittingCriteria = numFits
                bestFittingShape = testShape.name
        
        return bestFittingShape