了解等高线层次结构:如何在OpenCV中区分填充圆/轮廓和非填充圆/轮廓?

8

我无法区分以下两个轮廓。 cv2.contourArea() 给出的值是相同的。有没有Python函数可以区分它们? 如何使用轮廓层次确定差异?

图:圆/轮廓示例


5
cv2.findContours中,你可以将Retrieval Mode设置为RETR_CCOMP以获取轮廓的完整层次结构。对于未填充的圆形,您将获得两个轮廓,一个是外圆,一个是内圆。 - HansHirse
1个回答

20

为了区分填充和未填充的轮廓,可以在使用cv2.findContours()函数查找轮廓时,使用轮廓层次结构。具体来说,可以选择轮廓检索模式,可选地返回一个向量输出,其中包含有关图像拓扑的信息。有四种可能的模式:

  • cv2.RETR_EXTERNAL - 仅检索极端外部轮廓(没有层次结构)
  • cv2.RETR_LIST - 检索所有轮廓,而不建立任何层次关系
  • cv2.RETR_CCOMP - 检索所有轮廓,并将它们组织成两级层次结构。在顶层,是组件的外部边界,在第二层,是孔的边界。如果孔里有另一个轮廓,请仍然将其放在顶级。
  • cv2.RETR_TREE - 检索所有轮廓,并重建嵌套轮廓的完整层次结构

理解轮廓层次结构

因此,我们可以使用cv2.RETR_CCOMPcv2.RETR_TREE返回一个层次列表。以此图像为例:

当我们使用cv2.RETR_TREE参数时,轮廓按层次结构排列,每个对象的最外层轮廓位于顶部。沿着层次结构向下移动,每个新级别的轮廓表示每个对象的下一个内层轮廓。在上面的图像中,轮廓被着色以表示返回轮廓数据的分层结构。最外层的轮廓是红色的,并且它们位于层次结构的顶部。下一个内部轮廓 - 在这种情况下是骰子点数 - 是绿色的。

我们可以通过来自cv2.findContours函数调用的层次数组获得有关轮廓层次结构的信息。假设我们像这样调用函数:

(_, contours, hierarchy) = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

这段代码中保存在hierarchy变量中的第三个返回值是一个三维NumPy数组,其中有一行、X列和深度为4。这里的X列对应于函数找到的轮廓数。参数cv2.RETR_TREE使该函数查找每个对象的内部轮廓以及最外层轮廓。第0列对应于第一个轮廓,第1列对应于第二个轮廓,以此类推。

每列都有一个包含四个整数元素的数组,表示根据以下方案的其他轮廓索引:

[next, previous, first child, parent]

next指向此轮廓层级中下一个轮廓,previous指向此轮廓层级中上一个轮廓,first child指向包含在此轮廓内的第一个轮廓,parent指向包含此轮廓的轮廓。在所有情况下,值为-1表示不存在相应的nextpreviousfirst childparent轮廓。以下是一些示例hierarchy值的具体示例。这些值用方括号括起来,在每个条目之前都有轮廓的索引。如果您打印出轮廓结构数组,将会得到类似以下内容:

0:  [ 6 -1  1 -1]   18: [19 -1 -1 17]
1:  [ 2 -1 -1  0]   19: [20 18 -1 17]
2:  [ 3  1 -1  0]   20: [21 19 -1 17]
3:  [ 4  2 -1  0]   21: [22 20 -1 17]
4:  [ 5  3 -1  0]   22: [-1 21 -1 17]
5:  [-1  4 -1  0]   23: [27 17 24 -1]
6:  [11  0  7 -1]   24: [25 -1 -1 23]
7:  [ 8 -1 -1  6]   25: [26 24 -1 23]
8:  [ 9  7 -1  6]   26: [-1 25 -1 23]
9:  [10  8 -1  6]   27: [32 23 28 -1]
10: [-1  9 -1  6]   28: [29 -1 -1 27]
11: [17  6 12 -1]   29: [30 28 -1 27]
12: [15 -1 13 11]   30: [31 29 -1 27]
13: [14 -1 -1 12]   31: [-1 30 -1 27]
14: [-1 13 -1 12]   32: [-1 27 33 -1]
15: [16 12 -1 11]   33: [34 -1 -1 32]
16: [-1 15 -1 11]   34: [35 33 -1 32]
17: [23 11 18 -1]   35: [-1 34 -1 32]

第一个轮廓的条目是[6, -1, 1, -1]。这代表最外层轮廓中的第一个;请注意,轮廓没有特定的顺序,例如,默认情况下它们不是从左到右存储的。该条目告诉我们下一个骰子轮廓的索引是六,列表中没有前一个轮廓,此内部的第一个轮廓的索引为一,此轮廓没有父轮廓(不包含此轮廓的轮廓)。我们可以将hierarchy数组中的信息可视化为七棵树,每棵树代表图像中的一个骰子。

enter image description here

七个最外层轮廓都没有父轮廓,即它们在hierarchy条目的第四个字段中具有值-1。其中一个“根”下面的每个子节点表示最外层轮廓内的轮廓。请注意,在图表中,轮廓13和14在轮廓12下面。这两个轮廓表示最内层的轮廓,可能是噪音或其中一个点的失去的颜色。一旦我们了解轮廓如何排列成层次结构,我们就可以执行更复杂的任务,例如计算形状内轮廓的数量以及图像中对象的数量。


回到您的问题,我们可以使用层次结构来区分内部和外部轮廓,以确定轮廓是否被填充或未填充。我们可以将没有子节点的轮廓定义为填充轮廓,而至少有一个子节点的轮廓定义为未填充轮廓。因此,对于输入图像的此屏幕截图(去掉框):

enter image description here

结果

enter image description here

代码

import cv2

# Load image, grayscale, Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Filter using contour hierarchy
cnts, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2:]
hierarchy = hierarchy[0]
for component in zip(cnts, hierarchy):
    currentContour = component[0]
    currentHierarchy = component[1]
    x,y,w,h = cv2.boundingRect(currentContour)
    # Has inner contours which means it is unfilled
    if currentHierarchy[3] > 0:
        cv2.putText(image, 'Unfilled', (x,y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (36,255,12), 2)
    # No child which means it is filled
    elif currentHierarchy[2] == -1:
        cv2.putText(image, 'Filled', (x,y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (36,255,12), 2)

cv2.imshow('image', image)
cv2.waitKey()

6
非常好的解释! - fmw42
3
这样详细的回答,包含有注释的代码片段和图片,是其他人应该追求的。(+1) - George Profenza
1
这个回答真是太棒了!感谢@nathancy所做的一切努力。 - PydPiper
感谢您的解决方案,但检查层次结构并不完全正确。 - Hung Dang

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