【RoboMaster大符識別】你確定真的瞭解尋找輪廓函數嗎?【opencv實踐】

前幾天師兄跟我講了一下opencv的findContours()函數識別大符,感覺真的是妙啊!自己學的時候馬馬虎虎,就導致很多細節都沒有領悟到,今天給大家分享一下。

大家看完如果覺得不能很好的理解,就等有時間了動手複製粘貼一遍代碼,就一定能懂了。

還是和前面幾篇文章一樣,我們要找個小項目實踐一下。就以RoboMaster比賽的大符識別這個小項目爲例好了。首先,先給大家介紹一下這個小項目:
在這裏插入圖片描述
這是一個不停在轉的輪盤,上面有兩種不同的紅色的標識,我們需要識別的是封面右上方的那種標識的中心框,識別效果圖如下:
在這裏插入圖片描述
要識別出上圖藍色所標的矩形框,其實有很多方法(圖像處理從來都是仁者見仁智者見智妙招無窮),但利用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:輪廓檢索模式,取值如下圖:
在這裏插入圖片描述
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 <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);

該自定義函數就包含了我們所有的處理操作了。

下文所介紹的,就都是該自定義函數的內容了!

步驟一:扣圖


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<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。
這裏是在做啥呢?
在這裏插入圖片描述
上圖中,藍色框是我們檢測出來的父輪廓,輪廓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函數的妙處了嗎?

如果覺得有收穫,就請點個贊再走叭!我也想閱讀量高一點吖//

作者簡介

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章