OpenCV轮廓

轮廓提取任务是许多图像分析、识别的必要步骤,前置工作是将灰度图进行二值化,然后基于二值化图进行轮廓提取,本文记录了OpenCV一系列轮廓相关的分析手段,包括基本轮廓提取、绘制、外接多边形/椭圆/圆、多轮廓的交、并、差填充等。

轮廓提取与绘制

cv提供了强大的基本轮廓识别绘制功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void cv::findContours(InputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset = Point())

参数:
image:输入的二值图像

contours:轮廓信息,一般为点的二维vector,第一维是数组,代表第i个轮廓的点集,第二维是某个轮廓的第j个点

hierarchy:轮廓层次信息,一般是关于Vec4i的vector,第一维代表第i个轮廓;
第二维0、1、2、3分别代表当前轮廓的同级Next轮廓编号,同级Previous轮廓编号,第一个子轮廓编号、父轮廓的编号。

mode:轮廓检索模式,即:
- RETR_TREE:从顶级轮廓到子轮廓按完整层次整理;
- RETR_EXTERNAL:只检验外部轮廓;
- RETR_LIST:不建立等级关系;
- RETR_CCOMP:仅二级层次,如果具有第三级轮廓,该轮廓会作为顶层轮廓。

method:轮廓近似方法,与性能
- CHAIN_APPROX_SIMPLE:仅保留轮廓中直线端点,性能高;
- CHAIN_APPROX_NONE:完整保留轮廓每一个点,性能略低但准确。
- CHAIN_APPROX_TC89_L1/CHAIN_APPROX_TC89_KCOS:基于L1距离、Cos距离的Teh-Chin近似算法;

offset:偏移量,两个方向会添加对应的坐标值作为偏移量;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void cv::drawContours(InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point())

参数:
image:输入底图,会在此图上进行绘制;
contours:输入轮廓数组;
contourIdx:第几个轮廓,负数代表所有轮廓;
color:BGR颜色;
thickness:轮廓画笔粗度;
lineType:画线类型,LINE_4/LINE_8/LINE_AA,四方向、八方向、抗锯齿线;
hierarchy:层次列表;
maxLevel:最深层次数,若maxLevel = 0,仅绘制contourIdx轮廓;
- 若maxLevel = 1,则还会绘制其子轮廓,以此类推;负数或INT_MAX会绘制contourIdx下所有层次的轮廓;
offset:偏移量,同上;

点集格式

这里说明一下,在cv中除了常用的vector<Point>可以代表点的数组,Mat类型也可以用于表示点数组,例如使用Mat绘制一个三角形:

1
2
3
4
5
6
7
8
9
10
11
Mat_<int> triangle = (Mat_<int>(3,2)<<
0,0,
500,10,
10,500
);

for(int i=0; i<3; i++){
line(rawPicColor,Point(triangle.at<int>(i,0),triangle.at<int>(i,1)),\
Point(triangle.at<int>((i+1)%3,0),triangle.at<int>((i+1)%3,1)),Scalar(0,255,0),2,LINE_AA);
}
imshow("Triangle",rawPicColor);
Mat三角形

可见Mat的索引是比较麻烦的,,对于闭合图形还涉及取余来解析点,因此不如Point数组常用,下面基本基于Point数组描述,只需要知道各函数也支持Mat作为点集类型

轮廓特征

轮廓面积、长度

cv::contourArea计算某个轮廓围成的面积第二个参数默认为false,代表无方向;若为true,轮廓逆时针为正面积,顺时针为负面积

cv::arcLength计算某个轮廓的长度,第二个参数orienttrue代表封闭轮廓false代表开放轮廓

1
2
3
4
5
6
7
8
9
10
11
12
cv::contourArea(vector<Point>contours[0], bool orient);  //轮廓面积
cout<<cv::arcLength(vector<Point>contours[i], bool orient); //轮廓长度

example:
for(int i=0; i<contours.size(); i++){ //计算子轮廓面积
if(hierarchy[i][3]==0){
drawContours(rawPicColor, contours , i, Scalar(0,0,255), 4 ,LINE_AA, hierarchy, 0);
cout<<cv::contourArea(contours[i],true)<<"\t";
cout<<cv::arcLength(contours[i],true)<<endl;
}
}
imshow("area",rawPicColor);

近似多边形approxPolyDP

cv::approxPolyDP使用多边形去拟合轮廓的形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cv::approxPolyDP(vector<Point> contours[1], vector<Point> appContours, double epsilon, bool orient);
- contours:输入轮廓;
- appContours:输出拟合轮廓;
- epsilon:精度,越小精度越高、点越多,越大精度越差,常和轮廓长度合用调节;
- orient:为true闭合;

example:
vector<Point> appContours;
double epsilon = 0.02 * arcLength(contours[1], true); //精度
cv::approxPolyDP(contours[1],appContours,epsilon,true);

//注意draw接受的是二维
drawContours(rawPicColor,vector<vector<Point>>{appContours},0,Scalar(0,255,0), 2, LINE_AA);
imshow("approx",rawPicColor);
近似多边形

近似直矩形boundingRect

cv::boundingRect返回的近似轮廓是横平竖直的矩形,若需要形态拟合无需正方向应该是使用旋转矩形

1
2
3
Rect bdRect = boundingRect(contours[1]);
rectangle(rawPicColor,bdRect,Scalar(0,255,0),2,LINE_AA);
imshow("bdRect",rawPicColor);
近似直矩形

近似旋转矩形minAreaRect

cv中还没有合适的函数可以直接绘制旋转矩形,需要提取端点再使用line函数绘制

1
2
3
4
5
6
7
8
9
RotatedRect rtRect = minAreaRect(contours[1]); //输出旋转矩形

