OpenCV轮廓相关操作 C++

参考
参考

轮廓的基本概念

在OpenCV中,可以通过cv::findContours()函数,在灰度图中寻找轮廓。

函数原型

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

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

参数image是输入的灰度图(注意背景要是黑的,物体是白的);contours是找到的轮廓(每个轮廓由点集表示),其类型一般为std::vector<std::vector<Point> >hierarchy是每个轮廓对应的属性(例如拓扑信息);mode是轮廓的检索模式;method是寻找轮廓时使用的近似算法(cv::CHAIN_APPROX_NONE把轮廓上所有的点存储;cv::CHAIN_APPROX_SIMPLE只存储轮廓上的拐点,例如矩形就只保存四个顶点;cv::CHAIN_APPROX_TC89_L1cv::CHAIN_APPROX_TC89_KCOS为使用"teh-Chinl chain"近似算法)。

检索模式的种类
原图为:在这里插入图片描述
cv::RETR_EXTERNAL仅检索所有外部轮廓,不包含子级轮廓。
效果为:
cv::RETR_LIST检索所有轮廓,不创建任何父子关系。
效果为:在这里插入图片描述
cv::RETR_CCOMP检索所有轮廓并将它们排列为2级层次结构,所有外轮廓为1级,所有子级轮廓为2级。
效果为:在这里插入图片描述
cv::RETR_TREE检索所有轮廓不创建完整的层次列表,如父级、子级、孙子级等。
效果为:在这里插入图片描述
常用的的是cv::RETR_EXTERNALcv::RETR_TREE选项。

轮廓的相关操作

画轮廓
可以通过cv::drawContours()函数,进行画轮廓。
函数原型:

void 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是输入输出图片;contourscv::findContours()检测到的所有轮廓;contourIdx是取第几个轮廓;color是轮廓的颜色;thickness是轮廓的线宽(注意这个参数为-1时会填充整个轮廓);lineType是线的形式;hierarchy为关于层级的可选参数,只有绘制部分轮廓时才会用到;maxLevel为绘制轮廓的最高级别,这个参数只有hierarchy有效的时候才有效(maxLevel=0,绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓、maxLevel=1, 绘制与输入轮廓同一等级的所有轮廓与其子节点、maxLevel=2,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点);offset为轮廓偏移量,配合ROI使用。
例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	for (int i = 0; i < contours.size(); ++i) {  //绘制所有轮廓
		cv::drawContours(dst, contours, i, cv::Scalar(0, 255, 0), -1);  //thickness为-1时为填充整个轮廓
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

效果为:
计算轮廓的长度、面积
可以通过cv::contourArea()来获取轮廓的面积;可以通过cv::arcLength()来获取轮廓的长度。
函数原型:

double contourArea( InputArray contour, bool oriented = false );
double arcLength( InputArray curve, bool closed );

contour表示单个轮廓构成的点集;oriented为面积的方向性,true表示面积具有方向性,false表示不具有方向性。
curve表示单个轮廓构成的点集或任意二维点集;closed表示点集是否闭合,对应找到的轮廓此值应为true。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		printf("第%d个轮廓的面积为%lf 长度为%lf\n", i,
			cv::contourArea(contours[i]), cv::arcLength(contours[i], true));
	}

	return 0;
}

输出:
第0个轮廓的面积为31110.000000 长度为706.0000001个轮廓的面积为32569.000000 长度为767.910811

获取轮廓的最小外接矩形
可以通过cv::minAreaRect()来获取轮廓的最小外接矩形。
函数原型:

RotatedRect minAreaRect( InputArray points );

其返回值是一个矩形(矩形包含了四个角点的信息,以及相对于x轴的旋转角度),参数points是单个轮廓。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<cv::RotatedRect> minAreaRects(contours.size());  //存储轮廓的最小外接矩形

	cv::Point2f ps[4];  //外接矩形四个端点的集合
	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		minAreaRects[i] = cv::minAreaRect(contours[i]);  //获取轮廓的最小外接矩形
		minAreaRects[i].points(ps);  //将最小外接矩形的四个端点复制给ps数组

		for (int j = 0; j < 4; j++) {  //绘制最小外接轮廓的四条边
			line(dst, cv::Point(ps[j]), cv::Point(ps[(j + 1) % 4]), cv::Scalar(0, 255, 0), 2);
			cv::putText(dst, std::to_string(j), ps[j], 0, 1, cv::Scalar(0, 0, 255), 2);  //将点集按顺序显示在图上
		}

		printf("minAreaRects[%d]的旋转角度为%f\n", i, minAreaRects[i].angle);  //输出旋转角度
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

