【参考】opencv文档
Contours Hierarchy

关于边界的继承结构,例如边界的父子结构

使用 cv2.findContours(),我们需要选择边界回溯模式,例如:cv2.RETR_LIST 或者 cv2.RETR_TREE
函数得到的三个数组,第一个是图片,第二个是边界,第三个是继承结构 hierarchy,一般我们都只用后面两个。
形式如:
python:

contours, hierarchy = cv2.findContours(img, 
             cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

有时一些图片对象在不同的位置上,但有时候,一些图形会在另一些图形之中,被包含在内部。
例如:黑色区域就被包含在白色区域中,而白色区域外面又有黑色区域,这样就把外层称为是 父层(红色),内层称为是孩子层(绿色)。
在这里插入图片描述
这样一来,我们就可以找出不同的形状间的关系了,例如我们可以分辨出一个边界是如何和其他边界联系的了,例如它是否是一些边界的父边界,或者是另一些边界的孩子边界,这就叫做hierarchy

例如:
在这里插入图片描述

说明:

在这张图片里,0,1,2边界是最外层的边界,称为0层(绿色),或者简单地说,他们在同一层次上(没有互相之间的包含关系)
下一层是2a,它是层2的子层,注意是白框内部的部分!我们这里说的一直是边界!!所以它是层1(粉色)。
类似的,边界3是2a的子层,是层2(蓝色),
3a是3的子层,是层3(红色)
最后是4,5层,他们是3a层的子层,是最后一层,是层4(黄色)
在这里插入图片描述所以每个边界都有关于它是哪一个层次的信息,记录结构为:[Next, Previous, First_Child, Parent]
[下一个,上一个,第一个孩子层,父层(上一层)]
参考java版本Java OpenCV findContours函数RETR_CCOMP轮廓顺序

ds[0]->同层级的下一个轮廓(不存在为-1)
ds[1]->同层级的上一个轮廓(不存在为-1)
ds[2]->第一个子轮廓(不存在为-1)
ds[3]->父轮廓(不存在为-1)
例如:

对于边界0,它的下一个同级边界是边界1,上一个同级轮廓没有,第一个子轮廓没有,没有父轮廓,所以是
[1,-1,-1,-1]

边界2的上一个同级边界是边界1,下一个同级边界没有,第一个子轮廓是2a,没有父轮廓,所以是
[-1,1,2a,-1]

不同的边界flag表示不一样的意思:

cv2.RETR_LIST, cv2.RETR_TREE, cv2.RETR_CCOMP, cv2.RETR_EXTERNAL

1. RETR_LIST

父子结构都不管了,他们只是单纯的边界结构,他们都属于同一层。所以得到的结果是:

>>> hierarchy
    2 array([[[ 1, -1, -1, -1],
    3         [ 2,  0, -1, -1],
    4         [ 3,  1, -1, -1],
    5         [ 4,  2, -1, -1],
    6         [ 5,  3, -1, -1],
    7         [ 6,  4, -1, -1],
    8         [ 7,  5, -1, -1],
    9         [-1,  6, -1, -1]]])

2. RETR_EXTERNAL

这个模式只返回外层边界,所有的子层都不要了
在这个规则下,只考虑“最老的人”,其他人全都不考虑。

    1 >>> hierarchy
    2 array([[[ 1, -1, -1, -1],
    3         [ 2,  0, -1, -1],
    4         [-1,  1, -1, -1]]])

当你只要外层边界的时候,这个标志位很有用。

3. RETR_CCOMP

这个标志会返回全部的边界,但是会把它们分为两层,可以算是一种简化吧,例如:
在这里插入图片描述
用两种颜色标志就是下面这样的:一层绿色一层粉色

在这里插入图片描述
所以可以看到只有两层结构,要么是外层要么是里层,

    1 >>> hierarchy
    2 array([[[ 3, -1,  1, -1],
    3                [ 2, -1, -1,  0],
    4                [-1,  1, -1,  0],
    5                [ 5,  0,  4, -1],
    6                [-1, -1, -1,  3],
    7                [ 7,  3,  6, -1],
    8                [-1, -1, -1,  5],
    9                [ 8,  5, -1, -1],
   10               [-1,  7, -1, -1]]])
注意:
ds[0]->同层级的下一个轮廓(不存在为-1)
ds[1]->同层级的上一个轮廓(不存在为-1)
ds[2]->第一个子轮廓(不存在为-1)
ds[3]->父轮廓(不存在为-1)

这里为什么父层不是为0或者-1呢?
因为每层标志都必须用真实标志,所以层也要按照父子关系来,需要用每一层的真实标号。

如果只想显示父层或者子层,可以通过层与层记录之间的关系来达到,隔一层记录一次,或者仅仅遍历某层之间的上一个和下一个。

例如,

hierachy[0][0][0]开始,有上一个轮廓不存在,下一个轮廓是3,
所以查询轮廓hierachy[0][0][3],可见[ 5, 0, 4, -1],同级别的下一个轮廓是5,查询hierachy[0][0][5],下一个轮廓是hierachy[0][0][7] [ 8, 5, -1, -1],,下一个轮廓是hierachy[0][0][8], [-1, 7, -1, -1],下一个轮廓是-1不存在,得到了0,3,5,7,8,都是一层,和图片相符,而且有子轮廓,是外层(父层)
同理,1,2,4,6是一层,都是子层(内层)

代码实现:

OpenCV中findContours轮廓提取一个边缘只对应的一个轮廓
opencv轮廓提取的时候,图像中一条边缘有查找到两个轮廓。当然只提取最外轮廓是不会出现重复情况,但设置提取所有轮廓会出现两个轮廓,对于利用得到的轮廓进一步处理带来不必要的麻烦,所以需要从一条轮廓开始,然后找他的同级链,直到找到结尾。
这样的处理方式就不会有双层轮廓的问题了。

由于截图有些噪点,边框白色部分又比较小,显示出来的效果一般。
在这里插入图片描述
当我把图片反转为~img时,用于检测表格:
在这里插入图片描述
表格检测代码:

import cv2


if __name__ =='__main__':
    image = cv2.imread('mask1.jpg')
    img = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
    img = cv2.adaptiveThreshold(~img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, -2) 
    contours,hierachy = cv2.findContours(img, cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
    # 查找内层的例子:
    i=0
    hierach_id = 0
    inner_class =[]
    # 一直搜索到找到没有子轮廓的结构,且注意搜索长度不能超过边界数目
    while(hierachy[0][i][2]!=-1 and hierachy[0][i][1]==-1 and i<len(contours) and hierachy[0][i][3]!=-1):
        hierach_id = i
        i+=1
    print(hierach_id,hierachy[0][hierach_id])
    while(hierachy[0][hierach_id][0]!=-1):
        inner_class.append(contours[hierach_id])
        hierach_id=hierachy[0][hierach_id][0]
        print(hierach_id,hierachy[0][hierach_id])
    for cnt in inner_class: # 画图
        cv2.drawContours(image,[cnt],0,(122,122,0),3)
    cv2.imshow('img',image)
    cv2.waitKey(10000)

img = cv2.adaptiveThreshold(~img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, -2)

改为:

img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, -2) 

也就是图片黑白反转回来就是检测外侧边框了:
在这里插入图片描述
在这里插入图片描述

最后一个4. RETR_TREE

It even tells, who is the grandpa, father, son, grandson and even beyond… 😃.
也就说,保存了全部的层次结构,

例如:

在这里插入图片描述
得到的结构:

    1 >>> hierarchy
    2 array([[[ 7, -1,  1, -1],
    3         [-1, -1,  2,  0],
    4         [-1, -1,  3,  1],
    5         [-1, -1,  4,  2],
    6         [-1, -1,  5,  3],
    7         [ 6, -1, -1,  4],
    8         [-1,  5, -1,  4],
    9         [ 8,  0, -1, -1],
   10         [-1,  7, -1, -1]]])
用途:
ds[0]->同层级的下一个轮廓(不存在为-1)
ds[1]->同层级的上一个轮廓(不存在为-1)
ds[2]->第一个子轮廓(不存在为-1)
ds[3]->父轮廓(不存在为-1)

这个其实就可以随意diy使用了,想要哪层要哪层,
保存了全部的信息,
如果需要查找最里面的孔洞,直接找

hierachy[0][i][2]==-1 and hierachy[0][i][1]==-1 

的层就行。

例如下面的图片:
在这里插入图片描述这是用 RETR_CCMP标志位得到的。但是,现在我不需要红色标注的边界,我需要绿色标注的边界,也就是最小的方格子里面的内容。
在这里插入图片描述如何获取这些内容呢?
首先我们观察到,他们都是最里层,所以有:

# 无子轮廓                  无上一个轮廓,找到同辈的起点
hierachy[0][i][2]==-1 and hierachy[0][i][1]==-1 

找到第一个最内层,然后找寻它的同层就好了。

——理论上是这样,然而经过反复实验:

在这里插入图片描述很明显的其中有一些边框是存在子结构的,
但是hierachy[0][i][2]也是-1,
可能这个标志位的注释和实现其实也是存在问题的吧。。

在这里插入图片描述即使用自己生成的基本无噪点的图片也不行:
在这里插入图片描述
而且仔细看,还有两条重复的边缘,不知道是什么意思。。。
可能还是得看opencv的源码才能揭开这个未解之谜了。
不过好事情是,我们还有其他方法来提取这些小格子,例如,先近似为矩形,利用cv2.approxPolyDP(contour, 3, True) # 趋近矩形 然后再计算是否有重叠,有重叠的就不是小格子,没有重叠的就是最小的格子。
图上我们就可以看到,格子之间不是重叠的,某些边缘可能包含了这些格子,但恰巧这些边缘都是我们不要的。
用这样的办法去除非小格子的部分。
还有许多其他办法,我就不一一赘述了。
具体实现我做好了,由于项目相关,这个又比较简单,我就不单独写了。

在这里插入图片描述
最内层搜索代码:

import cv2


if __name__ =='__main__':
    image = cv2.imread('test2.jpg')
    img = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
    img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, -2) 
    contours,hierachy = cv2.findContours(img, cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE)
    # 查找最内层的例子:
    hierach_ids = []
    inner_class =[]
    # 一直搜索到找到没有子轮廓的结构,且注意搜索长度不能超过边界数目,且需要找到第一个,
    #    这样的边界可能有多个,因为不存在包含的关系,需要完整的搜索。
    for i in range(len(contours)):
        if(hierachy[0][i][2]==-1 and hierachy[0][i][1]==-1):
            hierach_ids.append(i)
    
   
    for hierach_id in hierach_ids:
        print(hierachy[0][hierach_id])
        inner_class.append(contours[hierach_id])
        idx = hierachy[0][hierach_id][0]
        while(hierachy[0][idx][0]!=-1):
            inner_class.append(contours[idx])
            idx=hierachy[0][idx][0]
            print(hierachy[0][idx])
    for cnt in inner_class:
        cv2.drawContours(image,[cnt],0,(122,122,0),2)
    cv2.imshow('img',image)
    cv2.waitKey(10000)
    cv2.imwrite('save.png',image)
用contours做连通域掩膜

拓展连接:用contours做连通域掩膜
2020-05-22 02-32-37屏幕截图
在这里插入图片描述

findContour的规律:

规律

只有将内部轮廓全部找出才继续进行同级轮廓查找

结论
1. 不同层级的轮廓顺序从里到外
2. 同级轮廓根据轮廓最上像素点Y坐标从大到小排序(图片的从下到上),若存在Y坐标相同情况下根据X坐标从大到小排序(图片的从右到左)
3. 若一个轮廓内有子轮廓,回先查找该轮廓的所有子轮廓后才会继续同级轮廓的查找。(有点像深搜)
4. RETR_TREE是分层的所以同层级的下一个轮廓(ds[0]),同层级的上一个轮廓(ds[1])需要注意是否为同级轮廓
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