【参考】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做连通域掩膜
findContour的规律:
规律
只有将内部轮廓全部找出才继续进行同级轮廓查找
结论
1. 不同层级的轮廓顺序从里到外
2. 同级轮廓根据轮廓最上像素点Y坐标从大到小排序(图片的从下到上),若存在Y坐标相同情况下根据X坐标从大到小排序(图片的从右到左)
3. 若一个轮廓内有子轮廓,回先查找该轮廓的所有子轮廓后才会继续同级轮廓的查找。(有点像深搜)
4. RETR_TREE是分层的所以同层级的下一个轮廓(ds[0]),同层级的上一个轮廓(ds[1])需要注意是否为同级轮廓