前几天师兄跟我讲了一下opencv的findContours()函数识别大符,感觉真的是妙啊!自己学的时候马马虎虎,就导致很多细节都没有领悟到,今天给大家分享一下。
大家看完如果觉得不能很好的理解,就等有时间了动手复制粘贴一遍代码,就一定能懂了。
还是和前面几篇文章一样,我们要找个小项目实践一下。就以RoboMaster比赛的大符识别这个小项目为例好了。首先,先给大家介绍一下这个小项目:
这是一个不停在转的轮盘,上面有两种不同的红色的标识,我们需要识别的是封面右上方的那种标识的中心框,识别效果图如下:
要识别出上图蓝色所标的矩形框,其实有很多方法(图像处理从来都是仁者见仁智者见智妙招无穷),但利用findContours()函数可以很完美的解决这个问题。我们一步步来。
观察分析图像是必不可少的,甚至你对图像理解的好变已经成功了一大部分。
首先我们肯定可以看出,我们需要识别的目标颜色是很鲜艳突出的红色,所以讲红色扣出来是很容易想到的。
那如何识别那个矩形框呢?我们可以看到,左下角的红色里面包裹这三块黑色,而右上角的红色里面仅包含着一块黑色。这就是我们来识别的依据了!
但为什么要以此为依据呢?看了下文findContours()函数的内容,你就知道了。
findContours(
InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point()
);
先看一下它的参数:
1@image:输入原图像,为8位单通道图像。
2@contours:检测到的轮廓,函数调用后的运行结构存在这里,每个轮廓存储为一个点向量,即用point类型的vector表示。
3@hierarchy:可选的输出向量,包含图像的拓扑信息。其作为轮廓数量的表示,包含了许多元素。每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0]~hierarchy[i][3],分别表示后一个轮廓,前一个轮廓,内嵌轮廓,父轮廓的索引编号。如果没有对应项,对应的hierarchy[i]值设置为负数。
4@mode:轮廓检索模式,取值如下图:
5@method:为轮廓的近似办法,取值如下图:
6@offset:每个轮廓点的可选偏移量,有默认值Point(),对ROI图像中找出的轮廓,并要在整个图像中进行分析时,这个参数便可排上用场。
其中第三个参数是我们需要重点关注的,它是我们解决这个问题的依据:
如何理解呢?我们以下图为例:
我们的findContours()函数会将上图中的黑色边框找出来,并依次标号为1~7。我们可以说边框1为边框3的前一个轮廓,也就是contours[3]的hierarchy[3][1] = 1。
同理,我们可以认为边框2的父轮廓为边框1,则contours[2]的hiearchy[2][3] = 1。
同样,边框6,7的父轮廓为边框5,只不过当我们返回边框5的内嵌轮廓(子轮廓)时,只能返回6,7其中之一。
到此理解了findContours()函数,我们再回顾一下我们要处理的图像:
结合上面关于findContours()函数的介绍,我们可以先将红色区域扣出来,然后寻找边框,之后我们只需找出那个仅含一个子轮廓的轮廓,就是我们要找的红色区域。而该轮廓的子轮廓,就是我们的目标target了。
#include
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main()
{
VideoCapture capture("大符.mp4"); //读入视频
Mat frame, srcImage;
Point2i center; //定义矩形中心
while (1)
{
capture >> frame;//读入帧
resize(frame, srcImage, Size(frame.cols / 3, frame.rows / 3));//转换大小(原视频太大了)
center = markred(srcImage); //自定义函数进行识别
imshow("效果图", srcImage);
cout << center << endl; //打印目标坐标
if (waitKey(30) >= 0) //按任意键退出
break;
}
return 0;
}
上面函数就是完成读取视频操作了,其中用到了一个自定义的函数
markred(srcImage);
该自定义函数就包含了我们所有的处理操作了。
下文所介绍的,就都是该自定义函数的内容了!
Mat hsvImage,dstImage1, dstImage2, HsvImage;
cvtColor(srcImage, hsvImage, COLOR_BGR2HSV);//转换为HSV图
inRange(hsvImage, Scalar(156, 43, 46), Scalar(180, 255, 255), dstImage1);//二值化图像,阈值为红色域
inRange(hsvImage, Scalar(0, 43, 46), Scalar(10, 255, 255), dstImage2);//二值化图像,阈值为红色域
add(dstImage1, dstImage2, HsvImage);
我们首先将RGB颜色空间转换为HSV颜色空间,因为扣颜色的话HSV颜色空间更直观:
由上图可以看到红色的HSV空间域的红色区间有两个:【156,180】以及【0,10】,因此我们分别扣出后进行add()函数合并为一个。效果图如下:
详细有关HSV的我们就不讲了,大家可以看这篇CSDN:
OpenCV学习笔记——HSV颜色空间超极详解&inRange函数用法及实战
这就是常规的图像处理操作啦,主要是为了防止白色的边框有断开的地方。
Mat dstImage;
Mat element = getStructuringElement(MORPH_RECT,Size(5, 5));
morphologyEx(HsvImage, dstImage, MORPH_OPEN, element);
这里就是重头戏了!
vector<vector<Point>>contours;//轮廓数组
vector<Vec4i>hierarchy; //一个参数
Point2i center; //用来存放找到的目标的中心坐标
//提取所有轮廓并建立网状轮廓结构
findContours(dstImage, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
我们首先定义了一个轮廓数组contours,是vector
然后定义了vectorhierarchy,这是我们要传给findContours函数的,用来存放每个轮廓contours[i]对应的4个hierarchy元素
hierarchy[i][0]~hierarchy[i][3]。
然后便是运行findContours函数啦。
int contour[20] = { 0 };
for (int i = 0; i < contours.size(); i++)//遍历检测的所有轮廓
{
if (hierarchy[i][3] != -1) //有内嵌轮廓,说明是一个父轮廓
{
contour[hierarchy[i][3]]++; //对该父轮廓进行记录
}
}
然后我们定义了一个20个单位长的0数组contour[20]。然后我们遍历所有上一步的检测到的轮廓,当某一轮廓的hierarchy[i][3]不等于-1时,也就是说明该轮廓有父轮廓,也就是说明该轮廓为一个内嵌轮廓。
这时,我们将数组
contour[hierarchy[i][3]]自增1。
这里是在做啥呢?
上图中,蓝色框是我们检测出来的父轮廓,轮廓1里面有一个黑洞,也就是包含一个内嵌轮廓,而2中没有内嵌轮廓,3中有三个内嵌轮廓。
而我们要检测的就是轮廓1的内嵌轮廓。但opencv中没有直接数父轮廓里所包含内嵌轮廓个数的函数。怎么办呢?
我们就检测子轮廓(内嵌轮廓),检测到一个子轮廓,就将其父轮廓对应的数组元素加1。然后看父轮廓对应数组元素的值就知道该父轮廓包含几个子轮廓了。
for (int j = 0; j < contours.size(); j++)//再次遍历所有轮廓
{
if (contour[j] == 1) //如果某轮廓对应数组的值为1,说明只要一个内嵌轮廓
{
int num = hierarchy[j][2]; //记录该轮廓的内嵌轮廓
RotatedRect box = minAreaRect(contours[num]); //包含该轮廓所有点
Point2f vertex[4];
box.points(vertex);//将左下角,左上角,右上角,右下角存入点集
for (int i = 0; i < 4; i++)
{
line(srcImage, vertex[i], vertex[(i + 1) % 4], Scalar(255, 0, 0), 4, LINE_AA); //画线
}
center = (vertex[0] + vertex[2]) / 2; //返回中心坐标
putText(srcImage, "target", vertex[0], FONT_HERSHEY_SIMPLEX, 1.0, Scalar(255, 255, 0));//打印字体
}
}
然后上面的程序就是筛选出我们想要的目标轮廓并画出来,再返回其坐标了。处理结果如下:
好了,到此我们就完成了。你感觉到findContours函数的妙处了吗?
如果觉得有收获,就请点个赞再走叭!我也想阅读量高一点吖//