正如我在评论中提到的那样,对于这个问题,分水岭似乎是一个很好的方法.但是当你回答时,定义标记的前景和背景是困难的部分!我的想法是使用形态梯度沿着冰晶获得良好的边缘并从那里开始工作;形态梯度似乎很有效.
import numpy as np
import cv2
img = cv2.imread('image.png')
blur = cv2.GaussianBlur(img, (7, 7), 2)
h, w = img.shape[:2]
# Morphological gradient
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
gradient = cv2.morphologyEx(blur, cv2.MORPH_GRADIENT, kernel)
cv2.imshow('Morphological gradient', gradient)
cv2.waitKey()
从这里开始,我使用一些阈值来对梯度进行二值化.可能有一种更清洁的方法来做到这一点……但这比我尝试过的其他一些想法更好.
# Binarize gradient
lowerb = np.array([0, 0, 0])
upperb = np.array([15, 15, 15])
binary = cv2.inRange(gradient, lowerb, upperb)
cv2.imshow('Binarized gradient', binary)
cv2.waitKey()
现在我们有几个问题.它需要一些清理,因为它很乱,而且,图像边缘的冰晶出现了 – 但我们不知道那些晶体实际上在哪里结束所以我们应该忽略那些.为了从掩码中删除它们,我遍历边缘上的像素并使用floodFill()从二进制图像中删除它们.不要在行和列的顺序上混淆; if语句指定图像矩阵的行和列,而floodFill()的输入需要点(即x,y形式,与row,col相反).
# Flood fill from the edges to remove edge crystals
for row in range(h):
if binary[row, 0] == 255:
cv2.floodFill(binary, None, (0, row), 0)
if binary[row, w-1] == 255:
cv2.floodFill(binary, None, (w-1, row), 0)
for col in range(w):
if binary[0, col] == 255:
cv2.floodFill(binary, None, (col, 0), 0)
if binary[h-1, col] == 255:
cv2.floodFill(binary, None, (col, h-1), 0)
cv2.imshow('Filled binary gradient', binary)
cv2.waitKey()
大!现在只是打开和关闭一些清理它…
# Cleaning up mask
foreground = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
foreground = cv2.morphologyEx(foreground, cv2.MORPH_CLOSE, kernel)
cv2.imshow('Cleanup up crystal foreground mask', foreground)
cv2.waitKey()
所以这个图像被标记为“前景”,因为它具有我们想要分割的对象的可靠前景.现在我们需要创建一个确定的对象背景.现在,我以天真的方式做到了这一点,这只是为了使你的前景成长,所以你的对象可能都是在那个前景中定义的.但是,您可以使用原始蒙版或甚至渐变以不同的方式来获得更好的定义.尽管如此,这仍然可以,但不是很强大.
# Creating background and unknown mask for labeling
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (17, 17))
background = cv2.dilate(foreground, kernel, iterations=3)
unknown = cv2.subtract(background, foreground)
cv2.imshow('Background', background)
cv2.waitKey()
因此所有的黑色都是流域的“确定背景”.我还创建了未知矩阵,它是前景和背景之间的区域,这样我们就可以预先标记传递到分水岭的标记,“嘿,这些像素肯定在前景中,这些像素肯定是背景,我我不确定这些之间.“现在剩下要做的就是分水岭!首先,使用连接的组件标记前景图像,识别未知和背景部分,并将它们传递到:
# Watershed
markers = cv2.connectedComponents(foreground)[1]
markers += 1 # Add one to all labels so that background is 1, not 0
markers[unknown==255] = 0 # mark the region of unknown with zero
markers = cv2.watershed(img, markers)
你会注意到我在img上运行了分水岭().您可以尝试在图像的模糊版本上运行它(可能是中间模糊 – 我尝试了这个并且为晶体获得了更平滑的边界)或其他预处理版本的图像,这些版本定义了更好的边界或某些东西.
将标记可视化需要一些工作,因为它们在uint8图像中都是小数字.所以我做的是在0到179中为它们分配一些色调并在HSV图像中设置,然后转换为BGR以显示标记:
# Assign the markers a hue between 0 and 179
hue_markers = np.uint8(179*np.float32(markers)/np.max(markers))
blank_channel = 255*np.ones((h, w), dtype=np.uint8)
marker_img = cv2.merge([hue_markers, blank_channel, blank_channel])
marker_img = cv2.cvtColor(marker_img, cv2.COLOR_HSV2BGR)
cv2.imshow('Colored markers', marker_img)
cv2.waitKey()
最后,将标记覆盖到原始图像上以检查它们的外观.
# Label the original image with the watershed markers
labeled_img = img.copy()
labeled_img[markers>1] = marker_img[markers>1] # 1 is background color
labeled_img = cv2.addWeighted(img, 0.5, labeled_img, 0.5, 0)
cv2.imshow('watershed_result.png', labeled_img)
cv2.waitKey()
嗯,那就是整个管道.您应该能够连续复制/粘贴每个部分,并且您应该能够获得相同的结果.该管道中最薄弱的部分是对梯度进行二值化并定义流域的确定背景.距离变换可能有助于以某种方式对梯度进行二值化,但我还没有到达那里.无论哪种方式……这是一个很酷的问题,我很想看到你对这个管道所做的任何改变,或者它对其他冰晶图像的影响.