一、前言
在上一文中,绘制多边形最小外接矩形中用到了fillContours() 函数,它可以在二值图像中查找图像轮廓,本文结合fillContours和drawContours函数,讲解一下轮廓查找与绘制的过程。
首先明确一下图像边缘并不代表图像轮廓,图像轮廓也不能全部表示边缘信息,他们之间的区别与联系如下:
区别:
- 边缘检测是通过一些方法来检测图像中灰度变化(明暗变化)较大的像素点,偏向于像素点的差异,而没有将图像边缘作为一个整体来看待,在对有噪声的图像进行边缘检测过程中很容易就把过亮或暗的噪声当做边缘特征一起输出了,这也是为什么边缘检测之前要进行平滑去噪的原因。
- 轮廓检测指检测图像的边界,它是把图像的轮廓特征当做整体来对待,轮廓像素点共同构成了整个图像的轮廓。
联系:
- 图像的边缘和轮廓都是图像的底层特征。
- 通常,我们先对原始图像边缘检测,再提取有用的边缘像素点,组合得到图像的轮廓信息。
二、轮廓检测与绘制函数解析
OpenCV中,使用findContours函数从二值图像中查找轮廓,其函数原型为:
void findContours(InputOutputArray image, //原始图像(8bit单通道图像)
OutputArrayOfArrays contours, //轮廓点集
OutputArray hierarchy, //可选输出信息,包含图像的拓扑信息。
int mode, //轮廓检索模式
int method, //轮廓近似方法
Point offset=Point()//每个轮廓点的可选偏移量,默认为Point()
)
//重载findContours形式:
void findContours(InputOutputArray image, //原始图像(8bit单通道图像)
OutputArrayOfArrays contours, //轮廓点集
int mode, //轮廓检索模式
int method, //轮廓近似方法
Point offset=Point() //每个轮廓点的可选偏移量,默认为Point()
)
【注】fillContours()函数的两种形式类似,只有可选参数hierarchy的差异,其包含了每一个轮廓点的拓扑信息。每个轮廓点contours[i]对应四个hierarchy元素hierarchy[i][0]-hierarchy[i][3],分别表示该轮廓点的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。输出轮廓contours是OutputArrayOfArrays类型,保存了每个轮廓点信息,用Point类型的vector表示。
轮廓检索模式mode,可选如下:
轮廓近似方法method,可选如下:
fillContours检测图像的轮廓后,通常会用drawContours函数将检测到的轮廓绘制出来,在OpenCV中,使用drawContours函数绘制轮廓,其函数原型为:
void drawContours(InputOutputArray image, //绘制轮廓所在图像
InputArrayOfArrays contours, //输入轮廓点集
int contourIdx, //轮廓绘制的标识符,负数表示绘制所有轮廓
const Scalar &color, //轮廓的颜色
int thickness = 1, //轮廓线宽
int lineType = 8, //线性
InputArray hierarchy = noArray(), //层次结构,可选
int maxLevel = 2147483647, //绘制轮廓的最大等级,默认为INT_MAX
Point offset = cv::Point() //可选的轮廓偏移参数,默认为Point() )
三、fillContours+drawContours基本用法
加载一幅RGB图像,灰度化后用Otsu求全局阈值,得到二值图像,对二值图像查找轮廓点,接着绘制所得到的的轮廓点得到原始图像的轮廓,显示。代码如下
#include<opencv2/opencv.hpp>
#include<vector>
using namespace cv;
using namespace std;
int main()
{
Mat src=imread("C:/Users/Administrator/Desktop/beauty.jpg");
if(!src.data){
printf("Load failture...");
return false;
}
Mat gray;
cvtColor(src,gray,CV_RGB2GRAY);
Size dsize=Size(600,400);
resize(gray,gray,dsize,0,0,INTER_LINEAR); //尺寸缩放
threshold(gray,gray,0,255,THRESH_OTSU); //Otsu二值化
//寻找并绘制轮廓
vector<vector<Point>> contours;
findContours(gray,contours,CV_RETR_EXTERNAL,CHAIN_APPROX_SIMPLE);
Mat contourImg(gray.size(),CV_8U,Scalar::all(255));
drawContours(contourImg,contours,-1,Scalar(0),4); //contours=-1表示:绘制所有轮廓
imshow("contours",contourImg);
imshow("gray",gray);
waitKey();
return 0;
}
由于全局阈值化后人物的轮廓已经被破坏了,所以drawContours得到的不是一个完全的包围轮廓,效果如下:
四、摄像头采集图像+边缘检测
下面可以做一个有趣的小demo,从摄像头采集图像,逐帧处理成灰度图,再经过边缘检测得到二值图,通过fillContours+drawContours绘制轮廓,代码如下:
#include<opencv2/opencv.hpp>
#include<vector>
#define WIN_NAME "show_contours"
using namespace cv;
using namespace std;
int main()
{
int key;
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
VideoCapture capture(0); //摄像头采集图像
while(1){
Mat frame;
capture>>frame; //逐帧读入图像
cvtColor(frame,frame,CV_RGB2GRAY);
blur(frame,frame,Size(3,3)); //使用3×3卷积核平滑降噪
Mat canny_frame;
Canny(frame,canny_frame,80,160,3); //边缘检测
findContours(canny_frame,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point(0,0));
Mat drawing=Mat::zeros(canny_frame.size(),CV_8UC3);
for(int i=0;i<contours.size();i++){
drawContours(drawing,contours,i,Scalar(255,0,0),2,8,hierarchy);
}
imshow(WIN_NAME,drawing); //实时显示
key=waitKey(30); //30ms更新
if(key == 27){
break; //ESC键退出
}
}
return 0;
}
效果如下:
此外,还可以通过threshold()、adaptivethreshold()、compare()、soble()等方法得到二值图像,作为fillContours的原始检测图像,利用fillContours+drawContours进行轮廓绘制。