目录
一、什么是形状与轮廓检测
给定一张图片:
OpenCV能帮我们使用计算机来识别这些图像,首先可以勾勒出他们的轮廓,其次还可以识别哪些是三角形,哪些是圆形,哪些是矩形。
这就是形状、轮廓检测的功能。
二、预处理
形状与轮廓检测首先需要进行预处理,目的是找到图像的边缘,然后再找到图像的轮廓/轮廓点。
步骤如下:
1.转为灰度图像
2.添加高斯模糊
3.进行坎尼边缘检测
4.进行膨胀操作,使边缘间没有缝隙,称为完整的封闭图
原图
2.1 灰度
2.2 高斯模糊
2.3 坎尼边缘检测
2.4 膨胀
2.5 代码
//定义图片路径
string path = "Resources/shapes.png";
//使用Mat对象来存储
Mat img = imread(path);
//预处理,找到图像的边缘
//找到边缘,然后找到轮廓/轮廓点
//1.转为灰度
Mat imgGray;
cvtColor(img, imgGray, COLOR_BGR2GRAY);
//2.添加高斯模糊
Mat imgBlur;
GaussianBlur(imgGray, imgBlur, Size(3, 3), 3, 0);
//3.进行坎尼边缘检测
Mat imgCanny;
Canny(imgBlur, imgCanny, 25, 75);
//4.进行膨胀操作
Mat imgDil;
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(imgCanny, imgDil, kernel);
三、形状与轮廓检测函数详解
然后我们要得到轮廓,其输入图像为预处理完成后的图像,也就是imgDil;
其输出图像为原图,我们需要在原图上进行图像的描写。
//5.得到轮廓
getContours(imgDil,img);
接下来我们要实现这个函数:
void getContours(Mat imgDil,Mat img)
3.1创建轮廓
首先,我们需要创建轮廓:
这是一个二维vector容器,外层vector存放的是待检测图中,所有形状的轮廓;
每个内层vector存放的是,某个形状轮廓的点集合。
代码
//创建轮廓
vector<vector<Point>> contours;
3.2 找到轮廓
注:3.2——找到轮廓中的函数原型解读,摘抄至优秀原创博主——-牧野-,特此鸣谢!
博客地址:https://blog.csdn.net/dcrmg/article/details/51987348#
函数原型如下:
findContours
(
1.InputOutputArray image,
2.OutputArrayOfArrays contours,
3.OutputArray hierarchy,
4.int mode,
5.int method, Point offset=Point());
)
第一个参数:
image,单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像。
第二个参数:
也就是3.1说明的轮廓二维向量。
第三个参数:
hierarchy,定义为“vector<Vec4i> hierarchy”,
Vec4i是Vec<int,4>的别名,定义了一个“向量内每一个元素包含了4个int型变量”的向量。
所以从定义上看,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(imgDil, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
3.3 过滤噪声点
我们可以看到,原图的中心偏右有一个小黑点,这显然不是我们要检测的图像。
所以我们应该写一个for循环将其过滤掉。
先打印一下每个轮廓的面积:
for (int i = 0; i < contours.size(); i++)
{
//先找到每个轮廓的面积
int area = contourArea(contours[i]);
//打印出面积
cout << area << endl;
}
发现噪声点的面积为185,而其他图像的面积均在1000以上。
所以我们可以在剩下的操作中,写一个if语句将其过滤:
for (int i = 0; i < contours.size(); i++)
{
//先找到每个轮廓的面积
int area = contourArea(contours[i]);
//打印出面积
cout << area << endl;
if (area > 1000)
{
//进行接下来的操作
}
}
3.4 周长检测
double arcLength(InputArray curve, bool closed)
参数详解:
InputArray curve:表示图像的轮廓
bool closed:表示轮廓是否封闭的
代码
//找到每个轮廓的周长
float peri = arcLength(contours[i], true);
3.5 多边形拟合(检测形状的预处理操作)
void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed);//找出轮廓的多边形拟合曲线
第一个参数 InputArray curve:输入的点集
第二个参数OutputArray approxCurve:输出的点集,当前点集是能最小包容指定点集的。画出来即是一个多边形。
第三个参数double epsilon:指定的精度,也即是原始曲线与近似曲线之间的最大距离。
第四个参数bool closed:若为true,则说明近似曲线是闭合的;反之,若为false,则断开。
代码
//角点向量,仅存储轮廓中的角点
vector<vector<Point>> conPoly(contours.size());
approxPolyDP(contours[i], conPoly[i], 0.02 * peri,true);
3.6 绘制轮廓(以拟合的多边形来绘制)
函数:
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()
)
代码
drawContours(img, conPoly, i, Scalar(255, 0, 255), 2);
3.7 绘制矩形边界框
矩形边界框和轮廓的区别在于:
紫色为轮廓,
绿色为边界框。
函数:Rect boundingRect(InputArray points)
points:输入信息,可以为包含点的容器(vector)或是Mat。
返回包覆输入信息的最小正矩形。
如下图:
代码
//存储矩形边界框向量
vector<Rect> boundRect(contours.size());
//绘制矩形边界框,将含纳每一个独立的形状
boundRect[i] = boundingRect(conPoly[i]);
//将边界框打印在原图上
rectangle(img, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 5);
3.8 轮廓判断
首先定义出每个轮廓的角点数:
//定义每个轮廓的角点数
int objCor = (int)conPoly[i].size();
然后进行判断
//定义轮廓对象的名称
string objectType;
//三角形判断
if (objCor == 3)
{
objectType = "Triangle";
}
//矩形判断
if (objCor == 4)
{
//定义长宽比
float aspectRatio = (float)boundRect[i].width /
(float)boundRect[i].height;
cout << "长宽比"<< aspectRatio << endl;
//正方形判断
if (aspectRatio > 0.95 && aspectRatio < 1.05)
{
objectType = "Square";
}
//不然即是矩形
else
{
objectType = "Rectangle";
}
}
//圆形判断
if (objCor >4)
{
objectType = "Circle";
}
最后绘制出名称:
//在形状上方写出对应的名称
putText(img, objectType, { boundRect[i].x,boundRect[i].y - 5 }, FONT_HERSHEY_DUPLEX, 0.75, Scalar(0, 69, 255), 2);
至此大功告成。
四、效果
可以看到精确度还是非常高的。
五、总代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void getContours(Mat imgDil,Mat img)
{
//创建轮廓
vector<vector<Point>> contours;
//创建层级轮廓
//Vec4i:每个向量具有4个整数值
vector<Vec4i> hierarchy;
//找到轮廓
findContours(imgDil, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//绘制轮廓
//绘制所有的,符号为-1,颜色为紫色,厚度为2
//drawContours(img, contours, -1,Scalar(255, 0, 255),10);
//过滤噪声点
for (int i = 0; i < contours.size(); i++)
{
//先找到每个轮廓的面积
int area = contourArea(contours[i]);
//打印出面积
cout << area << endl;
//定义轮廓对象的名称
string objectType;
//角点向量,仅存储轮廓中的角点
vector<vector<Point>> conPoly(contours.size());
//存储矩形边界框向量
vector<Rect> boundRect(contours.size());
//如果面积在可提取范围内,则提取对应轮廓
if (area > 1000)
{
//找到每个轮廓的周长
float peri = arcLength(contours[i], true);
//从轮廓中找到曲线的角点,true表示是否闭合
//轮廓:contours[i]中包含所有的点
//而角点:conPoly[i]中仅包含角点
//如果是矩形,将有4个,三角形则是3个
approxPolyDP(contours[i], conPoly[i], 0.02 * peri,true);
//绘制所有的,符号为-1,颜色为紫色,厚度为2
drawContours(img, conPoly, i, Scalar(255, 0, 255), 2);
//打印出每个轮廓的角点数
cout << conPoly[i].size() << endl;
//绘制矩形边界框,将含纳每一个独立的形状
boundRect[i] = boundingRect(conPoly[i]);
//将边界框打印在原图上
rectangle(img, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 5);
//定义每个轮廓的角点数
int objCor = (int)conPoly[i].size();
//三角形判断
if (objCor == 3)
{
objectType = "Triangle";
}
//矩形判断
if (objCor == 4)
{
//定义长宽比
float aspectRatio = (float)boundRect[i].width / (float)boundRect[i].height;
cout << "长宽比"<< aspectRatio << endl;
//正方形判断
if (aspectRatio > 0.95 && aspectRatio < 1.05)
{
objectType = "Square";
}
//不然即是矩形
else
{
objectType = "Rectangle";
}
}
//圆形判断
if (objCor >4)
{
objectType = "Circle";
}
//在形状上方写出对应的名称
putText(img, objectType, { boundRect[i].x,boundRect[i].y - 5 }, FONT_HERSHEY_DUPLEX, 0.75, Scalar(0, 69, 255), 2);
}
}
}
int main()
{
//定义图片路径
string path = "Resources/shapes.png";
//使用Mat对象来存储
Mat img = imread(path);
//预处理,找到图像的边缘
//找到边缘,然后找到轮廓/轮廓点
//1.转为灰度
Mat imgGray;
cvtColor(img, imgGray, COLOR_BGR2GRAY);
//2.添加高斯模糊
Mat imgBlur;
GaussianBlur(imgGray, imgBlur, Size(3, 3), 3, 0);
//3.进行坎尼边缘检测
Mat imgCanny;
Canny(imgBlur, imgCanny, 25, 75);
//4.进行膨胀操作
Mat imgDil;
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(imgCanny, imgDil, kernel);
//5.得到轮廓
getContours(imgDil,img);
//展示轮廓
imshow("Image", img);
//imshow("Image Gray", imgGray);
//imshow("Image Blur", imgBlur);
//imshow("Image Canny", imgCanny);
//imshow("Image Dil", imgDil);
waitKey(0);
return 0;
}