RoboMaster视觉教程(9)风车能量机关识别2

之前说能量机关的教程有很多了打算不写了,但是总有同学来问,想了想还是写一下吧。

风车能量机关我只做了识别,因为准备分区赛的时候没有实物可以测试就一直搁置了,之后复活赛视觉的打击和预测都是学弟们做的。所以如果问我预测方面的事我也只能给个大概的方向,因为毕业后就没有再做这个了。

风车能量机关识别的示例代码我放在了我的GitHub

有需要的可以自行下载,觉得有帮助的话可以点个赞给项目加个星。

图像预处理

和之前识别装甲板一样,首先需要把我们要的颜色保留不要的去除。通过红蓝颜色通道互减可以方便的得到符合条件的二值图。

1
2
3
4
5
6
7
8
9
10
//分割颜色通道
vector<Mat> imgChannels;
split(srcImage,imgChannels);
//获得目标颜色图像的二值图
#ifdef RED
Mat midImage=imgChannels.at(2)-imgChannels.at(0);
#endif
#ifndef RED
Mat midImage=imgChannels.at(0)-imgChannels.at(2);
#endif

这里说一下在 opencv c++ 中 Mat 之间是可以直接用 - 号相减的,而且如果减完后对应的像素点是负值的话会设为0,而在python中用 - 减完后如果像素值为负不会归零,而是类似于溢出的那种效果,也就是会变成255加上那个负数。在 c++ 中不会出现这样的问题。

1
2
3
//二值化,背景为黑色,图案为白色
//用于查找扇叶
threshold(midImage2,midImage2,100,255,CV_THRESH_BINARY);

在灰度图上进行二值化,得到的图片效果:

可以看到箭头之间有空隙,如果直接进行轮廓查找的话会得到许多小轮廓而不能把整个作为一个轮廓,所以需要对图片进行膨胀操作。腐蚀和膨胀都是针对于白色区域而言的,腐蚀就是白色区域减少,膨胀就是白色区域增加。

1
2
3
4
5
//膨胀
int structElementSize=2;
Mat element=getStructuringElement(MORPH_RECT,Size(2*structElementSize+1,2*structElementSize+1),Point(structElementSize,structElementSize));

dilate(midImage2,midImage2,element);

膨胀后可以看到箭头都连在一起了:

之后还要进行形态学闭运算,以去除白色区域中可能出现的小洞干扰轮廓查找。

1
2
3
4
5
6
//闭运算,消除扇叶上可能存在的小洞
structElementSize=3;

element=getStructuringElement(MORPH_RECT,Size(2*structElementSize+1,2*structElementSize+1),Point(structElementSize,structElementSize));

morphologyEx(midImage2,midImage2, MORPH_CLOSE, element);

图像预处理到这里就完成了。

风车扇叶识别

风车扇叶的识别是通过轮廓查找完成的。

1
2
3
4
5
6
7
8
//查找轮廓
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;

findContours(midImage2,contours2,hierarchy2,RETR_CCOMP,CHAIN_APPROX_SIMPLE);

RotatedRect rect_tmp2;
bool findTarget=0;

在查找轮廓前首先要判断轮廓是不是为空。之后再开始轮廓查找。

