发布时间:2023-01-16 14:30
最近在用vs和opencv库在做图像处理的项目,关于轮廓识别部分,我查阅了一些资料, 现结合自己的理解整理出来,希望能对你有用。
1.contours概述
在利用openCV对图像进行处理时,我们可能会需要提取出图片上物体的轮廓,进而判断其形状、获取轮廓上的点等等。为了便捷地获得图像轮廓,我们可以使用opencv库中的Contours相关API接口。
Contours可以简单地被理解为一条由其上全部连续点组成的曲线,这些点通常颜色相同、像素值接近。一张图片上可能有多条轮廓,而每条轮廓由多个连续的像素点组成。Contours主要用于形状分析、物体检测识别和轮廓周长面积计算等场合[1]。
2.常用函数
识别轮廓时,opencv中最常用的两个API就是 findContours() 和 drawContours() ,下面对这两者进行介绍。
findContours()
该函数用于提取图像中的轮廓,并将轮廓信息存储起来。
findContours( InputOutputArray image,//输入图像,一般为二值图
OutputArrayOfArrays contours,//用于保存轮廓信息的数组
OutputArray hierarchy,//轮廓层级关系
int mode,//轮廓的检索模式
int method,//定义轮廓的近似方法
Point offset=Point());//偏移量
以上六个参数的详细解释如下。
(1)为了提高轮廓识别准确度,输入的image一般为二值图像,即经过灰度化,滤波,canny、sobel或laplace边缘检测后的图像。
(2)我们需要定义一个容量可变的容器,来保存图像的轮廓信息。那么,第二个参数contours便来源于此。contours可以用 std::vector
语句进行定义。在此语句中,std::vector
是C++
中的一个类,表示容量可变的数组容器。该数组由多个contours
组成,数组中的每一个元素即为一个contours
,每一个contours
由多个Point
组成。那么,为什么要求数组的大小可以改变呢?这是因为在检测轮廓之前,计算机并不知道图像上的独立轮廓到底有多少个,如果提前设定数组大小,可能会出现数据溢出的情况。为了避免数据丢失,用std::vector
定义容器,随着更多的轮廓被检测出,数组容量也不断被扩大。这里可能不太容易理解,但下面的代码一看你就懂了。
(3)hierarchy表示轮廓的层级关系。用语句vector
来定义,一个hierarchy包含4个整型数据,分别表示:后一个轮廓的序号、前一个轮廓的序号、子轮廓的序号、父轮廓的序号,用于体现图像不同轮廓之间的层级关系[2]。hierarchy参数项与下面的mode参数相关联,选择不同的检索模式,hierarchy的值不同。
(4)mode为轮廓的检索模式,有4个选项,不同选项最终生成出来的轮廓样式有所不同[3]。你可以根据自己的需求进行选择,如下表所示。
模式 | 特点 |
---|---|
CV_RETR_LIST | 检测所有的轮廓,包括内围、外围轮廓,轮廓之间不建立层级关系 |
CV_RETR_CCOMP | 检测所有轮廓,轮廓之间只建立两个层级关系,不存在父轮廓或子轮廓关系 |
CV_RETR_TREE | 检测所有轮廓,轮廓之间建立完整的层级关系,全部四个层级关系 |
CV_RETR_EXTERNAL | 只检测最外围轮廓 ,忽略内围轮廓,轮廓之间不存在层级关系 |
(5)method,用来定义轮廓近似方法。根据前面的内容,我们知道轮廓是由物体外形的边界点组成的曲线,包含着边界上的点。但是,contours中是否一定存储着所有边界点呢?答案是不一定,这由轮廓近似方法method决定。
如果设置选项为CV_CHAIN_APPROX_NONE
,所有边界点都被保存。然而,它们并不一定都能派上用场。比如说,识别到一条直线轮廓,为了显示它,我们只须存储它的两个端点就够了,可以舍弃掉其余的点。要做到这点,将选项改为CV_CHAIN_APPROX_SIMPLE
即可,舍弃冗余的点,从而节省存储空间。结合下面的图片进行理解,左侧矩形轮廓通过CV_CHAIN_APPROX_NONE
得到,共734个点;右侧矩形轮廓通过CV_CHAIN_APPROX_SIMPLE
得到,只有4个点。可见,后者节省了很多空间。
不同的模式所提取出来的轮廓点有所不同,如下表所示。
模式 | 特性 |
---|---|
CV_CHAIN_APPROX_NONE | 保存物体边界上所有连续的轮廓点 |
CV_CHAIN_APPROX_SIMPLE | 仅保存轮廓的拐点信息,拐点与拐点之间直线段上的信息点不予保留 |
CV_CHAIN_APPROX_TC89_L1 或 CV_CHAIN_APPROX_TC89_KCOS | 使用teh-Chinl chain 近似法中的一个 |
(6)Point(a,b)可以用作图像中轮廓位置的修改。例如Point(20,30)表示每个轮廓点向右偏移20个像素点、向下偏移30个像素点。(默认右为正、下为正)
drawContours()
该函数将提取出来的轮廓信息绘制出来,显示在图片上。
void drawContours(InputOutputArray image, //输出的目标图像
InputArrayOfArrays contours, //输入前面findContours提取的轮廓数组
int contourIdx, //数组contours中的元素序号,0、1、2...
const Scalar& color, //轮廓的颜色
int thickness=1, //轮廓线宽度
int lineType=8, //轮廓线型
InputArray hierarchy=noArray(), //轮廓层级关系
int maxLevel=INT_MAX, //轮廓绘制最高层次
Point offset=Point() );//偏移量
以上为drawContours函数及其参数的解释,其中大部分很容易理解或者在上文中已解释过。这里详细介绍第8个参数int maxLevel=INT_MAX
,此参数表示绘制轮廓线的最大级别。如果为0,则只绘制指定的轮廓。如果值为1,函数将绘制该轮廓线和所有内嵌轮廓线。如果值为2,则该函数绘制轮廓线、所有内嵌轮廓线,和比内嵌轮廓线再低一级的内嵌轮廓线,以此类推。仅当存在可用的层次结构时才考虑此参数,就是说hierarchy有效时这个参数才有用。
3.代码实现
下面结合实例对图像轮廓识别地过程进行解释。
#include
#include
#include
using namespace cv;
using namespace std;
Mat src, src_gray, src_blur, src_canny, src_cont;
int main(int argc, char** argv){
src = imread("C:\\Users\\ASUS\\Pictures\\beauties\\lindaiyu.jpg");
if(!src.data){
printf("can not open the mark_image!!\n");
system("pause");
return -1;
}
namedWindow("tupian", CV_WINDOW_AUTOSIZE);
imshow("tupian", src);
cvtColor(src, src_gray, CV_BGR2GRAY); //将图像灰度化
blur(src_gray, src_blur, Size(3, 3)); //对图像进行均值滤波
Canny(src_blur, src_canny, 40, 120);
src_cont = Mat::zeros(src.size(), CV_8UC3); //创建一个与src尺寸相同的纯色图片
vector<vector<Point>> contours; //定义一个存储轮廓数组的容器
vector<Vec4i> hierarchy; // 每一个hierarchy由4个整形变量组成
findContours(src_canny, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //寻找轮廓
RNG rng(0); //定义随机数,初始值为0
for(int i = 0; i < contours.size(); i++){
Scalar color = Scalar(rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255)); //定义一个随机颜色
drawContours(src_cont, contours, i, color, 1, 8, hierarchy, 0, Point(0,0)); //绘制第i个轮廓
}
namedWindow("contours", CV_WINDOW_AUTOSIZE);
imshow("contours", src_cont);
waitKey(0);
return 0;
}
首先,对图像进行灰度化处理,再调用blur()函数去除噪声,然后使用canny算子获得图像边缘,之后找到图像的每一个轮廓,最后依次绘制出来。至此,轮廓识别的整个过程便搞定了。原图与轮廓图的对比如下所示。
4.查找指定轮廓上的点
识别出轮廓并在图像中显示出来之后,我们或许还有进一步的需求,例如判断轮廓形状、获得每条轮廓上像素点的信息、计算轮廓周长面积等,下面我们介绍查找轮廓点的方法。
在上面的程序中加入以下代码即可。
for(int i = 0; i < contours.size(); i++){
for(int j = 0; j< contours[i].size(); j++){
cout<<"第 "<<i<<" 个轮廓上第 "<<j<<" 个点的坐标为:";
cout<<contours[i][j].x<<", "<<contours[i][j].y<<endl;
}
}
contours.size()
返回整张图片上轮廓的总数目;contours[i].size()
返回第 i 个轮廓上像素点的总数;contours[i][j]
表示第 i 个轮廓上的第 j 个像素点,contours[i][j].x
和contours[i][j].y
分别为该点的x和y坐标值。
输出结果如下图所示。
至此,本文对Contours概念及相关函数做了简要的介绍,并读取出轮廓上所有的像素点,大家如有疑问或其它见解欢迎在评论区讨论。
参考引用
[1].opencv官网上关于contours的介绍(https://docs.opencv.org/3.3.1/d4/d73/tutorial_py_contours_begin.html)
[2].轮廓层级的进一步解释(https://blog.csdn.net/qq_33810188/article/details/81285867)
[3].findContours函数参数详解(https://blog.csdn.net/laobai1015/article/details/76400725)