python opencv中不规则的形状检测和测量

我试图在Python中使用OpenCV进行一些图像分析,但我认为这些图像本身会非常棘手,而且我从来没有做过这样的事情,所以我想说出我的逻辑,也许有一些想法/实际的代码来实现我想要做的事情,然后再投入大量时间走向错误的道路。

这个线程非常接近我想要达到的目标,在我看来,使用的图像应该比我的分析更难分析。 我会对这些彩色斑点的大小感兴趣,而不是它们与左上角的距离。 我也一直在关注这段代码,尽管我对引用对象并不特别感兴趣(现在只需要像素尺寸就足够了,并且可以在之后进行转换)。

以下是输入图像:

在这里输入图像描述

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

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

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

    我还没有完全理解所有的门槛,因此一些晶体被错过了,但由于它们被平均化了,所以目前这不是一个大问题。

    脚本存储处理后的图像,因此我还希望最终的输出图像与链接的SO线程中的“标记的斑点”图像相似,但每个blob都可以用其尺寸标注。

    这是一个(不完整的)理想化的输出结果,每个晶体都被识别,注释和测量(当我得到这个结果时,我很确定我可以处理测量)。

    在这里输入图像描述


    缩小图像和先前的代码尝试,因为它们使线程过长并且不再相关。


    编辑三:

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

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

    在这里输入图像描述

    编辑IV

    这里有一些我玩过的其他测试图片。 它比我期望的更小的晶体好得多,并且显然有很多事情可以用我尚未尝试的阈值完成。

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

    在这里输入图像描述

    这是另一个更大的晶体。

    在这里输入图像描述

    你会注意到这些颜色往往更加均匀,但更难辨别边缘。 我发现有一点令人惊讶的是,边缘填充对于一些图像来说有些过分热情,我认为这对于具有非常小的晶体的图像尤其如此,但实际上它似乎具有更多的效果在较大的。 从我们的实际显微镜来看,提高输入图像的质量可能有很大的空间,但是编程可以从系统中获得更多'松懈',我们的生活就会变得更加容易!


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

    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上运行了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()
    

    标记

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

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

    分割结果

    那么,这是整个管道。 你应该能够复制/粘贴每一部分,你应该能够得到相同的结果。 这条管线中最薄弱的部分是对梯度进行二值化处理,并确定分水岭的确切背景。 以某种方式对渐变进行二值化时,距离转换可能会很有用,但我还没有到达那里。 无论哪种方式......这是一个很酷的问题,我有兴趣看到你对这条管道做出的任何改变,或者它对其他冰晶图像的影响。

    链接地址: http://www.djcxy.com/p/39513.html

    上一篇: Irregular shape detection and measurement in python opencv

    下一篇: Rectangle detection / tracking using OpenCV