python opencv

时间:2017-09-04 11:36:47

标签: python image opencv image-processing computer-vision

我试图在python中使用OpenCV进行一些图像分析,但我认为这些图像本身会非常棘手,我之前从未做过这样的事情,所以我想发出声音在我投入大量时间走错路之前,我的逻辑并且可能会得到一些想法/实用代码来实现我想做的事情。

This thread非常接近我想要实现的目标,在我看来,使用的图像应该比我的更难分析。我对这些彩色斑点的大小感兴趣,而不是它们与左上角的距离。我也一直在关注this code,虽然我对参考对象并不特别感兴趣(仅像素的尺寸现在已足够,之后可以转换)。

这是输入图片:

enter image description here

你正在看的是冰晶,我想找到每个冰晶的平均大小。每个界限都有相当明确的定义,所以概念上这是我的方法,如果这是错误的方法,我想听听任何建议或意见:

  1. RGB中的图像被导入并转换为8位灰色(根据我在ImageJ中的测试,32会更好,但我还没想到如何在OpenCV中做到这一点)。
  2. 边缘可选择高斯模糊以消除噪音
  3. Canny边缘检测器拾取线
  4. 进行形态变换(侵蚀+膨胀)以试图进一步缩小边界。
  5. 此时似乎我可以选择。我可以对图像进行二值化,并测量高于阈值的斑点(即,如果斑点为白色则为最大值像素),或者通过更充分地关闭和填充轮廓来继续边缘检测。虽然看了那个教程,轮廓看起来很复杂,虽然我可以让代码在我的图像上运行,但它并没有正确地检测到晶体(不出所料)。我也不确定在进行二值化之前是否应该进行变换?

    假设我可以完成所有工作,我认为合理的测量将是最小封闭框或椭圆的最长轴。

    我还没有完全解决所有的门限,因此有些晶体被遗漏了,但由于它们被平均,所以目前还没有出现大问题。

    脚本在处理过的图像时会存储处理后的图像,所以我也希望最终的输出图像类似于标记的blob'链接的SO线程中的图像,但每个blob可以用它的尺寸注释。

    这是一个(不完整的)理想化输出的样子,每个晶体都被识别,注释和测量(当我走得那么远时,我非常确定我可以解决测量问题。)

    enter image description here

    对图像和之前的代码尝试进行了简化,因为它们使线程过长且不再相关。

    编辑III:

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

    我不认为这可以通过二值化过程通过阈值来解决,因为谷物的表观颜色变化远远大于该线程中的玩具示例。

    enter image description here

    编辑IV

    以下是我玩过的其他几张测试图片。它的表现比我预期的更小的水晶要好得多,显然很多可以通过我尚未尝试的阈值来完成。

    此处的1,左上角到右下角对应于Alex在以下步骤中输出的图像。

    enter image description here

    这是第二个有更大晶体的人。

    enter image description here

    您会注意到这些颜色往往更均匀,但更难辨别边缘。我发现有点令人惊讶的是边缘泛滥对某些图像有点过于热心,我原本认为对于具有非常微小晶体的图像尤其如此,但实际上它似乎有更多的效果在较大的那些。从我们的实际显微镜中可能有很大的空间来改善输入图像的质量,但是更多的松弛'编程可以从系统中获取,我们的生活就会越轻松!

1 个答案:

答案 0 :(得分:12)

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

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()的输入需要 points (即x, y形式,与{{ 1}})。

row, col

Filled binary grad

大!现在只需要打开和关闭就可以清理它......

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

Foreground

因此,此图像被标记为“前景”,因为它具有我们想要分割的对象的可靠前景。现在我们需要创建一个确定的对象背景。现在,我以天真的方式做到了这一点,这只是为了使你的前景成长,所以你的对象可能都是在那个前景中定义的。但是,您可以使用原始蒙版或甚至渐变以不同的方式来获得更好的定义。不过,这样做还可以,但效果不是很好。

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

Background

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

"""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) 上运行了watershed()。您可以尝试在图像的模糊版本上运行它(可能是中间模糊 - 我尝试了这个并且为晶体获得了更平滑的边界)或其他预处理版本的图像,这些版本定义了更好的边界或某些东西。

将标记可视化需要一些工作,因为它们都是img图像中的小数字。所以我做的是在0到179中为它们分配一些色调并在HSV图像中设置,然后转换为BGR以显示标记:

uint8

Markers

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

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

Segmentation result

嗯,这就是整个管道。您应该能够连续复制/粘贴每个部分,并且您应该能够获得相同的结果。该管道中最薄弱的部分是对梯度进行二值化并定义流域的确定背景。距离变换可能有助于以某种方式对梯度进行二值化,但我还没有到达那里。无论哪种方式......这是一个很酷的问题,我很想看到你对这个管道做出的任何改变,或者它对其他冰晶图像的影响。