【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】

前几天师兄跟我讲了一下opencv的findContours()函数识别大符,感觉真的是妙啊!自己学的时候马马虎虎,就导致很多细节都没有领悟到,今天给大家分享一下。

大家看完如果觉得不能很好的理解,就等有时间了动手复制粘贴一遍代码,就一定能懂了。

还是和前面几篇文章一样,我们要找个小项目实践一下。就以RoboMaster比赛的大符识别这个小项目为例好了。首先,先给大家介绍一下这个小项目:

这是一个不停在转的轮盘,上面有两种不同的红色的标识,我们需要识别的是封面右上方的那种标识的中心框,识别效果图如下:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第1张图片
要识别出上图蓝色所标的矩形框,其实有很多方法(图像处理从来都是仁者见仁智者见智妙招无穷),但利用findContours()函数可以很完美的解决这个问题。我们一步步来。

观察图像

观察分析图像是必不可少的,甚至你对图像理解的好变已经成功了一大部分。

首先我们肯定可以看出,我们需要识别的目标颜色是很鲜艳突出的红色,所以讲红色扣出来是很容易想到的。

那如何识别那个矩形框呢?我们可以看到,左下角的红色里面包裹这三块黑色,而右上角的红色里面仅包含着一块黑色。这就是我们来识别的依据了!

但为什么要以此为依据呢?看了下文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:轮廓检索模式,取值如下图:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第2张图片
5@method:为轮廓的近似办法,取值如下图:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第3张图片
6@offset:每个轮廓点的可选偏移量,有默认值Point(),对ROI图像中找出的轮廓,并要在整个图像中进行分析时,这个参数便可排上用场。

其中第三个参数是我们需要重点关注的,它是我们解决这个问题的依据:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第4张图片
如何理解呢?我们以下图为例:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第5张图片
我们的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()函数,我们再回顾一下我们要处理的图像:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第6张图片
结合上面关于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颜色空间更直观:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第7张图片
由上图可以看到红色的HSV空间域的红色区间有两个:【156,180】以及【0,10】,因此我们分别扣出后进行add()函数合并为一个。效果图如下:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第8张图片
详细有关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类型的,findContours函数检测到的轮廓都会存放到contours里。

然后定义了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。
这里是在做啥呢?
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第9张图片
上图中,蓝色框是我们检测出来的父轮廓,轮廓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));//打印字体
    }
  }

然后上面的程序就是筛选出我们想要的目标轮廓并画出来,再返回其坐标了。处理结果如下:
【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第10张图片
好了,到此我们就完成了。你感觉到findContours函数的妙处了吗?

如果觉得有收获,就请点个赞再走叭!我也想阅读量高一点吖//

作者简介

【RoboMaster大符识别】你确定真的了解寻找轮廓函数吗?【opencv实践】_第11张图片

你可能感兴趣的:(计算机视觉)