【Milk-V Duo 开发板免费体验】RoboMaster机器人装甲板的识别

感谢电子发烧友论坛和算能提供的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处理板的处理能力可能更适合摄像头采集并压缩传输的场景,对于视频检测这类的算法,如果实时性要求低的话,尚可一试。

本文转载自:https://bbs.elecfans.com/jishu_2366640_1_1.html,作者:zealsoft