输出:
minAreaRects[0]的旋转角度为90.000000
minAreaRects[1]的旋转角度为52.678967

效果为:在这里插入图片描述
获取轮廓的最小外接长方形
一般的图像操作都要求在方正的图像中进行,因此一般情况下需要轮廓的最小外接长方形。
可以通过cv::boundingRect()获取轮廓的最小外接长方形。
函数原型:

Rect boundingRect( InputArray array );

其返回值是一个长方形,参数array是单个轮廓。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<cv::RotatedRect> minAreaRects(contours.size());  //存储轮廓的最小外接矩形
	std::vector<cv::Rect> boundingRect(contours.size());  //存储轮廓的最小外接矩形

	cv::Point2f ps[4];  //外接矩形四个端点的集合
	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		minAreaRects[i] = cv::minAreaRect(contours[i]);  //获取轮廓的最小外接矩形
		minAreaRects[i].points(ps);  //将最小外接矩形的四个端点复制给rect数组

		for (int j = 0; j < 4; j++) {  //绘制最小外接轮廓的四条边
			line(dst, cv::Point(ps[j]), cv::Point(ps[(j + 1) % 4]), cv::Scalar(0, 255, 0), 8);
		}

		boundingRect[i] = cv::boundingRect(contours[i]);  //获取轮廓的最小外接长方形
		cv::rectangle(dst, boundingRect[i], cv::Scalar(255, 0, 0), 2);  //绘制最小外接长方形
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

效果为:在这里插入图片描述
获取轮廓的图像矩
轮廓矩指图像的某些特定像素灰度的加权平均值,或者是图像具有类似功能或意义的属性。可以通过图像的矩来获得图像的部分性质,包括面积(或总体亮度),以及有关几何中心和方向的信息。它可以被用来获得相对于特定变换的不变性(平移、缩放、旋转不变性) 。
通过轮廓矩可以获得轮廓的面积和几何中心。
二值图像的面积或灰度图像的像素总和,可以表示为 M 00 \mathrm{M}_{00} M00
图像的几何中心可以表示为 { x ‾ , y ‾ } = { M 10 M 00 , M 01 M 00 } \{\overline{\mathrm{x}}, \overline{\mathrm{y}}\}=\left\{\frac{\mathrm{M}_{10}}{\mathrm{M}_{00}}, \frac{\mathrm{M}_{01}}{\mathrm{M}_{00}}\right\} {x,y}={M00M10,M00M01}

可以通过cv::moments()来获取轮廓的轮廓矩。
函数原型:

Moments moments( InputArray array, bool binaryImage = false );

array是轮廓点集或任意二维点集,binaryImage图片是否为二值图像,二值图像是指灰度值只有0或255两种取值的图像。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<cv::Moments> mv(contours.size());  //存储所以轮廓的轮廓矩
	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		mv[i] = cv::moments(contours[i]);  //求轮廓矩
		cv::circle(dst, cv::Point(mv[i].m10 / mv[i].m00, mv[i].m01 / mv[i].m00), 5, cv::Scalar(0, 255, 0), -1);  //绘制轮廓几何中心
		printf("第%d个轮廓的面积为%lf   %lf\n", i, mv[i].m00, cv::contourArea(contours[i]));  //计算轮廓的面积,可知cv::contourArea()其实就是通过轮廓矩获得的
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

输出:
第0个轮廓的面积为31110.000000   31110.0000001个轮廓的面积为32569.000000   32569.000000

效果为:在这里插入图片描述
获取轮廓的最小外接圆
可以通过cv::minEnclosingCircle()来获取轮廓的最小外接矩形。
函数原型:

void minEnclosingCircle( InputArray points,
                                      CV_OUT Point2f& center, CV_OUT float& radius );