1
2
3
4
//遍历轮廓
if(hierarchy2.size())
for(int i=0;i>=0;i=hierarchy2[i][0])
{

hierarchy包含了轮廓的拓扑结构, hierarchy[i][0]~hierarchy[i][3] 中, 0代表与当前轮廓平级的后一个轮廓的的索引编号1代表与当前轮廓平级的前一个轮廓的索引编号2代表当前轮廓的子轮廓的索引编号3代表当前轮廓的父轮廓的索引编号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
hierarchy Optional output vector (e.g. std::vector<cv::Vec4i>), containing information about the image topology. It has as many elements as the number of contours.

For each i-th contour contours[i], the elements hierarchy[i][0] , hierarchy[i][1] , hierarchy[i][2] , and hierarchy[i][3] are set to 0-based indices in contours of the next and previous contours at the same hierarchical level, the first child contour and the parent contour, respectively.

If for the contour i there are no next, previous,parent, or nested contours, the corresponding elements of hierarchy[i] will be negative.
```
所以遍历轮廓的语句会写成

`for(int i=0 ; i>=0 ; i=hierarchy2[i][0])`

`hierarchy` 的大小和 `contours` 的大小一样,所以若其大小为零说明没有轮廓也就不能遍历了,遍历会报错。
```cpp
//找出轮廓的最小外接矩形
rect_tmp2=minAreaRect(contours2[i]);
Point2f P[4];
//将矩形的四个点保存在P
rect_tmp2.points(P);

//为透视变换做准备
Point2f srcRect[3];
Point2f dstRect[3];

double width;
double height;

//矫正提取的叶片的宽高
width=getDistance(P[0],P[1]);
height=getDistance(P[1],P[2]);
if(width>height)
{
srcRect[0]=P[0];
srcRect[1]=P[1];
srcRect[2]=P[2];
}
else
{
swap(width,height);
srcRect[0]=P[1];
srcRect[1]=P[2];
srcRect[2]=P[3];
}

这一步对每个轮廓的宽高进行处理使宽大于高,也就是在透视变换后将是一个不扭曲的长方形,如果不进行这一步可能会得到图像很胖的长方形。

1
2
3
//通过面积筛选
double area=height*width;
if(area>5000){

求得矩形的面积把小轮廓筛除。

1
2
3
4
5
6
7
8
9
10
11
12
dstRect[0]=Point2f(0,0);
dstRect[1]=Point2f(width,0);
dstRect[2]=Point2f(width,height);
//透视变换,矫正成规则矩形
Mat warp_mat=getAffineTransform(srcRect,dstRect);
Mat warp_dst_map;

warpAffine(midImage2,warp_dst_map,warp_mat,warp_dst_map.size());

// 提取扇叶图片
Mat testim;
testim = warp_dst_map(Rect(0,0,width,height));

透视变换得到需要的长方形。

待打击的扇叶(锤子) 已经被击中过的扇叶(宝剑)

接下来就是对这个长方形进行识别,在代码中我用了svm和模板匹配两种方法,效果差不多。

模板匹配法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
cv::Point matchLoc;
double value;
Mat tmp1;
//统一大小,模板匹配中被匹配图像需要大于等于模板大小
//当模板和被匹配物体大小相近时匹配效果最好
resize(testim,tmp1,Size(42,20));
//用于保存匹配得分
vector<double> Vvalue1;//识别待打击的扇叶
vector<double> Vvalue2;//识别已经打击过的扇叶
for(int j=1;j<=6;j++)
{
value = TemplateMatch(tmp1, templ[j], matchLoc, CV_TM_CCOEFF_NORMED);


Vvalue1.push_back(value);

}
for(int j=7;j<=8;j++)
{
value = TemplateMatch(tmp1, templ[j], matchLoc, CV_TM_CCOEFF_NORMED);


Vvalue2.push_back(value);

}
int maxv1=0,maxv2=0;
//找出匹配值最大的序号
for(int t1=0;t1<6;t1++)
{
if(Vvalue1[t1]>Vvalue1[maxv1])
{
maxv1=t1;
}
}
for(int t2=0;t2<2;t2++)
{
if(Vvalue2[t2]>Vvalue2[maxv2])
{
maxv2=t2;
}
}
if(Vvalue1[maxv1]>Vvalue2[maxv2]&&Vvalue1[maxv1]>0.6)
{

匹配后需要根据得分来预测是否是要打击的扇叶。

如通过svm来识别扇叶,则首先要把得到的扇叶图像转化成svm所需的向量形式

1
Mat test=get(testim);

再根据预测得分来进行判断

1
2
if(svm->predict(test)>=0.9)
{

装甲板识别

要打击的装甲板一定在之前识别的扇叶中,也就是一定是扇叶的子轮廓,所以我们只要对子轮廓的几何特征进行判断就能得到装甲板的位置。

在这一步需要之前预处理时把扇叶上可能出现的小洞消除掉,因为小洞也算是子轮廓,若带有小洞的话处理起来就会麻烦很多,这里对出现小洞的情况直接忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
   findTarget=true;
//查找装甲板
//扇叶轮廓的子轮廓就是装甲板所在的轮廓
if(hierarchy2[i][2]>0)
{
RotatedRect rect_tmp = minAreaRect(contours2[hierarchy2[i][2]]);



Point2f Pnt[4];
rect_tmp.points(Pnt);
//用于筛选装甲板
const float maxHWRatio=0.7153846;
const float maxArea=2000;
const float minArea=500;

float width=rect_tmp.size.width;
float height=rect_tmp.size.height;
if(height>width)
swap(height,width);
float area=width*height;
//筛选
if(height/width>maxHWRatio||area>maxArea ||area<minArea){
continue;
}
Point centerP=rect_tmp.center;
//打击点
circle(srcImage,centerP,1,Scalar(0,0,255),1);
//画出装甲位置
for(int j=0;j<4;++j)
{
line(srcImage,Pnt[j],Pnt[(j+1)%4],Scalar(0,255,255),2);
}
}
}

风车的定位

风车的定位有两种思路,一种是通过识别到的装甲位置来拟合圆另一种是通过识别中间的R来确定位置,两种都在官方的圆桌中提到了具体可看:

RM圆桌008 | 如何击打大风车

因为在准备分区赛时没有实物,我就写了个简单的拟合圆的算法,更好的方法是识别R不过我没有做(识别R也很简单有很多方法)。

这里讲一下拟合圆的方法吧,拟合的代码是参考CSDN上一位博主博客的内容

最小二乘法拟合圆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//在得到装甲板中心点后将其放入缓存队列中
circle(drawcircle,centerP,1,Scalar(0,0,255),1);
//用于拟合圆,用30个点拟合圆
if(cirV.size()<30)
{
cirV.push_back(centerP);
}
else
{
float R;
//得到拟合的圆心
CircleInfo2(cirV,cc,R);
circle(drawcircle,cc,1,Scalar(255,0,0),2);
cirV.erase(cirV.begin());

}
//将打击点围绕圆心旋转某一角度得到预测的打击点
if(cc.x!=0&&cc.y!=0){
//得到旋转一定角度(这里是30度)后点的位置
Mat rot_mat=getRotationMatrix2D(cc,30,1);
float sinA=rot_mat.at<double>(0,1);//sin(30);
float cosA=rot_mat.at<double>(0,0);//cos(30);
float xx=-(cc.x-centerP.x);
float yy=-(cc.y-centerP.y);
Point2f resPoint=Point2f(cc.x+cosA*xx-sinA*yy,cc.y+sinA*xx+cosA*yy);
circle(srcImage,resPoint,1,Scalar(0,255,0),10);
}

最终的效果图如下图:

风车能量机关识别算法讲完了!觉得不错点个赞呗^_^

欢迎关注公众号 江达小记

江达小记