python-open中的不规则形状检测

2024-05-14 23:12:17 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在尝试使用python中的OpenCV进行一些图像分析,但我认为图像本身会非常棘手,而且我以前从未做过类似的事情,所以我想在花大量时间走上错误的道路之前,先弄清我的逻辑,也许能得到一些想法/实用的代码来实现我想做的事情。

This thread非常接近我想要达到的目标,在我看来,使用的图像应该比我的更难分析。不过,我感兴趣的是那些彩色斑点的大小,而不是它们与左上角的距离。我也一直在关注this code,尽管我对一个引用对象不是特别感兴趣(仅像素的维度就足够了,可以在之后进行转换)。

这是输入图像:

enter image description here

你看的是冰晶,我想找出每个冰晶的平均尺寸。每种方法的界限都有很好的定义,所以在概念上这是我的方法,如果这是一个错误的方法,我希望听到任何建议或意见:

  1. RGB中的图像被导入并转换为8位灰度(根据我在ImageJ中的测试,32位会更好,但我还没有想出如何在OpenCV中实现这一点)。
  2. 边缘可以选择高斯模糊来去除噪声
  3. 一个精明的边缘探测器探测到这些线
  4. 形态转换(侵蚀+扩张)是为了尝试进一步关闭边界。

在这一点上,我似乎有一个选择。我可以二值化图像,测量高于阈值的斑点(即,如果斑点为白色,则为最大值像素),或者通过更充分地闭合和填充轮廓来继续边缘检测。虽然看了那个教程,轮廓看起来很复杂,尽管我可以在我的图像上运行代码,但它并不能正确地检测晶体(这并不奇怪)。我也不确定在二值化之前是否应该变形转换?

假设我能让所有这些工作,我想一个合理的措施将是最长的最小包围盒或椭圆轴。

我还没有完全消除所有的阈值,因此一些晶体被错过,但由于他们是平均的,这并不是一个巨大的问题,目前。

脚本在运行过程中存储经过处理的图像,因此我还希望最终输出图像类似于链接的so线程中的“标记blob”图像,但是每个blob可能都用其维度进行了注释。

下面是一个(不完整的)理想化输出的样子,每个晶体都被识别、注释和测量(当我走到这一步的时候,我很确定我可以处理测量)。

enter image description here


缩短图像和以前的代码尝试,因为它们使线程过长,不再相关


编辑三:

根据评论,分水岭算法看起来非常接近实现我所追求的目标。但这里的问题是,很难分配算法所需的标记区域(http://docs.opencv.org/3.2.0/d3/db4/tutorial_py_watershed.html)。

我不认为这是可以通过二值化过程用阈值来解决的问题,因为颗粒的表面颜色变化比线程中的玩具示例大得多。

enter image description here

编辑四

下面是我玩过的其他一些测试图片。它的性能比我预期的小晶体要好得多,显然有很多精细化可以用我还没有尝试过的阈值来完成。

这里是1,从左上到右下对应于Alex下面步骤中输出的图像。

enter image description here

这是第二个更大的水晶。

enter image description here

你会注意到它们的颜色往往更均匀,但边缘更难辨别。我有点吃惊的是边缘对一些图片来说,泛光是有点过分热情,我本以为这会特别适用于带有非常小晶体的图片,但实际上它似乎对较大的晶体有更大的影响。可能还有很大的空间来提高我们实际显微镜输入图像的质量,但是程序从系统中得到的“松弛”越多,我们的生活就越容易!


Tags: 方法代码图像目标错误图片阈值像素
1条回答
网友
1楼 · 发布于 2024-05-14 23:12:17

正如我在评论中提到的,分水岭看起来是解决这个问题的一个好方法。但正如你所回答的,为标记定义前景和背景是困难的部分!我的想法是利用形态梯度沿着冰晶获得好的边缘并从那里开始工作;形态梯度似乎工作得很好。

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()

Morph gradient

从这里开始,我使用一些阈值对梯度进行二值化。也许有一个更干净的方法来做这个…但这碰巧比我尝试过的其他十几个想法更有效。

# 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()

Binarized grad

现在我们有几个问题。它需要清理一下,因为它很凌乱,而且图像边缘的冰晶正在显现——但我们不知道这些冰晶到底在哪里结束,所以我们应该忽略它们。为了从遮罩中删除这些,我循环遍历边缘上的像素,并使用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()

Filled binary grad

太好了!现在只要打开和关闭。。。

# 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()

Background

所以所有的黑色都有“确定的背景”作为分水岭。我还创建了未知矩阵,这是前景和背景之间的区域,这样我们就可以预先标记通过分水岭的标记为“嘿,这些像素肯定在前景中,这些其他的肯定是背景,我不确定这些之间的。”现在剩下要做的就是运行分水岭!首先,使用连接的组件标记前景图像,标识未知部分和背景部分,然后将它们传入:

# 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上运行了watershed()。你可以在图像的模糊版本(可能是中值模糊——我试过了,晶体的边界变得更平滑)或其他定义更好边界的预处理版本上运行它。

由于标记都是一张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()

Markers

最后,将标记覆盖到原始图像上,检查它们的外观。

# 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()

Segmentation result

好吧,这是整个管道。您应该能够将每个部分复制/粘贴到一行中,并且应该能够得到相同的结果。这条管道最薄弱的部分是对梯度进行二值化,并为流域确定背景。距离变换在二值化梯度方面可能有用,但我还没有达到目的。不管怎样……这是一个很酷的问题,我很想看看你对这个管道做了什么改变,或者在其他冰晶图片上的表现。

相关问题 更多 >

    热门问题