关于OpenCV的轮廓检测函数findContours()各参数的大概意义,已在博文 https://blog.csdn.net/wenhao_ir/article/details/51798533中进行了介绍。
这篇博文以实际例子来进一步认识函数findContours()各参数的意义,这篇博文实际上是上篇博文的延伸和深入。
我们用下边这张简单的图像为例进行测试和说明:
上面这张图片的名字为“ring.bmp”百度网盘下载链接:https://pan.baidu.com/s/1SPxGouOli-XbBYfr-IJ9nw?pwd=dwch
我们把上面这张图的轮廓利用轮廓检测函数findContours()检测出来,代码如下:
# 博主微信/QQ 2487872782
# 有问题可以联系博主交流
# 有图像处理需求也请联系博主
# 图像处理技术交流QQ群 271891601
# !/usr/bin/env python
# -*- coding: utf-8 -*-
# OpenCV的版本为4.1
import numpy as np
import cv2 as cv
import sys
image = cv.imread('F:/material/images/2022/2022-06/ring.bmp')
# image = cv.imread('F:/material/images/P0044-hand-02.jpg')
if image is None:
print('Error: Could not load image')
sys.exit()
# cv.imshow('Source Image', image)
# 原图像转化为灰度图
img_gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# cv.imshow('img_gray', img_gray)
# 灰度图进行二值化处理,并不是函数findContours要求输入图像为二值图像,
# 而是函数findContours在进行轮廓提取前会把原图中的非0值全部当成1处理。
_, img_B = cv.threshold(img_gray, 71, 255, cv.THRESH_BINARY)
# cv.imshow('img_B', img_B)
cnts, harch = cv.findContours(img_B, mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_SIMPLE)
运行结果如下:
接下来开始分析运行结果:
从上面的运行结果可以看出,检测出了两个轮廓,存放在列表型对象cnts中的,如下图所示:
从上图我们可以看出,每个轮廓由一个三维的ndarray数据对象存储,假设某个轮廓有N个点,则由N个一行二列的二维矩阵组成这个三维的ndarray数据对象,每个一行二列的二维矩阵中存储了各个轮廓点的坐标。
每个轮廓在列表中的索引值就是轮廓的索引值,即轮廓被依次编号为0,1,2…
上面的示例代码便是轮廓检测模式为RETR_TREE时的结果,RETR_TREE模式表示返回所有的轮廓,并且建立完整的拓扑结构。
此种情况下的轮廓检测结果上面已经分析说明了,这里就不再赘述了。
我们来看下此时的轮廓拓扑结构(hiararchy)是一种怎样的数据结构。
从上面的截图中可以看出,轮廓拓扑结构(hiararchy)以ndarray的数据对象形式存储,每个ndarray数据对象是三维的。为了明白三个维度分别代表什么,我们对下面这幅图再运行一次上面的程序,
上面这幅图的名字为P0044-hand-02.jpg,百度网盘下载链接:https://pan.baidu.com/s/1uAClJ3xklpSGE1iA-7py5g?pwd=h237
结果如下:
将两次运行结果一对比,我们发现拓扑结构hiararchy-ndarray对象的第一个维度的尺寸应该是永远为1的,第二个维度的尺寸应该是代表轮廓数,第三个维度的尺寸应该永远为4,4个值依次代表某个轮廓的前一个轮廓的索引值(编号)、后一个轮廓的索引值(编号)、子轮廓(嵌套轮廓)的索引值(编号)、父轮廓的索引值(编号)。
再回到对图片ring.bmp的轮廓检测结果,我们来看下其拓扑结构的具体数值:
上面的截图中每一行代表一个轮廓的拓扑结构,
先来看第0行:
第0行的第0个值,代表第0个轮廓的前一个轮廓的索引值,其值为-1,说明其前一个轮廓不存在;
第0行的第1个值,代表第0个轮廓的后一个轮廓的索引值,其值为-1,说明其后一个轮廓不存在;
第0行的第2个值,代表第0个轮廓的子轮廓的索引值,其值为1,说明索引值为1的轮廓为其子轮廓;
第0行的第3个值,代表第0个轮廓的父轮廓的索引值,其值为-1,说明其父轮廓不存在。
再来看第1行:
第1行的第0个值,代表第1个轮廓的前一个轮廓的索引值,其值为-1,说明其前一个轮廓不存在;
第1行的第1个值,代表第1个轮廓的后一个轮廓的索引值,其值为-1,说明其后一个轮廓不存在;
第1行的第2个值,代表第1个轮廓的子轮廓的索引值,其值为-1,说明其子轮廓不存在;
第1行的第3个值,代表第1个轮廓的父轮廓的索引值,其值为1,说明索引值为1的轮廓为其父轮廓;
由上面的叙述我们可以看出,从拓扑结构上来看,一个轮廓虽然说可以有多个父轮廓,也可以有多个子轮廓,但在hiararchy数据结构中,只能为其填写一个父轮廓和一个子轮廓,通常就填写索引值最邻近它的子轮廓或父轮廓。比如下面的例子:
上面是某张图(实际上就是本文后面02-03点提到的小白兔手绘图片img_300_320.jpg)的轮廓拓扑结构,我们可以看到,索引值为12的轮廓是索引值为13、15、16、17、18、19、20、21、22、24的轮廓的父轮廓,但是在hiararchy数据结构中,只把轮廓索引值为13的轮廓作为了其子轮廓,但事实上它有10个子轮廓。
RETR_EXTERNAL轮廓检测模式代表只检测最外层轮廓,对所有轮廓设置hierarchy[i][2]= hierarchy[i][3]=-1,即没有子轮廓与父轮廓。
我们设置成这种模式再对图片ring.bmp进行轮廓检测。
时只需要把下面这句语句:
cnts, harch = cv.findContours(img_B, mode=cv.RETR_TREE, method=cv.CHAIN_APPROX_SIMPLE)
修改为:
cnts, harch = cv.findContours(img_B, mode=cv.RETR_EXTERNAL, method=cv.CHAIN_APPROX_SIMPLE)
即可修改为RETR_EXTERNAL轮廓检测模式。
运行结果如下:
从上面的截图中我们可以看出,只检测到一个轮廓了,这个轮廓的子轮廓被忽略了。
我们再看下此时的拓扑结构信息:
这个拓扑结构信息是符号原图、轮廓检测模式及轮廓检测结果的。
CV_RETR_CCOMP轮廓检测模式提取所有轮廓,并且将其组织为双层结构。顶层(the top levell)为连通域的外围边界,次层(the second level)为孔(hole)的内层边界,如果孔(hole)中还有其它轮廓,那么又依次被组织为顶层和次层。
只看上面这段话,相信诸君还是对这种轮廓检测模式具体是怎么操作的不是很清晰,没关系,我们看一个实际例子就清楚明白了。
将轮廓检测语句修改为:
cnts, harch = cv.findContours(img_B, mode=cv.CV_RETR_CCOMP, method=cv.CHAIN_APPROX_SIMPLE)
运行结果如下:
可见,与RETR_TREE的结果没有区别。难道RETR_CCOMP模式与RETR_TREE模式没有区别?不是的哈,其实是有区别的。
之所以上面的结果没有区别,是因为用的图像太简单了,我们把图像换成下面这幅图像就能看出差别了。
上面这幅小白兔手绘图片的名字为“img_300_320.jpg”,
其RETR_TREE模式的轮廓检测拓扑结果如下:
其RETR_CCOMP模式的轮廓检测拓扑结果如下:
这两个检测结果就有明显区别了。我们看到在RETR_CCOMP模式下,索引值为12至22的轮廓都是索引值为11的子轮廓,因为只组织为两层结构,所以索引值为12至22的轮廓再也没有成为别的轮廓的父轮廓。
而在RETR_TREE模式下,索引值为13的轮廓是14的父轮廓,但它同时又是12的子轮廓,所以它既有父亲的身份也有儿子的身份。
至此,我们可以总结一下:在RETR_TREE模式下,同一个轮廓既可以作为别的轮廓的父轮廓,也可作为别的轮廓的子轮廓;在RETR_CCOMP模式下,由于只将轮廓组织为两层,所以同一个轮廓只要作了某个轮廓的父轮廓或子轮廓,那么就再也不能做别的轮廓的子轮廓或父轮廓了,再说清楚点,假如一个轮廓作了某个轮廓的父轮廓,那么它就再不能做别的轮廓的子轮廓;假如一个轮廓作了某个轮廓的子轮廓,那么它就再不能做别的轮廓的父轮廓。
CV_RETR_LIST轮廓检测模式下,返回所有的轮廓,但是不建立轮廓的拓扑关系,我们还是以图片小白兔手绘图片“img_300_320.jpg”为例来看下这种情况下的运行结果。
cnts, harch = cv.findContours(img_B, mode=cv.RETR_LIST, method=cv.CHAIN_APPROX_SIMPLE)
运行结果如下:
从上面的运行结果我们可以看出,所有的子轮廓索引值和父轮廓索引值都为-1.说明的确没有建立轮廓的拓扑结构关系。
参数method表示算法采用的轮廓近似方法,有以下可选参数:
CV_CHAIN_APPROX_NONE:存储所有的轮廓点。这种方法下,两个连续的轮廓点,要么是水平相邻的,要么是垂直相邻的, 要么是对角相邻的,即满足max(abs(x1-x2),abs(y2-y1))==1.
CV_CHAIN_APPROX_SIMPLE: 压缩水平方向、垂直方向和对角线方向的中间点,只保留某个方向的终点坐标,例如一个矩形轮廓只需4个点来保持轮廓信息。
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:使用The-Chinl链逼近算法中的一个。
这个参数大家看了上面的解释叙述应该就比较清楚了,显然CV_CHAIN_APPROX_SIMPLE应该是比CV_CHAIN_APPROX_NONE检测到的轮廓的点要少的,我们看下是这不是这样。
我们以图片ring.bmp为例,CV_CHAIN_APPROX_SIMPLE下的检测结果如下:
cnts, harch = cv.findContours(img_B, mode=cv.RETR_LIST, method=cv.CHAIN_APPROX_SIMPLE)
从上图的结果我们可以看到,CV_CHAIN_APPROX_SIMPLE下检测到的两个轮廓点的个数分别为156个和166个。
CV_CHAIN_APPROX_NONE下的检测结果如下:
cnts, harch = cv.findContours(img_B, mode=cv.RETR_LIST, method=cv.CHAIN_APPROX_NONE)
从上图的结果我们可以看到,CV_CHAIN_APPROX_NONE下检测到的两个轮廓点的个数分别为322个和331个。
至此,我们便对轮廓检测函数findContours()的四个最难理解的参数contours、hiararchy、mode、method有了深入细致的认识,在图像处理的任务中使用轮廓检测函数findContours()时便会更加有的放矢了。