Commit 3a47533b authored by JosefBrandt's avatar JosefBrandt

More robust detection of invalid particles

currently only fully implemented in legacy conversion, has to be adapted during particle detection as well!
parent a79c38ed
......@@ -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])
......
......@@ -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()
......@@ -202,8 +202,6 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
self.layout.addLayout(self.menuLayout)
self.layout.addLayout(viewLayout)
self.createActions()
self.createMenus()
......@@ -220,40 +218,45 @@ 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.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 = QtWidgets.QAction("&Transparent Overlay", self)
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)
for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]:
act.setDisabled(False)
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)
def createMenus(self):
self.importMenu = QtWidgets.QMenu("&Import Spectra and Results")
self.importMenu.addActions([self.loadSpectraAct, self.loadTrueMatchAct])
......@@ -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:
......
......@@ -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):
......
......@@ -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)
......@@ -119,10 +108,7 @@ class Particle(object):
def applyHQITresholdToMeasurements(self, minHQI):
for measurement in self.measurements:
measurement.applyHQIThreshold(minHQI)
def recreateViewItem(self):
self.viewItem = SegmentationContour()
class Measurement(object):
def __init__(self):
......
......@@ -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
if cv2.contourArea(particle.contour) == 0:
return False
return True
rect = cv2.minAreaRect(cnt)
long, short = rect[1]
if short>long:
long, short = short, long
area = cv2.contourArea(cnt)
def getParticleStatsWithPixelScale(cnt, pixelscale, fullimage, dataset):
newStats = ParticleStats()
return long, short, longellipse, shortellipse, area
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)
newStats.height = getParticleHeight(cnt, dataset)
print('newHeight =', newStats.height)
newStats.shape = getParticleShape(cnt, newStats.height)
def containsHSV(self, hsv):
hue = hsv[0]
sat = hsv[1]
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
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)]
partImg = getParticleImageFromFullimage(cnt, fullimage)
newStats.color = getParticleColor(partImg)
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)
temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, flag)
if len(contour)>1:
raise NotConnectedContoursError
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)
return contour
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(contour)>1:
raise NotConnectedContoursError
return contour
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]
img = closeHolesOfSubImage(img)
contour = getContour(img, flag=cv2.CHAIN_APPROX_SIMPLE)
if len(contour) < 5:
contour = getContour(img, flag=cv2.CHAIN_APPROX_NONE)
contour = getSimpleContour(img)
if len(contour[0]) < 5:
contour = getFullContour(img)
newContour = contour[0]
for i in range(len(newContour )):
newContour [i][0][0] += xmin-padding
newContour [i][0][1] += ymin-padding
for i in range(len(contour)):
contour [i][0][0] += xmin-padding
contour [i][0][1] += ymin-padding
return newContour
return contour
def getContourExtrema(contours):
try:
......@@ -174,22 +194,19 @@ def getContourExtrema(contours):
ymin, ymax = cnt[:,0,:][:, 1].min(), cnt[:,0,:][:, 1].max()
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()
ymin, ymax = cnt[:, 1].min(), cnt[:, 1].max()
xmin, xmax = int(round(xmin)), int(round(xmax))
ymin, ymax = int(round(ymin)), int(round(ymax))
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))
\ No newline at end of file
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
#!/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/>.
"""
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
def containsHSV(self, hsv):
hue = hsv[0]
sat = hsv[1]
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
class ColorClassifier(object):
def __init__(self):
hue_tolerance = 50
self.colors = [ColorRangeHSV('yellow', 30, hue_tolerance, 40, 255),
ColorRangeHSV('blue', 120, hue_tolerance, 40, 255),
ColorRangeHSV('red', 180, hue_tolerance, 40, 255),
ColorRangeHSV('red', 0, hue_tolerance, 40, 255),
ColorRangeHSV('green', 70, hue_tolerance, 40, 255),
ColorRangeHSV('white', 128, 256, 0, 40)]
def classifyColor(self, meanHSV):
result = 'non-determinable'
for color in self.colors:
if color.containsHSV(meanHSV):
result = color.name
break
return result
\ No newline at end of file
#!/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
try:
long, short = self.getEllipseOrBoxLongAndShortSize()
except InvalidParticleError:
raise InvalidParticleError
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'