opencv(十四)-图像轮廓

1.轮廓的属性

二值图像分析最常见的一个主要方式就是轮廓发现与轮廓分析,其中轮廓发现的目的是为轮廓分析做准备,经过轮廓分析我们可以得到轮廓各种有用的属性信息、常见的如下:

轮廓面积
轮廓周长
轮廓几何矩
轮廓的最小外接矩形
轮廓的最大外接矩形
轮廓的最小外接圆
轮廓的最小外接三角形
轮廓拟合(支持拟合直线、椭圆、圆)
轮廓的凸包
轮廓层次信息提取
多边形逼近
计算欧拉数

2.轮廓查找并绘制

轮廓检测的基本步骤:
1.图像转换为灰度图像,
2.图像的滤波,降噪,
3.使用compare、inRange、threshold、adaptiveThreshold、Canny等来创建灰度或彩色的二值图像
4.寻找轮廓findContours
5.绘制轮廓drawContours

轮廓查找函数,用于形状分析以及对象检测和识别

void cv::findContours(
InputArray      image,// 输入图像、八位单通道的,背景为黑色
OutputArrayOfArrays contours,//得到的轮廓图像,是一系列的点集合vector<vector<Point>>
OutputArray hierarchy,//层次图像,根据需要提取轮廓层次信息,每个轮廓有四个相关信息,vector<Vec4i> 
                      //分别是同层下一个、前一个、第一个子节点、父节点
int   mode,
int   method,
Point offset = Point()//表示轮廓偏移,默认为0
)

void findContours(
        InputArray image, 
        OutputArrayOfArrays contours,
        int mode, 
        int method, 
        Point offset = Point()
    );

image原图像,一个8位的单通道图像。非零像素被认为为1,0像素为任然保留为0。所以图像被当做二值图像。您可以使用compare、inRange、threshold、adaptiveThreshold、Canny等来创建灰度或彩色的二值图像。如果mode等于RETR_CCOMP或RETR_FLOODFILL,则输入也可以是32位整数的标签图像(CV_32SC1)。
mode 表示轮廓寻找时候的拓扑结构返回
-RETR_EXTERNAL表示只返回最外层轮廓,包含在外围轮廓内的内围轮廓被忽略。
-RETR_LIST检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓。
-RETR_CCOMP检索所有轮廓并将其组织为两级层次结构。在顶层,组件具有外部边界。在第二层,有孔的边界。如果所连接零部件的孔内还有其他轮廓,则该轮廓仍将放置在顶层。
-RETR_TREE表示检索所有轮廓,并重建嵌套轮廓的完整层次。所有轮廓建立一个等级树结构,外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。

