使用OpenCV去除孤立像素

9
我正在寻找一种使用OpenCV从二进制图像中去除孤立的白色像素的方法。类似的问题(OpenCV get rid of isolated pixels)有许多“答案”,但是没有一个对我有效。我已经尝试了各种打开和关闭的组合,但都没有成功。
这里的文章:

https://homepages.inf.ed.ac.uk/rbf/HIPR2/hitmiss.htm

建议我可以使用hit-or-miss操作来实现这个目的:

Structuring elements

1 用于在二值图像中定位孤立点。

原因是与直接使用腐蚀/膨胀时0的解释不同(在那里,0被解释为“不关心”,而不是“非白色”,这基本上是我想要的)。然而,使用此核心只会呈现原始图像。

我的输入图像是这样的:

Calc input image

你会注意到图像左侧有几个白色像素,我想摆脱它们。
以下是代码:
kernel = np.array([ [0, 0, 0],
                    [0, 1, 0],
                    [0, 0, 0]],np.uint8)

hitormiss = cv2.morphologyEx(input_image, cv2.MORPH_HITMISS, kernel)

cv2.imshow('hitormiss', hitormiss)

什么是去除像这样孤立的像素的正确方法?
更新:Alexander的答案非常好用且速度最快。另一个答案也提供了一种解决方案,即使用cv2.connectedComponents函数,但它需要更多的处理器资源。以下是一个使用该方法的函数:
def remove_isolated_pixels(self, image):
    connectivity = 8

    output = cv2.connectedComponentsWithStats(image, connectivity, cv2.CV_32S)

    num_stats = output[0]
    labels = output[1]
    stats = output[2]

    new_image = image.copy()

    for label in range(num_stats):
        if stats[label,cv2.CC_STAT_AREA] == 1:
            new_image[labels == label] = 0

    return new_image

2
你尝试过使用中值模糊吗?选择正确的核以避免丢失额外信息。 - Jeru Luke
是的,对二值图像进行模糊处理会过于破坏性,即使使用最小光圈大小为3也是如此。 - Tom
1
这里OpenCV出了一些奇怪的问题。我使用了ImageMagick并应用了一个0,0,0 0,1,0 0,0,0内核的Hit-or-Miss形态学,它立即隔离了您的噪点像素。我注意到您的图像是调色板 - 您是否检查了输入图像中期望找到噪声的实际值 - 我的意思是打印出它们的实际值和周围8个像素? - Mark Setchell
3个回答

15

我认为OpenCV的实现有问题。与此相关的OpenCV的GitHub问题似乎已经合并了一个拉取请求进行修复;我认为这已添加到OpenCV 3.3-rc中,如在拉取请求中引用,希望下次更新OpenCV时可以解决。我不确定问题是否由同一原因引起。

选定答案中的创造性解决方案很好,但我同意您的看法:一定有更好的方法,尽管实现有问题。

OpenCV Hit-or-miss教程上,他们说:

因此,击中或错过操作包括三个步骤:

  1. 用结构元素B1侵蚀图像A
  2. 用结构元素B2侵蚀图像A的补集A_c
  3. AND处理步骤1和步骤2的结果。

然后它继续说,这可以通过单个内核在击中或错过变换中完成,但正如我们所知,它是有问题的。所以我们来执行这些步骤。

import cv2
import numpy as np

# load image, ensure binary, remove bar on the left
input_image = cv2.imread('calc.png', 0)
input_image = cv2.threshold(input_image, 254, 255, cv2.THRESH_BINARY)[1]
input_image_comp = cv2.bitwise_not(input_image)  # could just use 255-img

kernel1 = np.array([[0, 0, 0],
                    [0, 1, 0],
                    [0, 0, 0]], np.uint8)
kernel2 = np.array([[1, 1, 1],
                    [1, 0, 1],
                    [1, 1, 1]], np.uint8)

hitormiss1 = cv2.morphologyEx(input_image, cv2.MORPH_ERODE, kernel1)
hitormiss2 = cv2.morphologyEx(input_image_comp, cv2.MORPH_ERODE, kernel2)
hitormiss = cv2.bitwise_and(hitormiss1, hitormiss2)

cv2.imshow('isolated.png', hitormiss)
cv2.waitKey()

孤立像素

接着,要移除它就像翻转hitormiss那样简单,并使用其作为maskcv2.bitwise_and()input_image一起使用。

hitormiss_comp = cv2.bitwise_not(hitormiss)  # could just use 255-img
del_isolated = cv2.bitwise_and(input_image, input_image, mask=hitormiss_comp)
cv2.imshow('removed.png', del_isolated)
cv2.waitKey()

去除孤立像素


注意:正如评论中所讨论的,使用kernel1进行腐蚀在这种特定情况下与输入的二进制图像相同,因此不需要进行此计算,并且在这种特定情况下还引入了一些其他不必要的步骤。但是,您可以使用不同于中间的单个1的核心,因此我将保持代码原样以使其对任何内核通用。


不错的侦探工作 - 感谢您花时间调查并分享。 - Mark Setchell
@MarkSetchell 谢谢!是的,我记得几个月前有人问过这个问题,并且报告了它,所以我在 GitHub 上进行了快速搜索并找到了它。 - alkasm
@buckminst 我相信你是对的——然而,我的回答应该是针对任何内核的通用性,而不仅仅是针对这个特定的内核。不过你发现得很好,我会在我的回答中做出注释。 - alkasm
我们知道OpenCV中的bug是否已经修复了吗? - SpaceMonkey55

3
这是我解决问题的方法:
import cv2 as cv
import numpy as np

# let's say "image" is a thresholded image

kernel = np.array([ [-1, -1, -1],
                    [-1,  1, -1],
                    [-1, -1, -1] ], dtype="int")
single_pixels = cv.morphologyEx(image, cv.MORPH_HITMISS, kernel)
single_pixels_inv = cv.bitwise_not(single_pixels)
image = cv.bitwise_and(image, image, mask=single_pixels_inv)

# now "image" shouldn't have alone pixels

3
  1. 运行连通区域标记算法
  2. 计算每个连通区域包含的像素数
  3. 对于像素数小于指定最小值的每个连通区域,将其所有像素转换为零。

谢谢!我已经更新了我的问题,并编写了一个实现这个建议的函数,它正在按照预期工作。 - Tom
2
使用connectedComponents是过度的。它需要额外的标签矩阵(我猜是int32类型的),并需要额外的努力来填充它的值。所以不,这不能是正确的答案。 - Maksym Ganenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接