前言
开源代码github网址:GitHub - xinyang-go/SJTU-RM-CV-2019: 上海交通大学 RoboMaster 2019赛季 视觉代码
这里着重分析主函数main.cpp与装甲板识别部分的工程文件armer文件夹。
由于未完全分析,所以对于其中log.h、options.h中的函数并不清楚,在分析时会跳过这些内容。
该文章重点分析的是思路,部分函数中的数学逻辑、数据处理思路这里不详细解释(没能力)
所有思维导图通过 知犀知犀思维导图 - 知犀官网 (zhixi.com) 原创
框架分析
首先看一下该文章要分析的部分
头文件
这里我们只分析寻找装甲板的armor_finder.h和分类器classifier.h,而分类器其实是为寻找装甲板服务的
classifier.h
- load_conv_w/b:加载卷积层
- 先以只读方式打开文件。如果打开失败则输出日志提示打开失败,然后将state设为false,返回一个未分配空间的result
- 如果成功,就根据文件中的参数进行操作,通过emplace_back将数据添加到result中,最后return result
- load_fc_w/b:加载全连接层
- 基本思路与卷积层相同,不同的是:如果文件打开失败,返回一个现创建的1x1零向量而不是动态vector
- relu:relu激活函数
- 第一个relu用了三元表达式实现,后面的重载使用了这个relu方法
- leaky_relu:relu函数的变形
- 用来防止训练过程中ReLu函数负半轴导致神经元死亡的问题
- 有参构造
- 这里用到了state,有参构造中会调用所有前面写的load_xxx_x方法来给私有属性赋值
- 如果最终state为true,说明参数加载成功
- calculate:
- 在operator中使用
- operator将图片RGB三通道值都转换到0-1之间,传给calculate计算
- 经过relu,平均池化,relu,平局池化,,relu后,将其压平到一维
- 通过wx+b算出y,使用relu,再算下一层,进行softmax
- 将最后得到的矩阵返回
- operator
- 通道拆分
- 所有通道值/255,进行归一化
- 通过calculate进行CNN处理,得到代表概率的一维向量
- 找到最大概率的行,如果概率大于0.5,认为就是该类型
如果最大概率小于0.5,认为匹配失败,返回0(筛)
armor_finder.h
这里面写了三个类,每个类都有着自己的属性与方法,最后功能都会集合到findArmorBox上
- 里面的灯条类和装甲板类都通过typedef给自己类型的vector容器起了别名。他们被广泛用于后续的函数中,借助引用来将函数处理后的数据储存起来
- 自瞄类中很多属性、方法与装甲板识别功能无关,所以流程图中省略了
源码
- classifier.cpp是为了实现classifier.h中的各项函数
- armor_finder.cpp最关键的作用就是实现了有参构造和自瞄类的核心方法run
- 其他.cpp文件都是在实现armor_finder.h中的某个或某些函数,他们大多都有一下规律:先创建一下属于自己的函数作为铺垫,然后再通过这些函数去实现armor_finder.h的函数
具体分析
函数调用情况
图中的几个函数是装甲板识别功能中极为重要的函数,也是接下来我们要分析的函数。图中展示了他们的调用关系方便理清逻辑
- main.cpp中调用run来实现自瞄功能
- run中当状态为SEARCHING_STATE时,会使用装甲板识别的功能。这时候他会先判断是否能从图片中搜索到装甲板、搜索到的是否与上次搜索到的是同一目标,只有判断成功才会进行下一步tracker对象的创建(进入追踪状态的基础)。而这个判断功能就交给stateSearchingTarget来处理
- stateSearchingTarget对上面两个问题做出判断,其中第一个问题:”是否能从图片中搜索到装甲板“就交给了findArmorBox来处理。他会告诉程序能否搜索到,如果搜索到了他还会把装甲板的信息传递过来,以便进行第二个问题的判断。后面装甲板的信息还会传递到run中用来追踪
- findArmorBox主要进行一下操作:寻找灯条,通过灯条得到候选装甲板,通过分类器来筛选。上面三步分别交给了图中的三个函数
这是对整个装甲板识别流程的简要说明,详细的内容会在下文一一阐述
main.cpp
刨除掉其他功能,其实关于寻找装甲板的功能就是通过run体现的
自瞄主函数run()
run中对于不同的机器状态会有不同的处理,这里我们只分析搜索状态下的流程,流程图如下:
- anti_switch_cnt防止乱切目标计数器在装甲板识别中未使用,所以不进行讨论
- run中关于装甲板识别的内容其实只有stateSearchingTarget,所以这里只放出流程图,对于具体细节实现不讨论
我们根据函数调用情况图了解到是findArmorBox调用了findLightBlobs,但是为了逻辑的通畅,我们先分析灯条识别功能,不再按照情况图从下往上分析
灯条识别
函数
- lw_rate:旋转矩形的长宽比
- 利用三元运算符,保证输出的比值>1
- areaRatio:轮廓面积和其最小外接矩形面积比
- 其中的Point类以前也用过,就是用来记录坐标点的。将轮廓的所有坐标点保存到vector容器中,这时候就可以认为这个容器代表了这个轮廓
- 直接调用contourArea得到轮廓面积
- isValidLightBlob:判断轮廓是否为一个灯条
- 分析判断条件
- A:旋转矩形的长宽比在1.2与10之间
- B:最小外接矩形面积<50 并且 轮廓面积和其最小外接矩形面积之比>0.4
- C:最小外接矩形面积>=50 并且 轮廓面积和其最小外接矩形面积之比>0.6
- A && ( B || C)
- 判断方式的原理
- A是对灯条长宽比的基本数据限定,灯条什么角度都基本上是个长方形,而且比值不可能超过10
- 轮廓面积和其最小外接矩形面积之比 其实就是灯条识别图像与外接矩形的契合度
- 面积<50说明灯条比较远,那么杂光的干扰会大一些,灯条外形特征不明显,契合度就会相对较低
- 同理C为较近的情况,契合度理应更高,所以要求的最小值就会高一些
- 分析判断条件
- get_blob_color:判断灯条的颜色
- 整体思路是通过比较图片中红蓝的比例来判断灯条颜色(只看红蓝是因为灯条只可能是这两个颜色)
- isSameBlob:判断两个灯条区域是否为同一个灯条
- 如果两个灯条外接矩形的中心点坐标满足:(x坐标差值的平方+y坐标差值的平方)<9,则认为是同一个灯条
- imagePreProcess:开闭运算
- 其中用的都是3x5的矩阵卷积核
- 依次对图像进行腐蚀,膨胀,膨胀,腐蚀
findLightBlobs
通道拆分
- 通道拆分为RGB三个,根据敌人的颜色选择使用哪个颜色的通道
- 根据敌人颜色设置light_threshold(二值化的阈值)的值
二值化处理
-
方式一:对图像进行二值化处理,大于阈值(light_threshold)部分设置为255,小于部分设置为0,如果处理后图像为空返回false.对处理后图片进行一次开闭运算(imagePreProcess函数)
-
方式二:对图像以140为阈值进行一次二值化处理,然后一次开闭运算
使用两个不同的二值化阈值同时进行灯条提取,减少环境光照对二值化这个操作的影响。
同时剔除重复的灯条,剔除冗余计算,即对两次找出来的灯条取交集。
取交集
- 找两次处理的轮廓,检索模式为CV_RETR_CCOMP;定义轮廓的近似方法为CV_CHAIN_APPROX_NONE
- CV_RETR_CCOMP: 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
- CV_CHAIN_APPROX_NONE:保存物体边界上所有连续的轮廓点到contours向量内
- 分别给两个灯条容器对象(利用了
typedef std::vector\<LightBlob> LightBlobs
)添加内容:最小外接矩形,轮廓面积和最小外接矩阵面积之比,灯条的颜色 - 最后通过一系列取交集操作判断是否找到灯条(灯条数大于等于2),同时灯条的数据都存在了light_blobs这个LightBlobs类型的vectorr中
与灯条匹配成装甲板
matchArmorBoxes
- 清空ArmorBox类型的vector容器
- 读取light_blobs(里面存储着灯条的数据)的内容,这里的嵌套for循环是为了将所有灯条进行两两配对处理
- 先判断两个灯条是否匹配,不配对则继续
- 将两个灯条的矩形一个作为左矩形一个作为右矩形
- 通过一系列计算处理,如果一系列数据限制条件都通过,则将这个候选装甲板的坐标,宽高,两个相应的灯条数据,敌人颜色都存入armor_boxes这个容器中
findArmorBox
- 创建light_blobs,armor_boxes两个容器,初始化box对象
- 调用findLightBlobs函数,将找到的所有可能的灯条存到bight_blobs容器中
- 调用matchArmorBoxes函数,得到装甲板候选区
- 使用分类器classifier对装甲板候选去进行筛选(概率小于0.5的都会被筛选掉)
- 按照优先级对装甲板进行排序,选择优先级最大的作为结果(id不能为0),如果结果是空,返回false。如果分类器不可用就默认选择候选区第一个区域作为目标
上交的灯条识别与自己灯条识别的区别
C++学习记录
C++新知识
数值计算库Eigen
参考:
Matrix: (16条消息) Eigen教程2----MatrixXd和VectorXd的用法_MaybeTnT的博客-CSDN博客_eigen::matrixxd
.colise: (16条消息) Eigen初学相关介绍_小白要努力的博客-CSDN博客_colwise函数
maxCoeff()与索引:(16条消息) Eigen库使用之矩阵的最大/小值及其位置_Chen-Sh的博客-CSDN博客_maxcoeff
-
Marix类:在Eigen,所有的矩阵和向量都是Matrix模板类的对象,Vector只是一种特殊的矩阵(一行或者一列)
-
MatrixXd:Matrix的一种构造函数,X代表动态,d代表double类型,也就是说生成一个类型的double类型的动态大小的矩阵
- Matrix3f意味着生成一个3x3的类型为float的矩阵
- MatrixXd f(row, col)意味着f是一个类型的double的row*col矩阵(注意不是int类型,int是MatrixXi)
-
VectorXd:与MatrixXd同理,生成一个类型的double类型的动态大小的向量
-
.colwise:Mat.colwise()理解为分别去看矩阵的每一列,然后再作用maxCoeff()函数,即求每一列的最大值。
需要注意的是,colwise返回的是一个行向量(列方向降维),rowwise返回的是一个列向量(行方向降维)。
-
- 先通过calculate方法得到矩阵result
- 设置最小值的索引
- 通过.maxCoeff计算矩阵中的最大值最小值,这时得到了最大值的坐标
- 如果最大值>0.5返回行坐标,反之返回0
容器知识
参考:
emplace_back:(16条消息) C++的emplace_back函数介绍_Jason_Lee155的博客-CSDN博客_c++ emplace_back
- emplace_back:传统对vector进行尾插入都是使用push_back,但是其中有很多冗余的计算。而C++11引入了emplace_back函数,它通过完美转发实现了在vector中插入时直接在容器内构造对象,省略了创建临时对象的操作
文件读写函数
- fscanf:按“格式字符串”所指定的格式,从“文件类型指针”所指向的文件的当前位置读取数据,然后按“输入项地址表列”的顺序,将读取来的数据存入指定的内存单元中。
- 以
fscanf(fp,"%d,%f",&i,&t)
为例,意为从指针fp所指向的文件中以%d,%f格式读取两个值,分别存储在地址&i,&t的内存中
- 以
OOP知识
参考:
=default:(16条消息) C++ =default_c++ default_TABE_的博客-CSDN博客
转换运算符:关于c ++:“ operator bool()const”是什么意思 | 码农家园 (codenong.com)
构造函数冒号语法:(16条消息) C++子类的构造函数后面加:冒号的作用_lusirking的博客-CSDN博客_c++ 构造函数 冒号
- default 函数。程序员只需在函数声明后加上=default,就可将该函数声明为 default 函数,编译器将为显式声明的default函数自动生成函数体。
- operator bool()const:当使用时可以将对象转化为bool类型
- 该源码为第二种使用场景:对类成员进行初始化。正常写法也可以做到,但是这样会更加简便
其他
参考:
typedef:(16条消息) C++ typedef详解_jupeiii的博客-CSDN博客_c++ typedef
enum:(16条消息) C++枚举enum使用详解_赵大宝字的博客-CSDN博客_c++ enum
extern:(7条消息) C++中的extern_老胡写代码的博客-CSDN博客_c++ extern
auto类型:(7条消息) C++ auto类型总结_emper丶z的博客-CSDN博客_c++ auto
.erase:C++中erase函数的使用,可以用来删除内存擦除 - 初见不如不相见 - 博客园 (cnblogs.com)
- fmax,fmin是c语言中用来快速比较两数大小的函数,会返回两个浮点数中更x的那个
- auto类型:可以在声明变量时自动推断被声明变量的类型,更适用于类型冗长复杂、变量使用范围专一时,使程序更清晰易读
- .erase:可以用来更新(删除内容)迭代器
OpenCV新知识
参考:
getStructuringElement():(7条消息) getStructuringElement函数以及开、闭、腐蚀、膨胀原理讲解_SerendipityMIT的博客-CSDN博客
- RotatedRect是一个存储平面上旋转矩形的类,通常用来存储最小外包矩形函数minAreaRect( )和椭圆拟合函数fitEllipse( )返回的结果。以前给灯条找最小外接矩阵的时候用过一次
- .boundingRect():轮廓拟合函数中的一个,能够返回包围轮廓的矩形的边界信息。
- getStructuringElement():返回一个结构元素(卷积核)。第一个参数会决定卷积核的形状,源码中imagePreProcess方法使用的MORPH_RECT会使函数返回矩形卷积核
收获
- 如果一个函数作用是信息处理,那么一般返回值为bool,表示这个函数是否成功处理了数据。处理后的数据不作为返回值,而是通过参数中的引用直接存放到变量(容器)中。就比如findLightBlobs这个函数,通过容器是否为空来作为bool类型的返回,通过函数得到的灯条信息在函数体中已经存放到了容器中(容器参数利用了引用)。