RM裝甲識別程式再分析(一)
最近半個月在RM視覺組實習,有幸接觸到一些機器人比賽相關的演算法,真不錯嘿嘿!今晚仔細研究了一位大佬博主好久之前發的一篇博文(地址見下),感覺自己對博文中裝甲識別的代碼有一些見解(由于作者學習的是opencv4,鑒于年代隔閡,僅對opencv3.0.0部分代碼進行注解),所以想分享一下,由于作者水平有限,如有錯漏,歡迎各位大佬批評斧正:
RM裝甲識別程式分析(一)_healingwounds的博客-CSDN博客)
首先,將大佬博主的原始碼貼在下面:
//根據以上代碼將其在opencv3.0.0中撰寫
#include "stdafx.h"
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/videoio.hpp"
#include "opencv2/imgproc.hpp"
#include "iostream"
#include "omp.h"
using namespace cv;
using namespace std;
#define T_ANGLE_THRE 10
#define T_SIZE_THRE 5
void brightAdjust(Mat src, Mat dst, double dContrast, double dBright); //亮度調節函式
void getDiffImage(Mat src1, Mat src2, Mat dst, int nThre); //二值化
vector<RotatedRect> armorDetect(vector<RotatedRect> vEllipse); //檢測裝甲
void drawBox(RotatedRect box, Mat img); //標記裝甲
int main()
{
VideoCapture cap0("RawImage\\RedCar.avi");
Mat frame0;
Size imgSize;
RotatedRect s; //定義旋轉矩形
vector<RotatedRect> vEllipse; //定以旋轉矩形的向量,用于存盤發現的目標區域
vector<RotatedRect> vRlt;
vector<RotatedRect> vArmor;
bool bFlag = false;
vector<vector<Point> > contour;
cap0 >> frame0;
imgSize = frame0.size();
Mat rawImg = Mat(imgSize, CV_8UC3);
Mat grayImage = Mat(imgSize, CV_8UC1);
Mat rImage = Mat(imgSize, CV_8UC1);
Mat gImage = Mat(imgSize, CV_8UC1);
Mat bImage = Mat(imgSize, CV_8UC1);
Mat binary = Mat(imgSize, CV_8UC1);
Mat rlt = Mat(imgSize, CV_8UC1);
namedWindow("Raw");
while (1)
{
if (cap0.read(frame0))
{
brightAdjust(frame0, rawImg, 1, -120); //每個像素每個通道的值都減去120
Mat bgr[3];
split(rawImg, bgr); //將三個通道的像素值分離
bImage = bgr[0];
gImage = bgr[1];
rImage = bgr[2];
//如果像素R值-G值大于25,則回傳的二值影像的值為255,否則為0
getDiffImage(rImage, gImage, binary, 25);
dilate(binary, grayImage, Mat(), Point(-1,-1), 3); //影像膨脹
erode(grayImage, rlt, Mat(), Point(-1,-1), 1); //影像腐蝕,先膨脹在腐蝕屬于閉運算
findContours(rlt, contour, RETR_CCOMP , CHAIN_APPROX_SIMPLE); //在二值影像中尋找輪廓
for (int i=0; i<contour.size(); i++)
{
if (contour[i].size()> 10) //判斷當前輪廓是否大于10個像素點
{
bFlag = true; //如果大于10個,則檢測到目標區域
//擬合目標區域成為橢圓,回傳一個旋轉矩形(中心、角度、尺寸)
s = fitEllipse(contour[i]);
for (int nI = 0; nI < 5; nI++)
{
for (int nJ = 0; nJ < 5; nJ++) //遍歷以旋轉矩形中心點為中心的5*5的像素塊
{
if (s.center.y - 2 + nJ > 0 && s.center.y - 2 + nJ < 480 && s.center.x - 2 + nI > 0 && s.center.x - 2 + nI < 640) //判斷該像素是否在有效的位置
{
Vec3b v3b = frame0.at<Vec3b>((int)(s.center.y - 2 + nJ), (int)(s.center.x - 2 + nI)); //獲取遍歷點點像素值
//判斷中心點是否接近白色
if (v3b[0] < 200 || v3b[1] < 200 || v3b[2] < 200)
bFlag = false; //如果中心不是白色,則不是目標區域
}
}
}
if (bFlag)
{
vEllipse.push_back(s); //將發現的目標保存
}
}
}
//呼叫子程式,在輸入的LED所在旋轉矩形的vector中找出裝甲的位置,并包裝成旋轉矩形,存入vector并回傳
vRlt = armorDetect(vEllipse);
for (unsigned int nI = 0; nI < vRlt.size(); nI++) //在當前影像中標出裝甲的位置
drawBox(vRlt[nI], frame0);
imshow("Raw", frame0);
if (waitKey(50) == 27)
{
break;
}
vEllipse.clear();
vRlt.clear();
vArmor.clear();
}
else
{
break;
}
}
cap0.release();
return 0;
}
void brightAdjust(Mat src, Mat dst, double dContrast, double dBright)
{
int nVal;
omp_set_num_threads(8);
#pragma omp parallel for
for (int nI = 0; nI<src.rows; nI++)
{
Vec3b* p1 = src.ptr<Vec3b>(nI);
Vec3b* p2 = dst.ptr<Vec3b>(nI);
for (int nJ = 0; nJ <src.cols; nJ++)
{
for (int nK = 0; nK < 3; nK++)
{
//每個像素的每個通道的值都進行線性變換
nVal = (int)(dContrast * p1[nJ][nK] + dBright);
if (nVal < 0)
nVal = 0;
if (nVal > 255)
nVal = 255;
p2[nJ][nK] = nVal;
}
}
}
}
void getDiffImage(Mat src1, Mat src2, Mat dst, int nThre)
{
omp_set_num_threads(8);
#pragma omp parallel for
for (int nI = 0; nI<src1.rows; nI++)
{
uchar* pchar1 = src1.ptr<uchar>(nI);
uchar* pchar2 = src2.ptr<uchar>(nI);
uchar* pchar3 = dst.ptr<uchar>(nI);
for (int nJ = 0; nJ <src1.cols; nJ++)
{
if (pchar1[nJ] - pchar2[nJ]> nThre) //
{
pchar3[nJ] = 255;
}
else
{
pchar3[nJ] = 0;
}
}
}
}
vector<RotatedRect> armorDetect(vector<RotatedRect> vEllipse)
{
vector<RotatedRect> vRlt;
RotatedRect armor; //定義裝甲區域的旋轉矩形
int nL, nW;
double dAngle;
vRlt.clear();
if (vEllipse.size() < 2) //如果檢測到的旋轉矩形個數小于2,則直接回傳
return vRlt;
for (unsigned int nI = 0; nI < vEllipse.size() - 1; nI++) //求任意兩個旋轉矩形的夾角
{
for (unsigned int nJ = nI + 1; nJ < vEllipse.size(); nJ++)
{
dAngle = abs(vEllipse[nI].angle - vEllipse[nJ].angle);
while (dAngle > 180)
dAngle -= 180;
//判斷這兩個旋轉矩形是否是一個裝甲的兩個LED等條
if ((dAngle < T_ANGLE_THRE || 180 - dAngle < T_ANGLE_THRE) && abs(vEllipse[nI].size.height - vEllipse[nJ].size.height) < (vEllipse[nI].size.height + vEllipse[nJ].size.height) / T_SIZE_THRE && abs(vEllipse[nI].size.width - vEllipse[nJ].size.width) < (vEllipse[nI].size.width + vEllipse[nJ].size.width) / T_SIZE_THRE)
{
armor.center.x = (vEllipse[nI].center.x + vEllipse[nJ].center.x) / 2; //裝甲中心的x坐標
armor.center.y = (vEllipse[nI].center.y + vEllipse[nJ].center.y) / 2; //裝甲中心的y坐標
armor.angle = (vEllipse[nI].angle + vEllipse[nJ].angle) / 2; //裝甲所在旋轉矩形的旋轉角度
if (180 - dAngle < T_ANGLE_THRE)
armor.angle += 90;
nL = (vEllipse[nI].size.height + vEllipse[nJ].size.height) / 2; //裝甲的高度
nW = sqrt((vEllipse[nI].center.x - vEllipse[nJ].center.x) * (vEllipse[nI].center.x - vEllipse[nJ].center.x) + (vEllipse[nI].center.y - vEllipse[nJ].center.y) * (vEllipse[nI].center.y - vEllipse[nJ].center.y)); //裝甲的寬度等于兩側LED所在旋轉矩形中心坐標的距離
if (nL < nW)
{
armor.size.height = nL;
armor.size.width = nW;
}
else
{
armor.size.height = nW;
armor.size.width = nL;
}
vRlt.push_back(armor); //將找出的裝甲的旋轉矩形保存到vector
}
}
}
return vRlt;
}
void drawBox(RotatedRect box, Mat img)
{
Point2f pt[4];
int i;
for (i = 0; i<4; i++)
{
pt[i].x = 0;
pt[i].y = 0;
}
box.points(pt); //計算二維盒子頂點
line(img, pt[0], pt[1], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[1], pt[2], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[2], pt[3], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[3], pt[0], CV_RGB(0, 0, 255), 2, 8, 0);
}
然后,我要開始發揮啦:(其中,多行注釋部分是我個人的理解,多行注釋開頭的帶圈序號如①②③…… 分別對應原始碼中序號①②③,方便大家查看)
//根據以上代碼將其在opencv3.0.0中撰寫
#include "stdafx.h"
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/videoio.hpp"
#include "opencv2/imgproc.hpp"
#include "iostream"
#include "omp.h"
using namespace cv;
using namespace std;
#define T_ANGLE_THRE 10
#define T_SIZE_THRE 5
void brightAdjust(Mat src, Mat dst, double dContrast, double dBright); //亮度調節函式
void getDiffImage(Mat src1, Mat src2, Mat dst, int nThre); //二值化
vector<RotatedRect> armorDetect(vector<RotatedRect> vEllipse); //檢測裝甲
void drawBox(RotatedRect box, Mat img); //標記裝甲
int main()
{
/*
① 利用VideoCapture類讀取待檢測的視頻檔案,并將幀影像存放在Mat變數中
*/
VideoCapture cap0("RawImage\\RedCar.avi"); ①
Mat frame0; ①
Size imgSize;
RotatedRect s; //定義旋轉矩形
vector<RotatedRect> vEllipse; //定以旋轉矩形的向量,用于存盤發現的目標區域
vector<RotatedRect> vRlt;
vector<RotatedRect> vArmor;
bool bFlag = false;
vector<vector<Point> > contour;
cap0 >> frame0; ①
imgSize = frame0.size();
Mat rawImg = Mat(imgSize, CV_8UC3);
Mat grayImage = Mat(imgSize, CV_8UC1);
Mat rImage = Mat(imgSize, CV_8UC1);
Mat gImage = Mat(imgSize, CV_8UC1);
Mat bImage = Mat(imgSize, CV_8UC1);
Mat binary = Mat(imgSize, CV_8UC1);
Mat rlt = Mat(imgSize, CV_8UC1);
namedWindow("Raw");
while (1)
{
if (cap0.read(frame0))
{
brightAdjust(frame0, rawImg, 1, -120); //每個像素每個通道的值都減去120 ②
Mat bgr[3];
split(rawImg, bgr); //將三個通道的像素值分離
bImage = bgr[0]; ③
gImage = bgr[1]; ③
rImage = bgr[2]; ③
//如果像素R值-G值大于25,則回傳的二值影像的值為255,否則為0
getDiffImage(rImage, gImage, binary, 25); ③
/*
④ 先膨脹(dilate)后腐蝕(erode)屬于opencv影像像素的形態學操作中的閉運算操作,可以用于排除小型黑洞,使輪廓平滑化
*/
dilate(binary, grayImage, Mat(), Point(-1,-1), 3); //影像膨脹 ④
erode(grayImage, rlt, Mat(), Point(-1,-1), 1); //影像腐蝕,先膨脹在腐蝕屬于閉運算 ④
/*
⑤ 這里有一個小tip:findContours()函式有多載版本,原始碼中使用的是第二個版本,該版本的函式沒有第三個引數hierarchy
RETR_CCOMP表示檢測所有的輪廓,但所有輪廓只建立兩個等級關系,外圍為頂層,若外圍內的內圍輪廓還包含了其他的輪廓資訊,則內圍內的所有輪廓均歸屬于頂層
CHAIN_APPROX_SIMPLE表示僅保存輪廓的拐點資訊,把所有輪廓拐點處的點保存入contours向量內,拐點與拐點之間直線段上的資訊點不予保留
*/
findContours(rlt, contour, RETR_CCOMP , CHAIN_APPROX_SIMPLE); //在二值影像中尋找輪廓 ⑤
/*
⑥ 判斷輪廓的大小是否大于10個像素點,初步過濾掉一些噪聲區域(引數10可以進行除錯修改)
⑦ fitEllipse()函式是opencv橢圓擬合函式,輸入引數為不少于六個輪廓點組成的二維點集,通過最小二乘法(機器學習線性模型)進行擬合,輸出為擬合出橢圓的最小外接矩形(旋轉矩形RotatedRect)
⑧ 此處需要遍歷以旋轉矩形中心點為中心的5*5像素塊,先減二再加像素塊索引的目的是:先將檢索位置從旋轉矩形中心點大致移到旋轉矩形左邊界處(這里之所以說“大致”是因為 5/2=2.5!=2)
⑨ 由于用r-b之后得到的二值化影像中,白色區域表示原圖的紅色區域,因此只有中心點為白色的區域才是紅色LED燈帶真正所在區域,故通過旋轉矩陣中心點的三個通道的像素值與閾值200進行比較,判斷中心點是否為白色,另外,如果對方機器人車的紅燈亮度較低,可以調低閾值,反之亦然
*/
for (int i=0; i<contour.size(); i++)
{
if (contour[i].size() > 10) //判斷當前輪廓是否大于10個像素點 ⑥
{
bFlag = true; //如果大于10個,則檢測到目標區域
//擬合目標區域成為橢圓,回傳一個旋轉矩形(中心、角度、尺寸)
s = fitEllipse(contour[i]); ⑦
for (int nI = 0; nI < 5; nI++)
{
for (int nJ = 0; nJ < 5; nJ++) //遍歷以旋轉矩形中心點為中心的5*5的像素塊
{
if (s.center.y - 2 + nJ > 0 && s.center.y - 2 + nJ < 480 && s.center.x - 2 + nI > 0 && s.center.x - 2 + nI < 640) //判斷該像素是否在有效的位置 ⑧
{
Vec3b v3b = frame0.at<Vec3b>((int)(s.center.y - 2 + nJ), (int)(s.center.x - 2 + nI)); //獲取遍歷點點像素值 ⑧
//判斷中心點是否接近白色
if (v3b[0] < 200 || v3b[1] < 200 || v3b[2] < 200) ⑨
bFlag = false; //如果中心不是白色,則不是目標區域
}
}
}
if (bFlag)
{
vEllipse.push_back(s); //將發現的目標保存
}
}
}
//呼叫子程式,在輸入的LED所在旋轉矩形的vector中找出裝甲的位置,并包裝成旋轉矩形,存入vector并回傳
vRlt = armorDetect(vEllipse); ⑩
for (unsigned int nI = 0; nI < vRlt.size(); nI++) //在當前影像中標出裝甲的位置
drawBox(vRlt[nI], frame0); ⑩+①
imshow("Raw", frame0);
if (waitKey(50) == 27)
{
break;
}
vEllipse.clear();
vRlt.clear();
vArmor.clear();
}
else
{
break;
}
}
cap0.release();
return 0;
}
void brightAdjust(Mat src, Mat dst, double dContrast, double dBright)
{
int nVal;
omp_set_num_threads(8);
#pragma omp parallel for
/*
② 用兩個Vec3b的指標分別指向每個像素點
用nK遍歷每個像素點的三個通道
將每個像素點的每個通道值均進行調整(減小),增強亮暗對比度,突出LED燈帶的所在區域
更新每個像素點的每個通道值
*/
for (int nI = 0; nI<src.rows; nI++)
{
Vec3b* p1 = src.ptr<Vec3b>(nI); ②
Vec3b* p2 = dst.ptr<Vec3b>(nI); ②
for (int nJ = 0; nJ <src.cols; nJ++)
{
for (int nK = 0; nK < 3; nK++) ②
{
//每個像素的每個通道的值都進行線性變換
nVal = (int)(dContrast * p1[nJ][nK] + dBright); ②
if (nVal < 0)
nVal = 0;
if (nVal > 255)
nVal = 255;
p2[nJ][nK] = nVal; ②
}
}
}
}
/*
③ 使用split()函式對原圖的BGR三通道進行分離
用三個指標分別指向傳入的兩個單通道影像和輸出的二值影像
假設src1是R(red)通道影像,src2是G(green)通道影像,由于split()函式進行通道分離時,得到b、g、r三個通道的像素值分別為[255,0,0]、[0,255,0]、[0,0,255],用r-b得到的像素值為[0,0,255](uchar型別沒有負值),在原始碼中若r-b>25,則在二值化影像中為白色,否則為黑色(也就是說此時二值化影像中白色區域對應原圖中的紅色區域,檢測出了紅燈),這樣經過二值化后,可以看到LED燈區域的輪廓
*/
void getDiffImage(Mat src1, Mat src2, Mat dst, int nThre)
{
omp_set_num_threads(8);
#pragma omp parallel for
for (int nI = 0; nI<src1.rows; nI++)
{
uchar* pchar1 = src1.ptr<uchar>(nI); ③
uchar* pchar2 = src2.ptr<uchar>(nI); ③
uchar* pchar3 = dst.ptr<uchar>(nI); ③
for (int nJ = 0; nJ <src1.cols; nJ++)
{
if (pchar1[nJ] - pchar2[nJ]> nThre) // ③
{
pchar3[nJ] = 255;
}
else ③
{
pchar3[nJ] = 0;
}
}
}
}
/*
⑩ 為了方便大家閱讀,我首先將該函式中用到的宏定義復制過來:#define T_ANGLE_THRE 10 #define T_SIZE_THRE 5;另外,如果大家無法理解我接下來的解釋,請先查看本文尾部的“armorDetect() 函式未呼叫的效果圖”和“armorDetect() 函式呼叫后需要達到的效果圖”,可以輔助理解
dAngle變數記錄任意兩個旋轉矩形的角度差的絕對值,代碼中的 if (180 - dAngle < T_ANGLE_THRE) armor.angle += 90; 陳述句解釋請查看本文尾部的“dAngle解釋圖”
原始碼中“判斷這兩個旋轉矩形是否是一個裝甲的兩個LED燈條”下方長長的if陳述句起到的功能以及作者想要提出的改進見本文尾部的“if陳述句思考圖”
用歐氏距離表示:裝甲的寬度等于兩側LED燈所在旋轉矩形中心坐標的距離
為避免寬高相反,添加判斷陳述句
*/
vector<RotatedRect> armorDetect(vector<RotatedRect> vEllipse)
{
vector<RotatedRect> vRlt;
RotatedRect armor; //定義裝甲區域的旋轉矩形
int nL, nW;
double dAngle;
vRlt.clear();
if (vEllipse.size() < 2) //如果檢測到的旋轉矩形個數小于2,則直接回傳
return vRlt;
for (unsigned int nI = 0; nI < vEllipse.size() - 1; nI++) //求任意兩個旋轉矩形的夾角
{
for (unsigned int nJ = nI + 1; nJ < vEllipse.size(); nJ++)
{
dAngle = abs(vEllipse[nI].angle - vEllipse[nJ].angle); ⑩
while (dAngle > 180)
dAngle -= 180; ⑩
//判斷這兩個旋轉矩形是否是一個裝甲的兩個LED燈條
if ((dAngle < T_ANGLE_THRE || 180 - dAngle < T_ANGLE_THRE) && abs(vEllipse[nI].size.height - vEllipse[nJ].size.height) < (vEllipse[nI].size.height + vEllipse[nJ].size.height) / T_SIZE_THRE && abs(vEllipse[nI].size.width - vEllipse[nJ].size.width) < (vEllipse[nI].size.width + vEllipse[nJ].size.width) / T_SIZE_THRE) ⑩
{
armor.center.x = (vEllipse[nI].center.x + vEllipse[nJ].center.x) / 2; //裝甲中心的x坐標
armor.center.y = (vEllipse[nI].center.y + vEllipse[nJ].center.y) / 2; //裝甲中心的y坐標
armor.angle = (vEllipse[nI].angle + vEllipse[nJ].angle) / 2; //裝甲所在旋轉矩形的旋轉角度
if (180 - dAngle < T_ANGLE_THRE)
armor.angle += 90;
nL = (vEllipse[nI].size.height + vEllipse[nJ].size.height) / 2; //裝甲的高度
nW = sqrt((vEllipse[nI].center.x - vEllipse[nJ].center.x) * (vEllipse[nI].center.x - vEllipse[nJ].center.x) + (vEllipse[nI].center.y - vEllipse[nJ].center.y) * (vEllipse[nI].center.y - vEllipse[nJ].center.y)); //裝甲的寬度等于兩側LED所在旋轉矩形中心坐標的距離 ⑩
if (nL < nW) ⑩
{
armor.size.height = nL;
armor.size.width = nW;
}
else ⑩
{
armor.size.height = nW;
armor.size.width = nL;
}
vRlt.push_back(armor); //將找出的裝甲的旋轉矩形保存到vector
}
}
}
return vRlt;
}
/*
⑩+① box.points 為旋轉矩形結構中旋轉矩形類的獲取旋轉矩形的四個頂點坐標函式,在這里作者提出改進思路:必須添加確保四個頂點0、1、2、3滿足順時針連續或逆時針連續規則的代碼,否則將得不到矩形,無法在當前影像中標出裝甲的位置
*/
void drawBox(RotatedRect box, Mat img)
{
Point2f pt[4];
int i;
for (i = 0; i<4; i++)
{
pt[i].x = 0;
pt[i].y = 0;
}
box.points(pt); //計算二維盒子頂點 ⑩+①
line(img, pt[0], pt[1], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[1], pt[2], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[2], pt[3], CV_RGB(0, 0, 255), 2, 8, 0);
line(img, pt[3], pt[0], CV_RGB(0, 0, 255), 2, 8, 0);
}
⑦ fitEllipse() 函式擬合效果圖:
⑩ armorDetect() 函式未呼叫的效果圖:

armorDetect() 函式呼叫后需要達到的效果圖:

dAngle解釋圖:

if陳述句思考圖:

備注:改進思路只是作者自己的思考,目前并沒有得到驗證,歡迎討論!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/295280.html
標籤:其他
