前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【opencv实践】你确定真的了解寻找轮廓函数吗?【RM大符识别】

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

作者头像
周旋
修改2020-06-05 14:39:00
2.5K0
修改2020-06-05 14:39:00
举报
文章被收录于专栏:行走的机械人行走的机械人

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

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

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

大家仅看这个封面也是可以的,这是一个不停在转的轮盘,上面有两种不同的红色的标识,我们需要识别的是封面右上方的那种标识的中心框,识别效果图如下:

要识别出上图蓝色所标的矩形框,其实有很多方法(图像处理从来都是仁者见仁智者见智妙招无穷),但利用findContours()函数可以很完美的解决这个问题。我们一步步来。

理论分析

观察图像

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

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

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

但为什么要以此为依据呢?看了下文findContours()函数的内容,你就知道了。

findContours()函数

代码语言:javascript
复制
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了:

编程实现

整体框架搭建

代码语言:javascript
复制

#include <iostream>
#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);

该自定义函数就包含了我们所有的处理操作了。

下文所介绍的,就都是该自定义函数的内容了!

步骤一:扣图

代码语言:javascript
复制

#include <iostream>
#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;
}

我们首先将RGB颜色空间转换为HSV颜色空间,因为扣颜色的话HSV颜色空间更直观:

由上图可以看到红色的HSV空间域的红色区间有两个:【156,180】以及【0,10】,因此我们分别扣出后进行add()函数合并为一个。效果图如下:

详细有关HSV的我们就不讲了,大家可以看这篇CSDN:

代码语言:javascript
复制
https://blog.csdn.net/ColdWindHA/article/details/82080176?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

步骤二:闭操作去小黑洞

这就是常规的图像处理操作啦,主要是为了防止白色的边框有断开的地方。

代码语言:javascript
复制
Mat dstImage;
Mat element = getStructuringElement(MORPH_RECT,Size(5, 5));
morphologyEx(HsvImage, dstImage, MORPH_OPEN, element);

步骤三:寻找边界

这里就是重头戏了!

代码语言:javascript
复制
vector<vector<Point>>contours;//轮廓数组
  vector<Vec4i>hierarchy; //一个参数
  Point2i center; //用来存放找到的目标的中心坐标
  //提取所有轮廓并建立网状轮廓结构
  findContours(dstImage, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));

我们首先定义了一个轮廓数组contours,是vector<vector<Point>>类型的,findContours函数检测到的轮廓都会存放到contours里。

然后定义了vector<Vec4i>hierarchy,这是我们要传给findContours函数的,用来存放每个轮廓contours[i]对应的4个hierarchy元素

hierarchy[i][0]~hierarchy[i][3]。

然后便是运行findContours函数啦。

代码语言:javascript
复制
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。然后看父轮廓对应数组元素的值就知道该父轮廓包含几个子轮廓了。

代码语言:javascript
复制
 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函数的妙处了吗?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Opencv视觉实践 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档