points为传入的轮廓点集或任意二维点集;center是返回的圆心;radius为返回的圆半径。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		cv::Point2f center;
		float radius = 0.0f;
		cv::minEnclosingCircle(contours[i], center, radius);  //获取轮廓的最小外接圆

		cv::circle(dst, center, radius, cv::Scalar(0, 0, 255), 2);
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

效果为:在这里插入图片描述
获取轮廓的凸包
可以通过cv::convexHull()获取轮廓的凸包。
凸包即拟合出来的最小外接多边形。
效果为:
函数原型:

void convexHull( InputArray points, OutputArray hull,
                              bool clockwise = false, bool returnPoints = true );

points即输入的二维点集;hull为输出的二维点集;clockwise决定出来的轮廓是否为顺时针方向,为true时为顺时针方向;returnPoints标志,为true时,hull将返回点集,此时为std::vector<cv::Point>类型,为false时,hull将返回返回对应于外壳点的轮廓点的索引。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_0.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<std::vector<cv::Point>> Hulls(contours.size());

	for (int i = 0; i < contours.size(); ++i) {  //遍历所有轮廓
		cv::convexHull(contours[i], Hulls[i], true, true);  //寻找轮廓的凸包,输出点集为顺时针

		cv::drawContours(dst, Hulls, i, cv::Scalar(0, 255, 0), 2);
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

获取逼近后的轮廓
可以通过cv::approxPolyDP()来获取逼近后的轮廓。

函数原型:

void approxPolyDP( InputArray curve,
                                OutputArray approxCurve,
                                double epsilon, bool closed );

curve为输入的点集;approxCurve为逼近后的输出的点集;epsilon为逼近精度,此值越大,逼近后输出的点越少;closed为是否封闭,若封闭此值为true。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_11.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<std::vector<cv::Point>> approxCurves(contours.size());
	for (int i = 0; i < contours.size(); ++i) {  //绘制逼近后的轮廓
		double epsilon = 0.1 * cv::arcLength(contours[i], true);
		cv::approxPolyDP(contours[i], approxCurves[i], epsilon, true);

		cv::drawContours(dst, approxCurves, i, cv::Scalar(0, 255, 0), 2);
	}

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

原图为
在这里插入图片描述
原始轮廓为
在这里插入图片描述
逼近后的轮廓为
在这里插入图片描述
判断一个点和轮廓之间的关系,并获取轮廓的最大内接圆
点和轮廓的关系有3种,在轮廓外、在轮廓内、在轮廓上。
可以通过cv::pointPolygonTest()来获取点和轮廓的关系,此函数会计算得到一个点距离多边形的距离,如果点是轮廓点或者属于轮廓多边形上的点,距离是零,如果是多边形内部的点是是正数,如果是负数返回表示点是外部。

函数原型:

double pointPolygonTest( InputArray contour, Point2f pt, bool measureDist );

返回值表示点到轮廓的距离。
contour是输入的点集;pt是输入的点;measureDist标志,为true时,返回每个点到轮廓的距离,为false时,返回+1,0,-1三个值,其中+1表示点在轮廓内部,0表示点在轮廓上,-1表示点在轮廓外。

例子:

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_11.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	cv::Point pt(100, 50);
	int res = cv::pointPolygonTest(contours[0], pt, false);  //只和第0个轮廓进行比较
	if (res == 0) {
		std::cout << "点在轮廓上" << std::endl;
	}
	else if (res == -1) {
		std::cout << "点在轮廓外" << std::endl;
	}
	else if (res == 1) {
		std::cout << "点在轮廓内" << std::endl;
	}

	cv::drawContours(dst, contours, 0, cv::Scalar(0, 0, 255), 2);
	cv::circle(dst, pt, 5, cv::Scalar(0, 255, 0), -1);
	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

输出:
点在轮廓内

效果:
在这里插入图片描述
通过这个判定,可以获取轮廓的最大内接圆,其思路是,遍历所有点(当然可以自定义ROI),计算所有点到轮廓的距离,仅比较轮廓内的点到轮廓的距离,其中距离最大的轮廓内的点即为最大内接圆的圆心。

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

int main() {
	cv::Mat src = cv::imread("1_11.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	cv::Point center;  //记录最大内接圆的圆心
	float radius = 0.0f;  //记录最大内接圆的半径

	//遍历所有点
	for (int i = 0; i < src.cols; ++i) {
		for (int j = 0; j < src.rows; ++j) {
			double res = cv::pointPolygonTest(contours[0], cv::Point(i, j), true);  //在轮廓内的点,返回的距离是正数
			if (res > 0.0f) {  //只关注轮廓内的点
				if (res > radius) {  //当当前点距离大于记录的点时,更新
					radius = res;
					center = { i, j };
				}
			}
		}
	}

	cv::drawContours(dst, contours, 0, cv::Scalar(0, 0, 255), 2);  //绘制轮廓
	cv::circle(dst, center, radius, cv::Scalar(0, 255, 0), -1);  //绘制最大内接圆
	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

效果:
在这里插入图片描述
缩小、放大轮廓
这个函数还不完善,但可以试试。
建议将轮廓转为凸包后再使用,且cv::findContours()的最后一个参数必须选择为 cv::CHAIN_APPROX_SIMPLE,且需要轮廓为顺时针的。

#include <iostream>
#include <vector>

#include <opencv2\opencv.hpp>

//函数功能:对cv::findContours(...,cv::CHAIN_APPROX_SIMPLE)找到的轮廓进行放大、缩小处理(注意最后的参数必须为cv::CHAIN_APPROX_SIMPLE)
//in为输入轮廓;out为输出轮廓;scalar负数为内缩,正数为外扩
static void contours_handle(std::vector<cv::Point>& in, std::vector<cv::Point>& out, const float scalar) {
	float SAFELINE = scalar;

	std::vector<cv::Point2f> dpList, ndpList;
	int count = in.size();
	for (int i = 0; i < count; ++i) {
		int next = (i == (count - 1) ? 0 : (i + 1));
		dpList.emplace_back(in.at(next) - in.at(i));
		float unitLen = 1.0f / sqrt(dpList.at(i).dot(dpList.at(i)));
		ndpList.emplace_back(dpList.at(i) * unitLen);
	}

	for (int i = 0; i < count; ++i) {
		int startIndex = (i == 0 ? (count - 1) : (i - 1));
		int endIndex = i;
		float sinTheta = ndpList.at(startIndex).cross(ndpList.at(endIndex));
		cv::Point2f orientVector = ndpList.at(endIndex) - ndpList.at(startIndex);  //i.e. PV2-V1P=PV2+PV1
		if (std::isinf(SAFELINE / sinTheta * orientVector.x) || std::isinf(SAFELINE / sinTheta * orientVector.y)) {  //过滤掉离谱数据
			continue;
		}
		out.emplace_back(cv::Point2f(in.at(i).x + SAFELINE / sinTheta * orientVector.x, in.at(i).y + SAFELINE / sinTheta * orientVector.y));
	}

	return;
}

int main() {
	cv::Mat src = cv::imread("1_11.png", -1);

	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);  //只找最外层轮廓

	cv::Mat dst;  //用于绘制结果
	cv::cvtColor(src, dst, cv::COLOR_GRAY2BGR);

	std::vector<std::vector<cv::Point>> Hulls(contours.size());  //保存凸包
	cv::convexHull(contours[0], Hulls[0], true, true);  //寻找轮廓的凸包,输出点集为顺时针

	std::vector <std::vector<cv::Point>> outs(contours.size());  //保存放大、缩小后的轮廓
	contours_handle(Hulls[0], outs[0], -15.0f);  //将轮廓缩小15个像素

	cv::drawContours(dst, contours, 0, cv::Scalar(0, 0, 255), 2);  //绘制原始轮廓
	cv::drawContours(dst, Hulls, 0, cv::Scalar(0, 255, 0), 2);  //绘制轮廓的凸包
	cv::drawContours(dst, outs, 0, cv::Scalar(255, 0, 0), 2);  //绘制缩小后的轮廓

	cv::imshow("dst", dst);
	cv::waitKey();

	cv::destroyAllWindows();
	return 0;
}

效果:
在这里插入图片描述