http://blog.csdn.net/maomao1011120756/article/details/49794997
最近用OpenCV的轮廓提取函数,总结一下。
void findContours//提取轮廓,用于提取图像的轮廓
(
InputOutputArray image,//输入图像,必须是8位单通道图像,并且应该转化成二值的
OutputArrayOfArrays contours,//检测到的轮廓,每个轮廓被表示成一个point向量
OutputArray hierarchy,//可选的输出向量,包含图像的拓扑信息。其中元素的个数和检测到的轮廓的数量相等
int mode,//说明需要的轮廓类型和希望的返回值方式
int method,//轮廓近似方法
Point offset = Point()
)
void drawContours//绘制轮廓,用于绘制找到的图像轮廓
(
InputOutputArray image,//要绘制轮廓的图像
InputArrayOfArrays contours,//所有输入的轮廓,每个轮廓被保存成一个point向量
int contourIdx,//指定要绘制轮廓的编号,如果是负数,则绘制所有的轮廓
const Scalar& color,//绘制轮廓所用的颜色
int thickness = 1, //绘制轮廓的线的粗细,如果是负数,则轮廓内部被填充
int lineType = 8, /绘制轮廓的线的连通性
InputArray hierarchy = noArray(),//关于层级的可选参数,只有绘制部分轮廓时才会用到
int maxLevel = INT_MAX,//绘制轮廓的最高级别,这个参数只有hierarchy有效的时候才有效
//maxLevel=0,绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓
//maxLevel=1, 绘制与输入轮廓同一等级的所有轮廓与其子节点。
//maxLevel=2,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点
Point offset = Point()
)
注意:findContours()运行的时候,这个图像会被直接涂改,因此如果是将来还有用的图像,应该复制之后再传给findContours()。
接下来总结一下我在书上看到的解释和自己实验的结果。
一《学习opencv》中给的例子。
图1 输入的测试图(上图)和得到的轮廓图(下图)
传递给findContours()测试图(上图),得到轮廓图(下图)。得到的轮廓只有两种,外部轮廓(橙色虚线)或者孔(蓝色点线)。
1.参数mode的意义
mode的值决定把找到的轮廓如何挂到轮廓树节点变量(h_prev, h_next, v_prev, v_next)上。图2展示了四种可能的mode值所得到的结果的拓扑结构。
图2 轮廓连接方法
每种情况下,结构都可以看成是被横向连接(h_prev, h_next)联系和被纵向连接(v_prev, v_next)不同层次。
CV_RETR_EXTERNAL 只检测出最外轮廓即c0。图2中第一个轮廓指向最外的序列,除此之外没有别的连接。
CV_RETR_LIST 检测出所有的轮廓并将他们保存到表(list)中,图2中描绘了这个表,被找到的9条轮廓相互之间由h_prev和h_next连接。这里并没有表达出纵向的连接关系,没有使用v_prev和v_next.
CV_RETR_COMP 检测出所有的轮廓并将他们组织成双层的结构,第一层是外部轮廓边界,第二层边界是孔的边界。从图2可以看到5个轮廓的边界,其中3个包含孔。最外层边界c0有两个孔,c0之间的所有孔相互间由h_prev和h_next指针连接。
CV_RETR_TREE 检测出所有轮廓并且重新建立网状的轮廓结构。图2中,根节点是最外层的边界c0,c0之下是孔h00,在同一层中与另一个孔h01相连接。同理,每个孔都有子节点(相对于c000和c010),这些子节点和父节点被垂直连接起来。这个步骤一直持续到图像最内层的轮廓,这些轮廓会成为树叶节点。
2. method的五个值
CV_CHAIN_CODE 用freeman链码输出轮廓,其他方法输出多边形(顶点的序列)。
CV_CHAIN_APPROX_NONE将链码编码中的所有点转换为点。
CV_CHAIN_APPROX_SIMPLE压缩水平,垂直或斜的部分,只保存最后一个点。
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_QPPROX_TC89_KCOS使用Teh-Chin链逼近算法中的一个。
CV_LINK_RUNS与上述的算法完全不同,连接所有的水平层次的轮廓。
二 实验部分
我用跟上面的例子结构一样的图作为测试图片(这是我照着上面的图自己画的)。
图3 原图
测试代码如下:
#include "opencv/highgui.h"
#include "opencv/cv.h"
using namespace cv;
void main()
{
Mat image = imread("test.png");
Mat gray;
cvtColor(image, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3), 3, 3);
Mat img;
threshold(gray, img, 0, 255, THRESH_OTSU);
Mat img1;
img.copyTo(img1);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(img, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);
Mat resultImage = Mat ::zeros(img.size(),CV_8U);
drawContours(resultImage, contours, -1, Scalar(255, 0, 255));
return;
}
1. mode的四种方法
下图是分别用mode的四种方法得到的轮廓的结果
图4 四种mode方式分别得到的轮廓结果
从图中可以看出,图4(a)得到的只有一个最外的轮廓,hierarchy的值为{-1,-1,-1,-1},表示除此之外没有别的连接。其他三种方式都可以得到所有的轮廓,而hierarchy的值是不同的,反映了不同的轮廓连接方法即结果的拓扑机构。
2.method的五种方法
CV_CHAIN_CODE是用Freeman链码输出轮廓,在Freeman链码中,多边形被表示成一系列位移,每一个位移有8个方向,使用整数0~7表示。关于Freeman链码的具体内容《学习opencv》这本书里有详细的介绍。通过试验发现用这种方式表示的轮廓不可以通过drawContours()绘制出检测到的轮廓。
CV_CHAIN_APPROX_NONE 是将链码编码中的所有点转换为点,也就是参数contours中的每个轮廓是用构成该轮廓的所有像素点表示的。
CV_CHAIN_APPROX_NONE是输出多边形顶点的序列,也就是说参数contours中的每个轮廓是用该轮廓的所有顶点表示的。
图5分别画出了这两种方式下的contours中的点,用红色的点表示。
图5 轮廓的表达方式
其中(a)中由于像素之间没有间隔已经练成线了。
图6是CV_CHAIN_APPROX_TC89_L1和CV_LINK_RUNS两种方式下contours的结果。
图6 轮廓的表达方式
从图中可以看出,CV_CHAIN_APPROX_TC89_L1的结果也是保存的轮廓的顶点,但是仔细看可以看出轮廓线是有断的感觉不是连贯的,这可能是轮廓逼近过程中的误差,具体情况就不清楚了,也有可能这种方法对具有某种特征的图逼近效果比较好。
书上说CV_LINK_RUNS是和上述算法完全不同的算法,连接所有水平层次的轮廓,但结果画出来的是一条条垂直的线,也不清楚具体怎么回事,不过应该也肯定是有它存在的意义的。另外,在mode=CV_RETR_EXTERNAL的前提下,选用这种方式也是可以画出所有轮廓的,而其他的方式只能画出最外层的轮廓。还有书上说此方法只可与 CV_RETR_LIST搭配使用。我试了其他的方法,也是可以的。
所以method这个参数决定了轮廓的表达方式,这要根据自己提取轮廓后的应用选择合适的轮廓描述方法来决定用哪一种。
3.drawContours()函数中的参数thinkness
thinkness=CV_FILLED可以填充轮廓,opencv官网对这个参数的解释原文是“If it is negative (for example, thickness=CV_FILLED ), the contour interiors are drawn”,图7给出了这个thinkness=CV_FILLED时的绘制结果,可以看出我们得到了跟原图一致的结果。
图7 轮廓绘制结果
关于更复杂的情况就没研究了,我要的就是图7这样的结果。有不对的地方希望大家批评指正!
//ss is the target image of mat.
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(ss, contours, hierarchy, 2, CV_CHAIN_APPROX_NONE);
Mat resultImage = Mat::zeros(ss.size(), CV_8U);
//drawContours(resultImage, contours, -1, Scalar(0, 0, 255));
#if 1
Mat imageContours = Mat::zeros(ss.size(), CV_8UC1); //最小外接矩形画布
int toatal = 0;
int index = 0;
//cout << "the coutours size " << contours.size();
for (int i = 0; i<contours.size(); i++)
{
int length = contours[i].size();
//cout << "the coutours " << i << "length " << contours[i].size();
if (length > 10)
{
index = i;
toatal++;
//绘制轮廓
#if 0
drawContours(ss,contours,i,Scalar(255),1,8,hierarchy);
#else
//绘制轮廓的最小外结矩形
RotatedRect rect = minAreaRect(contours[i]);
Point2f P[4];
rect.points(P);
//rectangle(resultImage,rect, Scalar(255), 1);//can not used this type of RotatedRect
for (int j = 0; j <= 3; j++)
{
line(resultImage, P[j], P[(j + 1) % 4], Scalar(255), 1);
}
double x0, y0, alpha;
x0 = rect.center.x; // 中心位置
y0 = rect.center.y;
alpha = -rect.angle*CV_PI / 180; // 角度转换成弧度
int w = 20;//十字叉的宽度
//绘制轮廓的中心
line(resultImage, Point2f(x0 - w, y0), Point2f(x0 + w, y0), Scalar(0), 1);
line(resultImage, Point2f(x0, y0 - w), Point2f(x0, y0 + w), Scalar(0), 1);
//绘制图像的中心
//line(ss, Point2f(imWidth / 2 - w, imHeight / 2), Point2f(imWidth / 2 + w, imHeight / 2), Scalar(0), 2);
//line(ss, Point2f(imWidth / 2, imHeight / 2 - w), Point2f(imWidth / 2, imHeight / 2 + w), Scalar(0), 2);
double x_offset, y_offset, angle;
//x_offset = x0 - imWidth / 2;
//y_offset = y0 - imHeight / 2;
//angle = -rect.angle*CV_PI / 180;
//emit SendRectInfo(x_offset, y_offset, angle, length);
#endif
}
}
#endif