查找图像的轮廓在图像处理及应用中扮演着重要的角色。openCV 中的轮廓指的是由一系列点组成的点的集合,不同的轮廓可以有不同的点集。openCV中,轮廓是由STL风格的vector<>模板对象表示的,其中vector中的每个元素都编码了曲线上,下一点的位置信息。openCV 中查找图像轮廓的函数是 findContours(),并通过 drawContours()将查找到的轮廓绘制到图像上。
openCV 中查找图像轮廓的函数被封装在
CV_EXPORTS_W void findContours( InputOutputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset=Point());
这个函数中,其中:
第一个参数:image表示输入图像,可以为灰度图,但实际使用时一般需要是二值化的图像(如经过canny算子处理后的图像,或者是threshold阈值提取后的图像等);PS:findContours后会改变输入的2值图像,所以如果不想改变该2值图像,需创建新mat来存放。(此问题只出现在OpenCV3.2以前的版本,3.2以后已经不会再改变输入的2值图像了)
第二个参数:contours表示得到的轮廓点的集合,必须提前申请内存空间。其被定义为“vector
第三个参数:hierarchy被定义为“vector hierarchy”,从定义上看,hierarchy也是一个向量,向量内每个元素保存了一个包含4个int整型的数组。向量hiararchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy向量内每一个元素的4个int型变量——hierarchy[i][0] ~hierarchy[i][3],分别表示第i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~hierarchy[i][3]的相应位被设置为默认值-1。
第四个参数:int型的mode,定义轮廓的检索模式:
取值一:CV_RETR_EXTERNAL只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
取值二:CV_RETR_LIST 检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1
取值三:CV_RETR_CCOMP 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
取值四:CV_RETR_TREE, 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
第五个参数:int型的method,定义轮廓的近似方法:
取值一:CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
取值二:CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
取值三和四:CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
第六个参数:Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且Point还可以是负值!
借用网上的图片来说明轮廓结构,根据findContours()这个函数的第四个参数 int mode 的不同选择将会产生不同的结构模型,当mode为CV_RETR_TREE 时将会产生图二所示的层级结构,最外围的轮廓为父轮廓,内部还有很多内嵌轮廓,内嵌轮廓之间有可能会出现并列现象,就如上图右下角的轮廓树所表示的那样。
openCV 中查找图像轮廓的函数被封装在
CV_EXPORTS_W void drawContours( InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness=1, int lineType=8,
InputArray hierarchy=noArray(),
int maxLevel=INT_MAX, Point offset=Point() );
这个函数中,其中:
第一个参数:image表示输入图像,可以为灰度图,但实际使用时一般需要是二值化的图像(如经过canny算子处理后的图像,或者是threshold阈值提取后的图像等);
第二个参数:contours表示得到的轮廓点的集合,由函数findContours()产生;
第三个参数:contourIdx表示指定要绘制轮廓的编号,如果是负数,则绘制所有的轮廓;
第四个参数:color为绘制轮廓所用的颜色
第五个参数:thickness,绘制轮廓的线的粗细,如果是负数,则轮廓内部被填充;
第六个参数:lineType表示绘制轮廓的线的连通性;
第七个参数:hierarchy表示层级的可选参数,只有绘制部分轮廓时才会用到,由函数findContours()产生;
第八个参数:maxLevel取值如下:
maxLevel=INT_MAX:绘制轮廓的最高级别,这个参数只有hierarchy有效的时候才有效
maxLevel=0,绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓
maxLevel=1, 绘制与输入轮廓同一等级的所有轮廓与其子节点
maxLevel=2,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点
第九个参数:Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量。
图像处理中也经常会遇到需要对输入的图像的角度进行测量的情况,OpenCV中提供了一个很好的角度测量的函数:
CV_EXPORTS_W RotatedRect minAreaRect( InputArray points );
其中的参数 InputArray points 即为 findContours( ) 函数返回的轮廓点集:OutputArrayOfArrays contours。
其具体使用方法如下:
findContours(CannyOut, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //查找轮廓
RotatedRect AngleRect = minAreaRect(contours.at(0));
Point2f vertices[4]; //存储得到的矩形轮廓的顶点坐标
AngleRect.points(vertices);
line(image, vertices[0], vertices[1], Scalar(0, 0, 255));
line(image, vertices[0], vertices[3], Scalar(0, 0, 255));
line(image, vertices[1], vertices[2], Scalar(0, 0, 255));
line(image, vertices[2], vertices[3], Scalar(0, 0, 255));
cout << "AngleRect_angle: " << AngleRect.angle << endl;
rectangle(image, rect, Scalar(255, 0, 0), 1);
其中 RotatedRect 的具体定义如下:
class CV_EXPORTS RotatedRect
{
public:
//! various constructors
RotatedRect();
RotatedRect(const Point2f& center, const Size2f& size, float angle);
RotatedRect(const CvBox2D& box);
//! returns 4 vertices of the rectangle
void points(Point2f pts[]) const;
//! returns the minimal up-right rectangle containing the rotated rectangle
Rect boundingRect() const;
//! conversion to the old-style CvBox2D structure
operator CvBox2D() const;
Point2f center; //< the rectangle mass center
Size2f size; //< width and height of the rectangle
float angle; //< the rotation angle. When the angle is 0, 90, 180, 270 etc., the rectangle becomes an up-right rectangle.
};
其中最容易让人疑惑地就是 Size2f size; float angle; 这两个参数,结合上图三与图四所示的测试图依次说明其意义:
size 指的是矩形的高和宽,但是不是我们平常理解的那样,上图三中红色椭圆区域标出来的结果说明 width 参数有可能大也有可能小,这点需要重点注意,避免以后 直接引用 width 和 height 这两个参数导致错误;
angle 指的是计算出的矩形的旋转角,其注释说明当角度为 0, 90, 180, 270 等时,角度是以矩形右上边来计算的,因此其角度只能是 [0,90) 的左闭右开区间。具体角度的计算方法及方向如上图四所示,在opencv中,坐标的原点在左上角,与x轴平行的方向为角度为0,逆时针旋转角度为负,顺时针旋转角度为正。而RotatedRect类是以矩形的哪一条边与x轴的夹角作为角度的呢?将X轴从下往上平移,碰到的第一个点记为角度的顶点,以此为基逆时针旋转X轴,X轴碰到的第一条边即为 angle 对应的另一条边,且openCV默认把这个边的边长作为width。由前面所说,angle的取值范围必然是负的,因此实际angle的取值范围为(-90,0]。
#include "stdafx.h"
#include "highgui.h"
#include "cv.h"
#include "math.h"
using namespace std;
using namespace cv;
int _tmain(int argc, _TCHAR* argv[])
{
Mat image = imread(".\\Square1.bmp", CV_LOAD_IMAGE_COLOR);
Mat GrayOut, CannyOut;
//GrayOut = cvCreateMat(image.rows, image.cols, CV_8UC1);
cvtColor(image, GrayOut, CV_BGR2GRAY);
//threshold(GrayOut, GrayOut, 70, 255, CV_THRESH_BINARY);
Canny(GrayOut, CannyOut, 50, 150,3);
vector<vector<Point> > contours, contours0;
vector<Vec4i> hierarchy;
//查找轮廓
findContours(CannyOut, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
//对得到的轮廓曲线进行直线拟合
contours0.resize(contours.size());
for (size_t k = 0; k < contours.size(); k++)
approxPolyDP(Mat(contours[k]), contours0[k], 1, true);
//计算轮廓矩
vector<Moments> mu(contours.size());
for (int i = 0; i < contours.size(); i++)
{
mu[i] = moments(contours[i], false);
}
//计算轮廓的质心
vector<Point2f> mc(contours.size());
for (int i = 0; i < contours.size(); i++)
{
mc[i] = Point2d(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
}
//画出最外围轮廓
drawContours(GrayOut, contours0, -1, cvScalar(0,255,255), 3, 8, hierarchy);
circle(image, Point(mc[0].x, mc[0].y), 2, Scalar(0, 255, 255), 1);
//画出外围轮廓的最小外包矩形框
Rect rect;
Point2f vertices[4];
rect = boundingRect(contours.at(0));
RotatedRect AngleRect = minAreaRect(contours.at(0));
AngleRect.points(vertices);
line(image, vertices[0], vertices[1], Scalar(0, 0, 255));
line(image, vertices[0], vertices[3], Scalar(0, 0, 255));
line(image, vertices[1], vertices[2], Scalar(0, 0, 255));
line(image, vertices[2], vertices[3], Scalar(0, 0, 255));
cout << "AngleRect_angle: " << AngleRect.angle << endl;
rectangle(image, rect, Scalar(255, 0, 0), 1);
//求三角形各顶点坐标
vector<Point2i> CoordinatePoint(3);
CoordinatePoint[0].x = rect.x; //直角顶点
CoordinatePoint[0].y = rect.y;
for (int i = 0; i < contours.size();i++)
{
for (int j=0; j < contours[i].size();j++)
{
//取出左边第一个点 (三角形斜边第一个点)
if (rect.x == contours[i][j].x)
{
CoordinatePoint[1].x = contours[i][j].x;
CoordinatePoint[1].y = contours[i][j].y;
}
//取出上边第一个点 (三角形斜边第二个点)
if (rect.y == contours[i][j].y)
{
CoordinatePoint[2].x = contours[i][j].x;
CoordinatePoint[2].y = contours[i][j].y;
}
}
}
//算出三角形斜边长度,短直角边长度,求正弦得夹角度数
float Hypotenuse = 0, ShortEdge = 0;
Hypotenuse = sqrt((CoordinatePoint[2].x - CoordinatePoint[1].x) * (CoordinatePoint[2].x - CoordinatePoint[1].x) +
(CoordinatePoint[2].y - CoordinatePoint[1].y) * (CoordinatePoint[2].y - CoordinatePoint[1].y)); //斜边长度
ShortEdge = sqrt((CoordinatePoint[1].x - CoordinatePoint[0].x) * (CoordinatePoint[1].x - CoordinatePoint[0].x) +
(CoordinatePoint[1].y - CoordinatePoint[0].y) * (CoordinatePoint[1].y - CoordinatePoint[0].y));
//用反三角函数求出角度
float angle = 0;
angle = asinf(ShortEdge / Hypotenuse);
angle = (angle * 180) / M_PI;
cout << "AngleRect_angle_test: " << angle << endl;
namedWindow("src", CV_WINDOW_AUTOSIZE);
namedWindow("canny", CV_WINDOW_AUTOSIZE);
imshow("src", image);
imshow("canny", CannyOut);
while (true)
{
if (waitKey(10) == 27)
{
break;
}
}
destroyAllWindows();
return 0;
}
上面的代码中我自己根据 findContours() 及 boundingRect() 函数的作用重写了一个测量矩形角度的简单算法,测试结果对比如上图三所示,OpenCV自带的算法得到的结果为 angle1 = -30.07° ,取绝对值为30.07°,而我自己实现的算法angle2 = 31.30°,相差了1.23°。