感谢电子发烧友论坛和算能提供的Milk-V Duo 开发板试用机会。
上次我们介绍了OpenCV图像处理库的移植,这次我们尝试将RoboMaster机器人装甲板识别的程序移植到板子上,测试开发板的图像处理能力。
下图是装上装甲板后的RoboMaster步兵轴测图。装甲板竖直固定在小车的四周,同一装甲板两灯条平行、灯条长宽确定、两灯条间的间距确定。
装甲板识别的算法已经非常成熟,其基本思想是先利用阈值分割、膨胀等算法对图像中的灯条进行识别,再根据长宽比、面积大小和凸度来筛选灯条,对找出的灯条进行匹配找到合适的配对,并将配对灯条作为候选装甲板,提取其中间的图案判断是否是数字进行筛选。详细的介绍可以参考:https://blog.csdn.net/u010750137/article/details/96428059。
Milk-V Duo处理板的CPU主频达到1GHz,但是其RAM只有64MB,远少于一般Linux系统,所以在移植程序时必须非常小心内存的占用。我们的程序每次仅处理一帧图像,而且在处理过程中尽量减少对图像的Clone,处理的内存稍微多一点就可能导致程序失败。
在视频处理方面,我们没有采用OpenCV的VideoCapture和VideoWrite类,这两个类的处理都非常耗用内存,再加上我们的视频分辨率较高(1280×1024),处理不了几帧就会出错。我们采用的是将视频以图片序列进行存储,这样可以保障每次处理的内存降到最少。
核心的代码如下:
#include<iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/imgproc/types_c.h>
#include<vector>
#include "ArmorParam.h"
#include "ArmorDescriptor.h"
#include "LightDescriptor.h"
#include <sys/time.h>
using namespace std;
using namespace cv;
template<typename T>
float distance(const cv::Point_<T>& pt1, const cv::Point_<T>& pt2)
{
return std::sqrt(std::pow((pt1.x - pt2.x), 2) + std::pow((pt1.y - pt2.y), 2));
}
class ArmorDetector
{
public:
//初始化各个参数和我方颜色
void init(int selfColor){
if(selfColor == RED){
_enemy_color = BLUE;
_self_color = RED;
}
}
void loadImg(Mat& img ){
_srcImg = img;
Rect imgBound = Rect(cv::Point(50, 50), Point(_srcImg.cols - 50, _srcImg.rows- 50) );
_roi = imgBound;
_roiImg = _srcImg(_roi).clone();//注意一下,对_srcImg进行roi裁剪之后,原点坐标也会移动到裁剪后图片的左上角
}
//识别装甲板的主程序,
int detect(){
//颜色分离
_grayImg = separateColors(); // 用颜色判断敌我,可以不用
int brightness_threshold = 120;//设置阈值,取决于你的曝光度
Mat binBrightImg;
//阈值化,只保留了亮的部分
threshold(_grayImg, binBrightImg, brightness_threshold, 255, cv::THRESH_BINARY);
//cout << "thresh" << endl;
//膨胀
Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));
dilate(binBrightImg, binBrightImg, element);
//找轮廓
vector<vector<Point> > lightContours;
findContours(binBrightImg.clone(), lightContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
//////debug/////
_debugImg = _roiImg.clone();
for(size_t i = 0; i < lightContours.size(); i++){
drawContours(_debugImg,lightContours, i, Scalar(0,0,255), 3, 8);
}
////////////////
//筛选灯条
vector<LightDescriptor> lightInfos;
filterContours(lightContours, lightInfos);
//没找到灯条就返回没找到
if(lightInfos.empty()){
return -1;
}
//debug 绘制灯条轮廓
drawLightInfo(lightInfos);
//匹配装甲板
_armors = matchArmor(lightInfos);
if(_armors.empty()){
return -1;
}
//绘制装甲板区域
for(size_t i = 0; i < _armors.size(); i++){
vector<Point2i> points;
for(int j = 0; j < 4; j++){
points.push_back(Point(static_cast<int>(_armors[i].vertex[j].x), static_cast<int>(_armors[i].vertex[j].y)));
}
polylines(_debugImg, points, true, Scalar(0, 255, 0), 3, 8, 0);//绘制两个不填充的多边形
}
return 0;
}
//分离色彩,提取我们需要(也就是敌人)的颜色,返回灰度图
Mat separateColors(){
vector<Mat> channels;
// 把一个3通道图像转换成3个单通道图像
split(_roiImg,channels);//分离色彩通道
Mat grayImg;
//剔除我们不想要的颜色
//对于图像中红色的物体来说,其rgb分量中r的值最大,g和b在理想情况下应该是0,同理蓝色物体的b分量应该最大,将不想要的颜色减去,剩下的就是我们想要的颜色
if(_enemy_color==RED){
grayImg=channels.at(2)-channels.at(0);//R-B
}
else{
grayImg=channels.at(0)-channels.at(2);//B-R
}
return grayImg;
}
//筛选符合条件的轮廓
//输入存储轮廓的矩阵,返回存储灯条信息的矩阵
void filterContours(vector<vector<Point> >& lightContours, vector<LightDescriptor>& lightInfos){
for(const auto& contour : lightContours){
//得到面积
float lightContourArea = contourArea(contour);
//面积太小的不要
if(lightContourArea < _param.light_min_area) continue;
//椭圆拟合区域得到外接矩形
RotatedRect lightRec = fitEllipse(contour);
//矫正灯条的角度,将其约束为-45~45
adjustRec(lightRec);
//宽高比、凸度筛选灯条 注:凸度=轮廓面积/外接矩形面积
if(lightRec.size.width / lightRec.size.height >_param.light_max_ratio ||
lightContourArea / lightRec.size.area() <_param.light_contour_min_solidity)
continue;
//对灯条范围适当扩大
lightRec.size.width *= _param.light_color_detect_extend_ratio;
lightRec.size.height *= _param.light_color_detect_extend_ratio;
//因为颜色通道相减后己方灯条直接过滤,不需要判断颜色了,可以直接将灯条保存
lightInfos.push_back(LightDescriptor(lightRec));
}
}
//绘制旋转矩形
void drawLightInfo(vector<LightDescriptor>& LD){
//cout << "enter" << endl;
// _debugImg = _roiImg.clone();
vector<std::vector<cv::Point> > cons;
int i = 0;
for(auto &lightinfo: LD){
RotatedRect rotate = lightinfo.rec();
auto vertices = new cv::Point2f[4];
rotate.points(vertices);
vector<Point> con;
for(int i = 0; i < 4; i++){
con.push_back(vertices[i]);
}
cons.push_back(con);
drawContours(_debugImg, cons, i, Scalar(0,255,255), 3, 8);
i++;
}
//cout << "exit" << endl;
}
//匹配灯条,筛选出装甲板
vector<ArmorDescriptor> matchArmor(vector<LightDescriptor>& lightInfos){
vector<ArmorDescriptor> armors;
//按灯条中心x从小到大排序
sort(lightInfos.begin(), lightInfos.end(), [](const LightDescriptor& ld1, const LightDescriptor& ld2){
//Lambda函数,作为sort的cmp函数
return ld1.center.x < ld2.center.x;
});
for(size_t i = 0; i < lightInfos.size(); i++){
//遍历所有灯条进行匹配
for(size_t j = i + 1; (j < lightInfos.size()); j++){
const LightDescriptor& leftLight = lightInfos[i];
const LightDescriptor& rightLight = lightInfos[j];
//角差
float angleDiff_ = abs(leftLight.angle - rightLight.angle);
//长度差比率
float LenDiff_ratio = abs(leftLight.length - rightLight.length) / max(leftLight.length, rightLight.length);
//筛选
if(angleDiff_ > _param.light_max_angle_diff_ ||
LenDiff_ratio > _param.light_max_height_diff_ratio_){
continue;
}
//左右灯条相距距离
float dis = distance(leftLight.center, rightLight.center);
//左右灯条长度的平均值
float meanLen = (leftLight.length + rightLight.length) / 2;
//左右灯条中心点y的差值
float yDiff = abs(leftLight.center.y - rightLight.center.y);
//y差比率
float yDiff_ratio = yDiff / meanLen;
//左右灯条中心点x的差值
float xDiff = abs(leftLight.center.x - rightLight.center.x);
//x差比率
float xDiff_ratio = xDiff / meanLen;
//相距距离与灯条长度比值
float ratio = dis / meanLen;
//筛选
int cnt = 0;
cnt++;
// cout << cnt << "times try:\n" << "yDiff_ratio: " << yDiff_ratio << "\nxDiff_ratio: " << xDiff_ratio << "\nratio: " << ratio << endl;
if(yDiff_ratio > _param.light_max_y_diff_ratio_ ||
xDiff_ratio < _param.light_min_x_diff_ratio_ ||
ratio > _param.armor_max_aspect_ratio_ ||
ratio < _param.armor_min_aspect_ratio_){
continue;
}
//按比值来确定大小装甲
int armorType = ratio > _param.armor_big_armor_ratio ? BIG_ARMOR : SMALL_ARMOR;
// 计算旋转得分
float ratiOff = (armorType == BIG_ARMOR) ? max(_param.armor_big_armor_ratio - ratio, float(0)) : max(_param.armor_small_armor_ratio - ratio, float(0));
float yOff = yDiff / meanLen;
float rotationScore = -(ratiOff * ratiOff + yOff * yOff);
//得到匹配的装甲板
ArmorDescriptor armor(leftLight, rightLight, armorType, _grayImg, rotationScore, _param);
armors.emplace_back(armor);
break;
}
}
return armors;
}
void adjustRec(cv::RotatedRect& rec)
{
using std::swap;
float& width = rec.size.width;
float& height = rec.size.height;
float& angle = rec.angle;
while(angle >= 90.0) angle -= 180.0;
while(angle < -90.0) angle += 180.0;
if(angle >= 45.0)
{
swap(width, height);
angle -= 90.0;
}
else if(angle < -45.0)
{
swap(width, height);
angle += 90.0;
}
}
cv::Mat _debugImg;
private:
int _enemy_color;
int _self_color;
cv::Rect _roi; //ROI区域
cv::Mat _srcImg; //载入的图片保存于该成员变量中
cv::Mat _roiImg; //从上一帧获得的ROI区域
cv::Mat _grayImg; //ROI区域的灰度图
vector<ArmorDescriptor> _armors;
ArmorParam _param;
};
int main(int argc, char *argv[])
{
std::string img_folder = "/media/user/png/"; // 图像序列所在的文件夹路径
std::string out_folder = "/media/user/output/"; // 图像序列所在的文件夹路径
// capture.open("/media/user/png/output_%2d.png", CAP_IMAGES );
//从设置帧开始
long frameToStart = 1;
cout << "从第" << frameToStart << "帧开始读" << endl;
int frameTostop = 100;
if (frameTostop < frameToStart)
{
cout << "结束帧小于开始帧,错误" << endl;
}
int count = 0;
Mat img;
int i = frameToStart;
struct timeval start, end;
double elapsed_time;
while(i < frameTostop)
{
Mat frame;
count++;
std::string img_path = img_folder + "output_" + std::to_string(i) + ".png";
img = imread(img_path );
if (img.empty())
{
std::cerr << "无法读取图像文件 " << img_path << std::endl;
return -1;
}
gettimeofday(&start, NULL); // 记录开始时间
ArmorDetector detector;
detector.init(RED);
detector.loadImg(img);
// cout << "detect" << endl;
detector.detect();
gettimeofday(&end, NULL); // 记录结束时间
elapsed_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000000.0;
img_path = out_folder + "result_" + std::to_string(i) + ".png";
imwrite(img_path, detector._debugImg);
i++;
cout << count << ":" << elapsed_time << endl;
}
}
测试结果视频见B站:https://www.bilibili.com/video/BV1gz4y1t7QB/。
我们采用的原始视频是每秒12帧。在处理过程中,我们打印输出了每帧的处理时间:
[root@milkv]/media/user# ./armor
从第1帧开始读
1:0.304027
2:0.313252
3:0.314704
4:0.407992
5:0.357159
6:0.324584
7:0.322873
8:0.333659
9:0.312911
10:0.313014
11:0.318744
12:0.574995
13:0.366383
14:0.311588
15:0.414218
16:0.354194
17:0.33607
18:0.314957
19:0.501529
20:0.364166
21:0.394598
22:0.44688
从处理结果看,视频中装甲板的检测效果和电脑上并没有差别,但是处理速度明显较慢,大概每秒3帧的样子,不能达到实时检测的要求。Milk-V Duo处理板的处理能力可能更适合摄像头采集并压缩传输的场景,对于视频检测这类的算法,如果实时性要求低的话,尚可一试。
更多回帖