//绘制旋转矩形:
Point2f rtPoint[4]; //提取点要求是Point2f
rtRect.points(rtPoint);
for(int i=0; i<4; i++){ //绘制
line(rawPicColor,rtPoint[i],rtPoint[(i+1)%4],Scalar(0,255,0),2,LINE_AA);
}
imshow("rtRect",rawPicColor);
近似旋转矩形

近似凸包convexHull

即返回给定轮廓点的凸多边形近似,如果轮廓本身就是全凸的,效果实际上和近似多边形类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void convexHull(
vector<Point> points, // 输入点集
vector<Point> hull, // 输出的凸包点集
bool clockwise = false, // 凸包点集的顺序(顺时针或逆时针,与面积计算等有关)
bool returnPoints = true // 是否返回点的索引
);

//example:
vector<Point>cvhContours;
convexHull(contours[1],cvhContours,false,true);

//凸包绘制
for(int i=0; i<cvhContours.size(); i++){
line(rawPicColor,cvhContours[i], cvhContours[(i+1)%cvhContours.size()], Scalar(0,255,0),2,LINE_AA);
}
imshow("cvhContours",rawPicColor);
近似凸多边形

最小外接圆minEnclosingCircle

cv::minEnclosingCircle接受轮廓点集contours[1],返回圆心Point2f center和半径float radius

1
2
3
4
5
6
7
8
minEnclosingCircle(contours[1],center,radius);

//example:
Point2f center;
float radius;
minEnclosingCircle(contours[1],center,radius);
circle(rawPicColor,center,radius,Scalar(0,255,0),2,LINE_AA);
imshow("ccircle",rawPicColor);
近似外接圆

最小外接椭圆fitEllipse

这里的旋转矩阵minAreaRect得到的旋转矩阵并非完全重合,其底层算法应该存在一定差异。

1
2
3
RotatedRect rtRect = fitEllipse(contours[2]); //返回旋转矩阵
ellipse(rawPicColor,rtRect, Scalar(0,255,0),2,LINE_AA);
imshow("ellipse",rawPicColor);
最小外接椭圆

最小外接三角形minEnclosingTriangle

1
2
3
4
5
6
7
8
vector<Point> triPoint;
minEnclosingTriangle(contours[1],triPoint);

//绘制三角形
for(int i=0; i<3; i++){
line(rawPicColor,triPoint[i],triPoint[(i+1)%3],Scalar(0,255,0),2,LINE_AA);
}
imshow("Triangle",rawPicColor);
最小外接三角形

轮廓填充fillPoly

对于圆形、椭圆形、矩形有自己的绘图函数,使用-1指定线宽代表填充,对于点集绘制的多边形,可以使用cv::fillPoly绘制任意形状的多边形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cv::fillPoly(
InputOutputArray img, // 输入输出图像
InputArrayOfArrays pts, // 多边形点集
const Scalar& color, // 填充颜色
int lineType = LINE_8, // 线型
int shift = 0, // 坐标精度(小数的位数)
Point offset = Point() // 偏移量
);


example:
Mat botPic(500,500,CV_8UC3,Scalar(255,255,255)); //底图
vector<Point> vp{{10,20},{110,320},{220,110}};

fillPoly(botPic,vector<vector<Point>>{vp},Scalar(0,255,0),LINE_AA);
imshow("fill",botPic);
填充三角形

图形交并差fillPoly

对不同对象求交、并、差等运算是一个重要问题,图形学引入了各种各样的库来处理这些问题,这里fillPoly也可以用于生成掩图,实现简单的图形运算。

注意新建点集时,必须按照图形顺时针/逆时针的方式指定轮廓点,否则可能出现意外的连线错误,如绘制矩形可能变成两个三角形,vector<Point> pic1{{10,20},{10,320},{220,20},{220,320}}: 非严格顺逆描点

当fillPoly同时接受一个列表的两个轮廓对象时,实际上填充的是它们的差集

1
2
3
4
5
6
7
Mat botPic(500,500,CV_8UC3,Scalar(255,255,255)); //底图

vector<Point> pic1{{10,20},{10,320},{220,320},{220,20},};
vector<Point> pic2{{150,200},{300,200},{300,420},{150,420}};
fillPoly(botPic,vector<vector<Point>>{pic1,pic2},Scalar(255,0,0),LINE_AA);

imshow("fill",botPic);
差集

求并也很简单,只需要将他们各自填充即可:

1
2
fillPoly(botPic,vector<vector<Point>>{pic1},Scalar(255,0,0),LINE_AA);
fillPoly(botPic,vector<vector<Point>>{pic2},Scalar(255,0,0),LINE_AA);
并集

交集可以使用掩图实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Mat botPic(500,500,CV_8UC3,Scalar(255,255,255));
vector<Point> pic1{{10,20},{10,320},{220,320},{220,20},};
vector<Point> pic2{{150,200},{300,200},{300,420},{150,420}};

Mat pic1_mask = Mat::zeros(botPic.size(),CV_8UC1);
Mat pic2_mask = Mat::zeros(botPic.size(),CV_8UC1);

fillPoly(pic1_mask,pic1,Scalar(255),LINE_AA);
fillPoly(pic2_mask,pic2,Scalar(255),LINE_AA);

Mat intersection_mask;
bitwise_and(pic1_mask,pic2_mask,intersection_mask);
botPic.setTo(Scalar(0,0,255),pic1_mask); //非交use red
botPic.setTo(Scalar(0,0,255),pic2_mask);
botPic.setTo(Scalar(255,0,0),intersection_mask); //intersection_mask为白,设置为对应颜色

imshow("intersection",botPic);
交集

参考链接:

  1. cv2.fillConvexPoly()与cv2.fillPoly()填充多边形

  2. opencv——图像轮廓

  3. 第12章:图像轮廓