Commit a79c38ed authored by JosefBrandt's avatar JosefBrandt

Particle Color Detection implemented

Particle colors are detected automatically, but can changed through particle context menu
parent 316bc7ba
# -*- 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 pickle
def loadAssigments(fname):
with open(fname, "rb") as fp:
assignment = pickle.load(fp)
return assignment
def saveAssignments(assignment, fname):
with open(fname, "wb") as fp:
pickle.dump(assignment, fp, protocol=-1)
class DBAssignment(object):
def __init__(self):
self.filename = None
self.assignments = []
def setFileName(self, fname):
self.filename = fname
def save(self):
saveAssignments(self, self.filename)
def hasAssignment(self, polymerName):
polymIsPresent = False
for assignment in self.assignments:
if assignment.polymerName == polymerName:
polymIsPresent = True
break
return polymIsPresent
def getAssignment(self, polymerName):
for assignment in self.assignments:
if assignment.polymerName == polymerName:
return assignment
def createNewAssignment(self, polymerName):
self.assignments.append(Assignment(polymerName))
def updateAssignment(self, polymerName, result, catRes, indic_paint):
for assignment in self.assignments:
if assignment.polymerName == polymerName:
assignment.update(result, catRes, indic_paint)
return
class Assignment(object):
def __init__(self, polymerName):
self.polymerName = polymerName
self.result = None
self.categorizedResult = None
self.indication_paint = None
def update(self, result, catRes, indic_paint):
self.result = result
self.categorizedResult = catRes
self.indication_paint = indic_paint
...@@ -129,7 +129,7 @@ class SpectraPlot(QtWidgets.QGroupBox): ...@@ -129,7 +129,7 @@ class SpectraPlot(QtWidgets.QGroupBox):
self.spectra = self.dataset.particleContainer.getSpectraFromDisk() self.spectra = self.dataset.particleContainer.getSpectraFromDisk()
self.canvas.draw() self.canvas.draw()
def updateParticleSpectrum(self, specIndex, assignment, particleSize, hqi): def updateParticleSpectrum(self, specIndex, assignment, particleSize, hqi, color):
#draw Sample Spectrum #draw Sample Spectrum
self.spec_axis.axis("on") self.spec_axis.axis("on")
self.spec_axis.clear() self.spec_axis.clear()
...@@ -139,7 +139,7 @@ class SpectraPlot(QtWidgets.QGroupBox): ...@@ -139,7 +139,7 @@ class SpectraPlot(QtWidgets.QGroupBox):
if self.spectra is not None: if self.spectra is not None:
self.spec_axis.plot(self.spectra[:, 0], self.spectra[:, specIndex+1]) self.spec_axis.plot(self.spectra[:, 0], self.spectra[:, specIndex+1])
self.spec_axis.set_title('{}, ScanPoint Number {}, Size = {} µm, HQI = {}'.format(assignment, specIndex+1, particleSize, hqi)) self.spec_axis.set_title('{}, ScanPoint Number {}, Size = {} µm, HQI = {}, color = {}'.format(assignment, specIndex+1, particleSize, hqi, color))
self.spec_axis.set_xbound(100, (3400 if self.spectra[-1, 0] > 3400 else self.spectra[-1, 0])) 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) wavenumber_diff = list(self.spectra[:, 0]-100)
y_start = wavenumber_diff.index(min(wavenumber_diff)) y_start = wavenumber_diff.index(min(wavenumber_diff))
......
...@@ -455,7 +455,8 @@ class ParticleAnalysis(QtWidgets.QMainWindow): ...@@ -455,7 +455,8 @@ class ParticleAnalysis(QtWidgets.QMainWindow):
particleSize = np.round(self.particleContainer.getSizeOfParticleByIndex(self.currentParticleIndex)) particleSize = np.round(self.particleContainer.getSizeOfParticleByIndex(self.currentParticleIndex))
hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex) hqi = self.particleContainer.getHQIOfSpectrumIndex(self.currentSpectrumIndex)
assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex) assignment = self.particleContainer.getParticleAssignmentByIndex(self.currentParticleIndex)
self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, particleSize, hqi) color = self.particleContainer.getParticleColorByIndex(self.currentParticleIndex)
self.specPlot.updateParticleSpectrum(self.currentSpectrumIndex, assignment, particleSize, hqi, color)
if self.refSelector.isEnabled() and self.refSelector.currentText() != '': if self.refSelector.isEnabled() and self.refSelector.currentText() != '':
refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText()) refID = self.dbWin.activeDatabase.spectraNames.index(self.refSelector.currentText())
......
...@@ -20,6 +20,7 @@ If not, see <https://www.gnu.org/licenses/>. ...@@ -20,6 +20,7 @@ If not, see <https://www.gnu.org/licenses/>.
""" """
import numpy as np import numpy as np
from viewitems import SegmentationContour, RamanScanIndicator from viewitems import SegmentationContour, RamanScanIndicator
from .particleCharacterization import getParticleColor
class Particle(object): class Particle(object):
def __init__(self): def __init__(self):
...@@ -32,7 +33,8 @@ class Particle(object): ...@@ -32,7 +33,8 @@ class Particle(object):
self.area = None self.area = None
self.contour = None self.contour = None
self.measurements = [] self.measurements = []
self.viewItem = None self.color = None
self.shape = None
def addMeasurement(self, refToMeasurement): def addMeasurement(self, refToMeasurement):
refToMeasurement.assignedParticle = self refToMeasurement.assignedParticle = self
......
...@@ -22,6 +22,7 @@ If not, see <https://www.gnu.org/licenses/>. ...@@ -22,6 +22,7 @@ If not, see <https://www.gnu.org/licenses/>.
import numpy as np import numpy as np
import cv2 import cv2
from copy import deepcopy
def getContourStatsWithPixelScale(cnt, pixelscale): def getContourStatsWithPixelScale(cnt, pixelscale):
long, short, longellipse, shortellipse, area = getContourStats(cnt) long, short, longellipse, shortellipse, area = getContourStats(cnt)
...@@ -42,4 +43,153 @@ def getContourStats(cnt): ...@@ -42,4 +43,153 @@ def getContourStats(cnt):
area = cv2.contourArea(cnt) area = cv2.contourArea(cnt)
return long, short, longellipse, shortellipse, area return long, short, longellipse, shortellipse, area
\ No newline at end of file
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 = 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
def getParticleColor(imgRGB, colorClassifier=None):
img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL)
meanHSV = cv2.mean(img)
if colorClassifier is None:
colorClassifier = ColorClassifier()
color = colorClassifier.classifyColor(meanHSV)
return color
def mergeContours(contours):
img, xmin, ymin, padding = contoursToImg(contours)
return imgToCnt(img, xmin, ymin, padding)
def getParticleImageFromFullimage(contour, fullimage):
contourCopy = deepcopy(contour)
xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
img = fullimage[ymin:ymax, xmin:xmax]
mask = np.zeros(img.shape[:2])
for i in range(len(contourCopy)):
contourCopy[i][0][0] -= xmin
contourCopy[i][0][1] -= ymin
cv2.drawContours(mask, [contourCopy], -1, (255, 255, 255), -1)
cv2.drawContours(mask, [contourCopy], -1, (255, 255, 255), 1)
img[mask == 0] = 0
img = np.array(img, dtype = np.uint8)
return img
def contoursToImg(contours, padding=2):
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))
img = np.zeros((rangey, rangex))
for curCnt in contourCopy:
for i in range(len(curCnt)):
curCnt[i][0][0] -= xmin-padding
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):
if cv2.__version__ > '3.5':
contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
else:
temp, contour, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if len(contour)>1:
raise NotConnectedContoursError
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
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
return newContour
def getContourExtrema(contours):
try:
cnt = np.vstack(tuple(contours))
xmin, xmax = cnt[:,0,:][:, 0].min(), cnt[:,0,:][:, 0].max()
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()
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
...@@ -248,6 +248,16 @@ class ParticleContainer(object): ...@@ -248,6 +248,16 @@ class ParticleContainer(object):
areas.append(particle.getArea()) areas.append(particle.getArea())
return areas return areas
def getColorsOfAllParticles(self):
colors = []
for particle in self.particles:
colors.append(particle.color)
return colors
def getParticleColorByIndex(self, particleIndex):
particle = self.getParticleOfIndex(particleIndex)
return particle.color
def getSizesOfParticleType(self, assignment): def getSizesOfParticleType(self, assignment):
particleSizes = [] particleSizes = []
for particle in self.particles: for particle in self.particles:
...@@ -281,6 +291,10 @@ class ParticleContainer(object): ...@@ -281,6 +291,10 @@ class ParticleContainer(object):
final_typehistogram = {i[0]: i[1] for i in sorted_typehistogram} final_typehistogram = {i[0]: i[1] for i in sorted_typehistogram}
return final_typehistogram return final_typehistogram
def changeParticleColor(self, index, newColor):
particle = self.getParticleOfIndex(index)
particle.color = newColor
def addMergedParticle(self, particleIndices, newContour, newStats, newAssignment=None): def addMergedParticle(self, particleIndices, newContour, newStats, newAssignment=None):
newParticle = Particle() newParticle = Particle()
#copy Measurements #copy Measurements
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Created on Wed Jan 16 12:43:00 2019
@author: brandt
"""
""" """
GEPARD - Gepard-Enabled PARticle Detection GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
...@@ -26,19 +20,17 @@ If not, see <https://www.gnu.org/licenses/>. ...@@ -26,19 +20,17 @@ If not, see <https://www.gnu.org/licenses/>.
""" """
import numpy as np import numpy as np
import cv2
from PyQt5 import QtWidgets, QtCore from PyQt5 import QtWidgets, QtCore
from copy import deepcopy
from .particlePainter import ParticlePainter from .particlePainter import ParticlePainter
from .particleCharacterization import getContourStatsWithPixelScale import analysis.particleCharacterization as pc
class ParticleContextMenu(QtWidgets.QMenu): class ParticleContextMenu(QtWidgets.QMenu):
combineParticlesSignal = QtCore.pyqtSignal(list, str) combineParticlesSignal = QtCore.pyqtSignal(list, str)
reassignParticlesSignal = QtCore.pyqtSignal(list, str) reassignParticlesSignal = QtCore.pyqtSignal(list, str)
paintParticlesSignal = QtCore.pyqtSignal(list, str) paintParticlesSignal = QtCore.pyqtSignal(list, str)
changeParticleColorSignal = QtCore.pyqtSignal(list, str)
deleteParticlesSignal = QtCore.pyqtSignal(list) deleteParticlesSignal = QtCore.pyqtSignal(list)
def __init__(self, viewparent): def __init__(self, viewparent):
...@@ -83,22 +75,33 @@ class ParticleContextMenu(QtWidgets.QMenu): ...@@ -83,22 +75,33 @@ class ParticleContextMenu(QtWidgets.QMenu):
elif numParticles == 1: elif numParticles == 1:
self.combineMenu.setDisabled(True) self.combineMenu.setDisabled(True)
self.colorMenu = QtWidgets.QMenu("Set Particle Color To")
self.colorActs = []
for color in ['white', 'black', 'blue', 'brown', 'green', 'grey', 'non-determinable', 'red', 'transparent', 'yellow']:
self.colorActs.append(self.colorMenu.addAction(color))
infoAct = self.addAction(f'selected {numParticles} particles')
infoAct.setDisabled(True)
self.addMenu(self.combineMenu) self.addMenu(self.combineMenu)
self.addMenu(self.reassignMenu) self.addMenu(self.reassignMenu)
self.addMenu(self.paintMenu) self.addMenu(self.paintMenu)
self.addMenu(self.colorMenu)
self.deleteAct = self.addAction("Delete particle(s)") self.deleteAct = self.addAction("Delete particle(s)")
action = self.exec_(screenPos) action = self.exec_(screenPos)
if action: if action:
newAssignment = self.validifyAssignment(action.text()) actionText = self.validifyAssignment(action.text())
if action in self.combineActs: if action in self.combineActs:
self.combineParticlesSignal.emit(self.selectedParticleIndices, newAssignment) self.combineParticlesSignal.emit(self.selectedParticleIndices, actionText)
elif action in self.reassignActs: elif action in self.reassignActs:
self.reassignParticlesSignal.emit(self.selectedParticleIndices, newAssignment) self.reassignParticlesSignal.emit(self.selectedParticleIndices, actionText)
elif action in self.paintActs: elif action in self.paintActs:
self.paintParticlesSignal.emit(self.selectedParticleIndices, newAssignment) self.paintParticlesSignal.emit(self.selectedParticleIndices, actionText)
elif action in self.colorActs:
self.changeParticleColorSignal.emit(self.selectedParticleIndices, actionText)
elif action == self.deleteAct: elif action == self.deleteAct:
self.deleteParticlesSignal.emit(self.selectedParticleIndices) self.deleteParticlesSignal.emit(self.selectedParticleIndices)
...@@ -130,6 +133,7 @@ class ParticleEditor(QtCore.QObject): ...@@ -130,6 +133,7 @@ class ParticleEditor(QtCore.QObject):
contextMenu.combineParticlesSignal.connect(self.combineParticles) contextMenu.combineParticlesSignal.connect(self.combineParticles)
contextMenu.reassignParticlesSignal.connect(self.reassignParticles) contextMenu.reassignParticlesSignal.connect(self.reassignParticles)
contextMenu.paintParticlesSignal.connect(self.paintParticles) contextMenu.paintParticlesSignal.connect(self.paintParticles)
contextMenu.changeParticleColorSignal.connect(self.changeParticleColors)
contextMenu.deleteParticlesSignal.connect(self.deleteParticles) contextMenu.deleteParticlesSignal.connect(self.deleteParticles)
def createSafetyBackup(self): def createSafetyBackup(self):
...@@ -146,12 +150,14 @@ class ParticleEditor(QtCore.QObject): ...@@ -146,12 +150,14 @@ class ParticleEditor(QtCore.QObject):
print(f'Combining particles {contourIndices} into {newAssignment}') print(f'Combining particles {contourIndices} into {newAssignment}')
contours = self.particleContainer.getParticleContoursByIndex(contourIndices) contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
try: try:
newContour = self.mergeContours(contours) newContour = pc.mergeContours(contours)
except NotConnectedContoursError: except pc.NotConnectedContoursError:
QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!',
'Particle contours are not connected or have holes.\nThat is currently not supported!')
return return
pixelscale = self.viewparent.dataset.getPixelScale() pixelscale = self.viewparent.dataset.getPixelScale()
stats = getContourStatsWithPixelScale(newContour, pixelscale) stats = pc.getContourStatsWithPixelScale(newContour, pixelscale)
self.mergeParticlesInParticleContainerAndSampleView(contourIndices,newContour, stats, newAssignment) self.mergeParticlesInParticleContainerAndSampleView(contourIndices,newContour, stats, newAssignment)
...@@ -172,9 +178,7 @@ class ParticleEditor(QtCore.QObject): ...@@ -172,9 +178,7 @@ class ParticleEditor(QtCore.QObject):
self.storedAssignmend = newAssignment self.storedAssignmend = newAssignment
contours = self.particleContainer.getParticleContoursByIndex(contourIndices) contours = self.particleContainer.getParticleContoursByIndex(contourIndices)
# topLeft = self.getTopLeft(contours) img, xmin, ymin, self.padding = pc.contoursToImg(contours, padding=0)
# img, self.xmin, self.ymin, self.padding = self.contoursToImg(contours, padding=0)
img, xmin, ymin, self.padding = self.contoursToImg(contours, padding=0)
topLeft = [ymin, xmin] topLeft = [ymin, xmin]
self.particlePainter = ParticlePainter(self, img, topLeft) self.particlePainter = ParticlePainter(self, img, topLeft)
...@@ -188,15 +192,17 @@ class ParticleEditor(QtCore.QObject): ...@@ -188,15 +192,17 @@ class ParticleEditor(QtCore.QObject):
img = self.particlePainter.img img = self.particlePainter.img
xmin = self.particlePainter.topLeft[1] xmin = self.particlePainter.topLeft[1]
ymin = self.particlePainter.topLeft[0] ymin = self.particlePainter.topLeft[0]
newContour = self.imgToCnt(img, xmin, ymin, 0) newContour = pc.imgToCnt(img, xmin, ymin, 0)
except NotConnectedContoursError: except pc.NotConnectedContoursError:
QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!',
'Particle contours are not connected or have holes.\nThat is currently not supported!')
self.storedIndices = [] self.storedIndices = []
self.storedAssignmend = None self.storedAssignmend = None
self.destroyParticlePainter() self.destroyParticlePainter()
return return
pixelscale = self.viewparent.dataset.getPixelScale() pixelscale = self.viewparent.dataset.getPixelScale()
stats = getContourStatsWithPixelScale(newContour, pixelscale) stats = pc.getContourStatsWithPixelScale(newContour, pixelscale)
self.mergeParticlesInParticleContainerAndSampleView(self.storedIndices, newContour, stats, self.storedAssignmend) self.mergeParticlesInParticleContainerAndSampleView(self.storedIndices, newContour, stats, self.storedAssignmend)
...@@ -211,57 +217,6 @@ class ParticleEditor(QtCore.QObject): ...@@ -211,57 +217,6 @@ class ParticleEditor(QtCore.QObject):
self.viewparent.update() self.viewparent.update()
self.particlePainter = None self.particlePainter = None
def mergeContours(self, contours):
img, xmin, ymin, padding = self.contoursToImg(contours)
return self.imgToCnt(img, xmin, ymin, padding)
def contoursToImg(self, contours, padding=2):
contourCopy = deepcopy(contours)
cnt = np.vstack(tuple(contourCopy)) #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 = padding #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 contourCopy:
for i in range(len(curCnt)):
curCnt[i][0][0] -= xmin-padding
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(self, img, xmin, ymin, padding):
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:
QtWidgets.QMessageBox.critical(self.viewparent, 'ERROR!',
'Particle contours are not connected or have holes.\nThat is currently not supported!')
raise NotConnectedContoursError
newContour = contours[0]
for i in range(len(newContour)):
newContour[i][0][0] += xmin-padding
newContour[i][0][1] += ymin-padding
return newContour
def getTopLeft(self, contours):
cnt = np.vstack(tuple(contours)) #combine contous
#draw contours
xmin = cnt[:,0,:][:, 0].min()
ymin= cnt[:,0,:][:, 1].min()
return [ymin, xmin]