pythonopencv中的不规则形状检测与测量
Irregular shape detection and measurement in python opencv
我正在尝试在 python 中使用 OpenCV 进行一些图像分析,但我认为图像本身会非常棘手,而且我以前从未做过这样的事情,所以我想在我投入大量时间走上错误的道路之前,试探一下我的逻辑并可能获得一些 ideas/practical 代码来实现我想做的事情。
comes pretty close to what I want to achieve, and in my opinion, uses an image that should be even harder to analyse than mine. I'd be interested in the size of those coloured blobs though, rather than their distance from the top left. I've also been following this code,虽然我对参考对象不是特别感兴趣(仅以像素为单位的尺寸现在就足够了,以后可以转换)。
这是输入图像:
你看的是冰 crystals,我想求出每块冰的平均大小。每个的边界都定义得相当好,所以从概念上讲这是我的方法,如果这是错误的方法,我想听听任何建议或评论:
- 导入 RGB 格式的图像并将其转换为 8 位灰度(根据我在 ImageJ 中的测试,32 位会更好,但我还没有弄清楚如何在 OpenCV 中执行此操作)。
- 可选地对边缘进行高斯模糊以去除噪声
- Canny 边缘检测器拾取线条
- 进行形态学变换(侵蚀 + 膨胀)以尝试进一步关闭边界。
在这一点上,我似乎可以做出选择。我可以将图像二值化,并测量高于阈值的斑点(即,如果斑点为白色,则为最大值像素),或者通过更完整地闭合和填充轮廓来继续边缘检测。虽然看了那个教程,轮廓看起来很复杂,虽然我可以在我的图像上获得 运行 的代码,但它没有正确检测到 crystals(不足为奇)。我也不确定是否应该在二值化之前进行变形变换?
假设我能让所有这些都起作用,我认为一个合理的度量是最小封闭框或椭圆的最长轴。
我还没有完全解决所有的阈值,因此一些 crystals 被遗漏了,但由于它们是平均的,所以目前这不是一个大问题.
脚本在运行过程中存储处理过的图像,所以我也希望最终输出图像类似于链接 SO 线程中的 'labelled blobs' 图像,但每个 blob 可能都用其尺寸注释.
这是一个(不完整的)理想化输出的样子,每个 crystal 都被识别、注释和测量(很确定我可以在到达那一步时处理测量)。
删节了图像和以前的代码尝试,因为它们使线程过长并且不再相关。
编辑三:
根据评论,分水岭算法看起来非常接近实现我所追求的目标。这里的问题是很难分配算法所需的标记区域 (http://docs.opencv.org/3.2.0/d3/db4/tutorial_py_watershed.html).
我不认为这是可以通过二值化过程的阈值来解决的问题,因为颗粒的表观颜色变化远远超过该线程中的玩具示例。
编辑四
这是我玩过的其他一些测试图像。使用较小的 crystals,它的表现比我预期的要好得多,而且显然有很多技巧可以用我还没有尝试过的阈值来完成。
这里是1,左上到右下分别对应Alex下面步骤输出的图片
这是第二个 crystals。
您会注意到它们的颜色往往更均匀,但边缘更难辨别。我发现有些令人惊讶的是边缘填充对某些图像有点过分热心,我本以为对于非常小的 crystals 的图像尤其如此,但实际上它似乎对较大的影响更大。实际显微镜输入图像的质量可能还有很大提升空间,但是 'slack' 编程可以从系统中获取的越多,我们的生活就会越轻松!
正如我在评论中提到的,分水岭看起来是解决这个问题的好方法。但是正如您所回答的那样,为标记定义前景和背景是困难的部分!我的想法是使用形态学梯度沿着冰 crystals 获得良好的边缘并从那里开始工作;形态梯度似乎很有效。
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()
现在我们有几个问题。它需要一些清理,因为它很乱,此外,图像边缘的冰 crystal 出现了——但我们不知道这些 crystal 到底在哪里结束所以我们实际上应该忽略那些。为了从蒙版中移除那些,我遍历了边缘上的像素并使用 floodFill()
将它们从二值图像中移除。不要在这里混淆行和列的顺序; if
语句指定图像矩阵的行和列,而 floodFill()
的输入期望 points (即 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()
所以这张图片被标记为 "foreground" 因为它有我们想要分割的对象的确定前景。现在我们需要为对象创建一个确定的背景。现在,我以一种天真的方式做到了这一点,这只是将你的前景扩大了一堆,这样你的对象就可能全部定义在前景中。但是,您可能会以不同的方式使用原始遮罩甚至渐变以获得更好的定义。尽管如此,这仍然可以正常工作,但不是很健壮。
# 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()
所以所有的黑色都是 "sure background" 的分水岭。我还创建了未知矩阵,它是前景和背景之间的区域,这样我们就可以预先将传递给分水岭的标记标记为 "hey, these pixels are definitely in the foreground, these others are definitely background, and I'm not sure about these ones between." 现在剩下要做的就是 运行 分水岭!首先,用连通分量标记前景图像,识别未知部分和背景部分,并将它们传入:
# 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)
您会注意到我 运行 watershed()
在 img
上。您可以在图像的模糊版本(可能是中值模糊---我试过这个并且 crystals 的边界更平滑)或图像的其他预处理版本上试验 运行ning定义更好的边界或其他东西。
将标记可视化需要一些工作,因为它们在 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()
好吧,这就是整个管道。您应该能够 copy/paste 一行中的每个部分,并且您应该能够获得相同的结果。这条管道最薄弱的部分是二值化梯度和定义分水岭的确定背景。距离 t运行sform 可能对以某种方式对梯度进行二值化很有用,但我还没有做到。无论哪种方式...这是一个很酷的问题,我很想知道您对此管道所做的任何更改或它在其他 ice-crystal 图像上的表现如何。
我正在尝试在 python 中使用 OpenCV 进行一些图像分析,但我认为图像本身会非常棘手,而且我以前从未做过这样的事情,所以我想在我投入大量时间走上错误的道路之前,试探一下我的逻辑并可能获得一些 ideas/practical 代码来实现我想做的事情。
这是输入图像:
你看的是冰 crystals,我想求出每块冰的平均大小。每个的边界都定义得相当好,所以从概念上讲这是我的方法,如果这是错误的方法,我想听听任何建议或评论:
- 导入 RGB 格式的图像并将其转换为 8 位灰度(根据我在 ImageJ 中的测试,32 位会更好,但我还没有弄清楚如何在 OpenCV 中执行此操作)。
- 可选地对边缘进行高斯模糊以去除噪声
- Canny 边缘检测器拾取线条
- 进行形态学变换(侵蚀 + 膨胀)以尝试进一步关闭边界。
在这一点上,我似乎可以做出选择。我可以将图像二值化,并测量高于阈值的斑点(即,如果斑点为白色,则为最大值像素),或者通过更完整地闭合和填充轮廓来继续边缘检测。虽然看了那个教程,轮廓看起来很复杂,虽然我可以在我的图像上获得 运行 的代码,但它没有正确检测到 crystals(不足为奇)。我也不确定是否应该在二值化之前进行变形变换?
假设我能让所有这些都起作用,我认为一个合理的度量是最小封闭框或椭圆的最长轴。
我还没有完全解决所有的阈值,因此一些 crystals 被遗漏了,但由于它们是平均的,所以目前这不是一个大问题.
脚本在运行过程中存储处理过的图像,所以我也希望最终输出图像类似于链接 SO 线程中的 'labelled blobs' 图像,但每个 blob 可能都用其尺寸注释.
这是一个(不完整的)理想化输出的样子,每个 crystal 都被识别、注释和测量(很确定我可以在到达那一步时处理测量)。
删节了图像和以前的代码尝试,因为它们使线程过长并且不再相关。
编辑三:
根据评论,分水岭算法看起来非常接近实现我所追求的目标。这里的问题是很难分配算法所需的标记区域 (http://docs.opencv.org/3.2.0/d3/db4/tutorial_py_watershed.html).
我不认为这是可以通过二值化过程的阈值来解决的问题,因为颗粒的表观颜色变化远远超过该线程中的玩具示例。
编辑四
这是我玩过的其他一些测试图像。使用较小的 crystals,它的表现比我预期的要好得多,而且显然有很多技巧可以用我还没有尝试过的阈值来完成。
这里是1,左上到右下分别对应Alex下面步骤输出的图片
这是第二个 crystals。
您会注意到它们的颜色往往更均匀,但边缘更难辨别。我发现有些令人惊讶的是边缘填充对某些图像有点过分热心,我本以为对于非常小的 crystals 的图像尤其如此,但实际上它似乎对较大的影响更大。实际显微镜输入图像的质量可能还有很大提升空间,但是 'slack' 编程可以从系统中获取的越多,我们的生活就会越轻松!
正如我在评论中提到的,分水岭看起来是解决这个问题的好方法。但是正如您所回答的那样,为标记定义前景和背景是困难的部分!我的想法是使用形态学梯度沿着冰 crystals 获得良好的边缘并从那里开始工作;形态梯度似乎很有效。
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()
现在我们有几个问题。它需要一些清理,因为它很乱,此外,图像边缘的冰 crystal 出现了——但我们不知道这些 crystal 到底在哪里结束所以我们实际上应该忽略那些。为了从蒙版中移除那些,我遍历了边缘上的像素并使用 floodFill()
将它们从二值图像中移除。不要在这里混淆行和列的顺序; if
语句指定图像矩阵的行和列,而 floodFill()
的输入期望 points (即 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()
所以这张图片被标记为 "foreground" 因为它有我们想要分割的对象的确定前景。现在我们需要为对象创建一个确定的背景。现在,我以一种天真的方式做到了这一点,这只是将你的前景扩大了一堆,这样你的对象就可能全部定义在前景中。但是,您可能会以不同的方式使用原始遮罩甚至渐变以获得更好的定义。尽管如此,这仍然可以正常工作,但不是很健壮。
# 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()
所以所有的黑色都是 "sure background" 的分水岭。我还创建了未知矩阵,它是前景和背景之间的区域,这样我们就可以预先将传递给分水岭的标记标记为 "hey, these pixels are definitely in the foreground, these others are definitely background, and I'm not sure about these ones between." 现在剩下要做的就是 运行 分水岭!首先,用连通分量标记前景图像,识别未知部分和背景部分,并将它们传入:
# 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)
您会注意到我 运行 watershed()
在 img
上。您可以在图像的模糊版本(可能是中值模糊---我试过这个并且 crystals 的边界更平滑)或图像的其他预处理版本上试验 运行ning定义更好的边界或其他东西。
将标记可视化需要一些工作,因为它们在 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()
好吧,这就是整个管道。您应该能够 copy/paste 一行中的每个部分,并且您应该能够获得相同的结果。这条管道最薄弱的部分是二值化梯度和定义分水岭的确定背景。距离 t运行sform 可能对以某种方式对梯度进行二值化很有用,但我还没有做到。无论哪种方式...这是一个很酷的问题,我很想知道您对此管道所做的任何更改或它在其他 ice-crystal 图像上的表现如何。