Method表示轮廓点集合取得是基于什么算法,常见的是基于CHAIN_APPROX_SIMPLE链式编码方法
①:CHAIN_APPROX_NONE
绝对存储所有轮廓点。也就是说,轮廓的任意两个后续点(x1,y1)和(x2,y2)将是水平,垂直或对角线邻居,即相邻的两个点的像素位置差不超过1,即max (abs (x1 - x2), abs(y2 - y1) =1。
②:CHAIN_APPROX_SIMPLE
压缩水平,垂直和对角线段,仅保留其端点。例如,例如一个矩形轮廓只需4个点来保存轮廓信息。
③:CHAIN_APPROX_TC89_L1
使用teh-Chinl chain 近似算法
④:CHAIN_APPROX_TC89_KCOS
应用teh-Chinl chain 近似算法

绘制轮廓函数,绘制轮廓或者填充轮廓

void cv::drawContours(
	InputOutputArray image,
	InputArrayOfArrays contours,
	int contourIdx,//绘制轮廓:-1代表绘制所有轮廓
	const Scalar & 	color,
	int thickness = 1,//正数的时候表示绘制该轮廓,负数的时候表示填充该轮廓
	int lineType = LINE_8,
	InputArray 	hierarchy = noArray(),
	int maxLevel = INT_MAX,
	Point offset = Point() 
)

hierarchy 表示绘制轮廓的最大级别。 如果为0,则仅绘制指定的轮廓。 如果为1,则该函数绘制轮廓和所有嵌套轮廓。 如果为2,则该函数绘制轮廓,所有嵌套轮廓,所有嵌套到嵌套的轮廓,等等。 仅当有可用的层次结构时才考虑此参数

	// 轮廓查找与绘制
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
	for (size_t t = 0; t < contours.size(); t++) 
	{
		drawContours(src, contours, t, Scalar(0, 0, 255), 2, 8);
	}

3.轮廓分析

3.1 常用API函数

// 计算轮廓面积
double cv::contourArea(
InputArray contour,
bool oriented = false
)
// 计算轮廓周长
double cv::arcLength(
InputArray      curve,
bool        closed
)
// 计算最小外接矩形
RotatedRect cv::minAreaRect(
InputArray      points
)
// 计算最大外接矩形
Rect cv::boundingRect(
InputArray      array
)
// 计算最小外接圆/拟合圆
void cv::minEnclosingCircle(
InputArray      points,
Point2f &        center,
float &    radius
)
// 计算最小外接三角形/拟合三角形
double cv::minEnclosingTriangle(
InputArray      points,
OutputArray   triangle
)
// 拟合直线
void cv::fitLine(
InputArray      points,
OutputArray   line,
int   distType,
double    param,
double    reps,
double    aeps
)
// 最小二乘法拟合椭圆
RotatedRect cv::fitEllipse(
InputArray      points
)
// 计算凸包
void cv::convexHull(
InputArray      points,
OutputArray   hull,
bool        clockwise = false,
bool        returnPoints = true
)
//判断一个轮廓是否为凸包,用于检测点是否落在多边形内之前的快速检测
bool cv::isContourConvex(
InputArray contour
)
// 多边形逼近-逼近真实形状
void cv::approxPolyDP(
InputArray      curve,
OutputArray   approxCurve,
double    epsilon,
bool        closed
)
//检测点是否落在多边形内
double cv::pointPolygonTest(
	InputArray contour,
	Point2f pt,
	bool measureDist //True,则返回每个点到轮廓的距离,如果是False则返回+1,0,-1三个值,其中+1表示点在轮廓内部,0表示点在轮廓上,-1表示点在轮廓外
)

3.2 DEMO

//1. 轮廓的面积和弧长
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
	for (size_t t = 0; t < contours.size(); t++) {
		// 最大外接轮廓
		Rect rect = boundingRect(contours[t]);
		// rectangle(src, rect, Scalar(0, 0, 255), 1, 8, 0);

		// 面积过滤
		double area = contourArea(contours[t]);
		double curvelen = arcLength(contours[t], true);
		if (area < 100 || curvelen < 100) {
			continue;
		}

		// 最小外接轮廓
		RotatedRect rrt = minAreaRect(contours[t]);
		Point2f pts[4];
		rrt.points(pts);//RotatedRect 矩形的四个角点坐标

		// 绘制旋转矩形与中心位置
		for (int i = 0; i < 4; i++) {
			line(src, pts[i % 4], pts[(i + 1) % 4], Scalar(0, 255, 0), 2, 8, 0);
		}
		Point2f cpt = rrt.center;
		circle(src, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
	}
  //2.多边形逼近-逼近真实形状
    vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
	Scalar color = Scalar(255, 0, 0);
	for (size_t t = 0; t < contours.size(); t++) {
		RotatedRect rrt = minAreaRect(contours[t]);
		Point2f cpt = rrt.center;
		circle(src, cpt, 2, Scalar(0, 255, 0), 2, 8, 0);

		Mat result;
		approxPolyDP(contours[t], result, 4, true);
		printf("corners : %d\n", result.rows);
		if (result.rows == 6) {
		   //在图上添加文字
			putText(src, "poly", cpt, FONT_HERSHEY_SIMPLEX, .7, color, 1, 8);
		}
		if (result.rows == 3) {
			putText(src, "triangle", cpt, FONT_HERSHEY_SIMPLEX, .7, color, 1, 8);
		}
		if (result.rows == 4) {
			putText(src, "rectangle", cpt, FONT_HERSHEY_SIMPLEX, .7, color, 1, 8);
		}
		if(result.rows > 10) {
			putText(src, "circle", cpt, FONT_HERSHEY_SIMPLEX, .7, Scalar(0, 255, 0), 1, 8);
		}
	}
		//3. 直线拟合
		Vec4f oneline;
		fitLine(contours[t], oneline, DIST_L1, 0, 0.01, 0.01);
		float vx = oneline[0];
		float vy = oneline[1];
		float x0 = oneline[2];
		float y0 = oneline[3];

		// 直线参数斜率k与截矩b
		float k = vy / vx;
		float b = y0 - k*x0;
        //4.椭圆或圆拟合
        RotatedRect rrt = fitEllipse(contours[t]);
		float w = rrt.size.width;
		float h = rrt.size.height;
		Point centers = rrt.center;
		ellipse(src, rrt, Scalar(0, 0, 255), 2, 8);
//5.绘制最大内接圆
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main(void)
{
	//点多边形绘制
	const int r = 100;
	Mat src = Mat::zeros(Size(4 * r, 4 * r), CV_8U);
	vector<Point2f> vert(6);
	vert[0] = Point(3 * r / 2, static_cast<int>(1.34*r));
	vert[1] = Point(1 * r, 2 * r);
	vert[2] = Point(3 * r / 2, static_cast<int>(2.866*r));
	vert[3] = Point(5 * r / 2, static_cast<int>(2.866*r));
	vert[4] = Point(3 * r, 2 * r);
	vert[5] = Point(5 * r / 2, static_cast<int>(1.34*r));
	for (int i = 0; i < 6; i++)
	{
		line(src, vert[i], vert[(i + 1) % 6], Scalar(255), 3);
	}
	// 点多边形测试
	vector<vector<Point> > contours;
	findContours(src, contours, RETR_TREE, CHAIN_APPROX_SIMPLE);
	Mat raw_dist(src.size(), CV_32F);
	for (int i = 0; i < src.rows; i++)
	{
		for (int j = 0; j < src.cols; j++)
		{
			raw_dist.at<float>(i, j) = (float)pointPolygonTest(contours[0], Point2f((float)j, (float)i), true);
		}
	}
	// 获取最大内接圆半径
	double minVal, maxVal;
	Point maxDistPt; // inscribed circle center
	minMaxLoc(raw_dist, &minVal, &maxVal, NULL, &maxDistPt);
	minVal = abs(minVal);
	maxVal = abs(maxVal);


	Mat drawing = Mat::zeros(src.size(), CV_8UC3);
	for (int i = 0; i < src.rows; i++)
	{
		for (int j = 0; j < src.cols; j++)
		{
			if (raw_dist.at<float>(i, j) < 0)
			{
				drawing.at<Vec3b>(i, j)[0] = (uchar)(255 - abs(raw_dist.at<float>(i, j)) * 255 / minVal);
			}
			else if (raw_dist.at<float>(i, j) > 0)
			{
				drawing.at<Vec3b>(i, j)[2] = (uchar)(255 - raw_dist.at<float>(i, j) * 255 / maxVal);
			}
			else
			{
				drawing.at<Vec3b>(i, j)[0] = 255;
				drawing.at<Vec3b>(i, j)[1] = 255;
				drawing.at<Vec3b>(i, j)[2] = 255;
			}
		}
	}

	// draw max inner circle
	circle(drawing, maxDistPt, (int)maxVal, Scalar(255, 255, 255));
	imshow("Source", src);
	imshow("Distance and inscribed circle", drawing);
	waitKey();
	return 0;
}
//5.凸包检测
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, const char *argv[])
{
	Mat src = imread("images/hand.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input", WINDOW_AUTOSIZE);
	imshow("input", src);

	// 二值化
	Mat dst, gray, binary;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);

	// 删除干扰块
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(binary, binary, MORPH_OPEN, k);
	imshow("binary", binary);

	// 轮廓发现与绘制
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
	for (size_t t = 0; t < contours.size(); t++) {
		vector<Point> hull;
		convexHull(contours[t], hull);
		bool isHull = isContourConvex(contours[t]);
		printf("test convex of the contours %s", isHull ? "Y" : "N");
		int len = hull.size();
		for (int i = 0; i < hull.size(); i++) {
			circle(src, hull[i], 4, Scalar(255, 0, 0), 2, 8, 0);
			line(src, hull[i%len], hull[(i + 1) % len], Scalar(0, 0, 255), 2, 8, 0);
		}
	}
	imshow("contours", src);

	waitKey(0);
	return 0;
}

4.轮廓匹配

4.1 矩与Hu矩匹配

矩的定义:https://www.cnblogs.com/fcfc940503/p/11319251.html

// 计算几何矩与中心距
Moments cv::moments(
InputArray      array,
bool        binaryImage = false
)
//1.使用几何矩计算轮廓中心与横纵比过滤
    vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
	for (size_t t = 0; t < contours.size(); t++) {
		// 最小外接轮廓
		RotatedRect rrt = minAreaRect(contours[t]);
		float w = rrt.size.width;
		float h = rrt.size.height;
		Point2f pts[4];
		rrt.points(pts);

		// 计算横纵比
		float ratio = min(w, h) / max(w, h);
		Point2f cpt = rrt.center;
		circle(src, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
		if (ratio > 0.9) {
			circle(src, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
			// 绘制旋转矩形与中心位置
			for (int i = 0; i < 4; i++) {
				line(src, pts[i % 4], pts[(i + 1) % 4], Scalar(0, 0, 255), 2, 8, 0);
			}
		} 
		if (ratio < 0.5) {
			circle(src, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
			// 绘制旋转矩形与中心位置
			for (int i = 0; i < 4; i++) {
				line(src, pts[i % 4], pts[(i + 1) % 4], Scalar(0, 255, 0), 2, 8, 0);
			}
		}
	}
// 使用Hu矩实现轮廓匹配
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void contours_info(Mat &image, vector<vector<Point>> &pts);
int main(int argc, char** argv) {
	Mat src = imread("images/abc.png");
	imshow("input", src);
	Mat src2 = imread("images/a5.png");
	imshow("input2", src2);

	// 轮廓提取
	vector<vector<Point>> contours1;
	vector<vector<Point>> contours2;
	contours_info(src, contours1);
	contours_info(src2, contours2);
	// hu矩计算
	Moments mm2 = moments(contours2[0]);
	Mat hu2;
	HuMoments(mm2, hu2);
	// 轮廓匹配
	for (size_t t = 0; t < contours1.size(); t++) {
		Moments mm = moments(contours1[t]);
		Mat hum;
		HuMoments(mm, hum);
		double dist = matchShapes(hum, hu2, CONTOURS_MATCH_I1, 0);
		printf("contour match distance : %.2f\n", dist);
		if (dist < 1) {
			printf("draw it \n");
			drawContours(src, contours1, t, Scalar(0, 0, 255), 2, 8);
		}
	}
	imshow("match result", src);
	waitKey(0);
	return 0;
}

void contours_info(Mat &image, vector<vector<Point>> &contours) {
	Mat gray, binary;
	vector<Vec4i> hierarchy;
	cvtColor(image, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
}

4.2 形状场景算法比较轮廓

4.2.1 形状场景距离提取

4.2.2 Hausdorff距离提取

参考

1.https://cloud.tencent.com/developer/article/1588277