基于OpenCV的数码管数字识别


Author:陈明聪

在Robomasters比赛中,神符(大龙BUFF)是要在1.5s时间内连续击打九宫格中随机手写数字,而目标数字则是由九宫格屏幕上方的数码管提供的,所以想要击打神符首先要做的就是识别数码管(不然惩戒都不带,打个皮皮虾大龙)。而且数码管字形的数字也广泛使用在电源、万用表以及各种仪器上,所以数码管数字识别可以应用在许多地方。


摄像头参数:300万像素,曝光-7,120FPS,640*480

一、 找到数码管数字

想要识别目标就必须找到目标,我们这里以最常见的红色数码管为例。先在原始图片中分离出红色通道,对红色取阈值(亮度大于200的并且红色亮度大于蓝色亮度的变为白色,其余为黑色),对处理完的图像取亮度阈值,留下亮度大于200的部分。这时候如果你的摄像头没有对着强光、背景没有洗剪吹广告牌,图像里基本只剩下数码管数字了。对,你没听错,只有数码管数字了!

代码如下:

 vector<Mat> channels;
    split(frame, channels);     //split color channel
    Mat Red = channels.at(2);
    Mat Blue = channels.at(0);
    for (int i = 0; i < Red.rows; i++){
        for (int j = 0; j < Red.cols; j++){
            if (Red.at<uchar>(i, j) > 200 && (Red.at<uchar>(i, j) > 1.1 * Blue.at<uchar>(i, j))){ //color threshold
                Red.at<uchar>(i, j) = 255;
            }
            else
                Red.at<uchar>(i, j) = 0;
        }
    }

    threshold(Red, Red, 200, 255, THRESH_BINARY); //brightness threshold
   
    imshow("split",Red);

然后提取出数字,先对数字做膨胀操作,方便识别到轮廓,紧接着识别轮廓,求出所有轮廓的最小包围矩形,根据矩形的面积、长宽比例就可以筛选出数码管数字了,最后根据轮廓的x坐标排序就可以得到目标了,当然还可以增加很多更鲁棒性的操作,我们在这里就不做赘述了,

这部分代码如下:

 morphologyEx(Red, Red, MORPH_DILATE, getStructuringElement(MORPH_RECT, Size(5, 5))); //dilate to get more obvious contour
         
    vector< vector<Point2i> > contours;
    vector<Vec4i> hierarchy;
    findContours(Red, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
    
    vector<vector<Point>> contours_poly(contours.size());
    vector<Rect> boundRect(contours.size());

    for (int i = 0; i < contours.size(); i++){
        approxPolyDP(Mat(contours[i]), contours_poly[i], 1, true);
        boundRect[i] = boundingRect(Mat(contours_poly[i]));
        double rectsize = boundRect[i].height * boundRect[i].width;
        double ratio = boundRect[i].height/boundRect[i].width;
        if (rectsize > 150 && rectsize < 1200)
        { //filter by area
            if(ratio>0.8&&ratio<2)
                targetRect.push_back(boundRect[i]);
        }
      std::sort(targetRect.begin(),targetRect.end(),[](const cv::Rect& t1,const cv::Rect& t2){return t1.x < t2.x;});
    morphologyEx(Red, Red, MORPH_ERODE, getStructuringElement(MORPH_RECT, Size(5, 5)));
    Red.copyTo(numbers);  
    if (targetRect.size() != 5)
        return false;
    return true;

二、 识别数字

识别数字我们采用的方法类似七段法,我们分别在数字的横向1/3、2/3和纵向1/2处画三条线,根据数字和线的交点位置和数量判断数字是几,是不是很简单。以6为例,交点数量为12(交点为边缘交点,看代码很容易理解),再根据交点位置就可以判断出数字,注意:数字1的情况比较特殊,需要单独考虑。

这里注意rowRange()(colRange())的用法,只包含左侧不含右侧。“画线”不是真正的画线,Mat.rowRange(int,int)返回指定行的图像,如colRange(singletubo.cols/2,singletubo.cols/2+1)返回的为singletubo图像的最中间一行,再根据行图像的每一列有没有发生颜色变化(由于二值化,只有黑白,所以只需检测相邻两列的差值的绝对值是否为255)判断有没有交点出现。

检测交点代码如下:

    int number =0;
    Mat row1 = singletubo.rowRange(singletubo.rows/3,singletubo.rows/3+1);
    Mat row2 = singletubo.rowRange(2*singletubo.rows/3,2*singletubo.rows/3+1);
    Mat col1 = singletubo.colRange(1*singletubo.cols/2,1*singletubo.cols/2+1);
    int row1_flag = 0;
    int row2_flag = 0;
    int col1_flag = 0;
    int flag=0;
    int row1_point[10], row2_point[10], col1_point[10];
    while(number==0){
    for(int i=0;i<row1.cols-1;i++){
        if(abs(row1.at<uchar>(0,i)-row1.at<uchar>(0,i+1))==255)
        {
            row1_point[row1_flag]=i;
            row1_flag++;
            flag++;
        }
    }

    for(int i=0;i<row2.cols-1;i++){
        if(abs(row2.at<uchar>(0,i)-row2.at<uchar>(0,i+1))==255)
        {
            row2_point[row2_flag]=i;
            row2_flag++;
            flag++;
        }
    }

    for(int i=0;i<col1.rows-1;i++){
        if(abs(col1.at<uchar>(i,0)-col1.at<uchar>(i+1,0))==255)
        {
            col1_point[col1_flag]=i;
            col1_flag++;
            flag++;
        }
    }
    if(singletubo.cols>15&&flag==6){
        number =7;
        return number;
    }

    else if(singletubo.cols<15){
        number =1;
        return number;
    }

    else if(flag==8){
        number =4;
        return number;
    }

    else if(flag==14){
        number =8;
        return number;
    }

    else if(flag==10){
        if(row1_point[0]>singletubo.cols/2&&row2_point[0]>singletubo.cols/2){
            number =3;
        }
        else if(row1_point[0]>singletubo.cols/2&&row2_point[0]<singletubo.cols/2){
            number =2;
        }
        else number =5;
        return number;
    }

    else if(flag==12){
        if(row2_point[0]<singletubo.cols/2)
            number =6;
        else number =9;
        return number;
    }
    else return 0;
    }

到这里就全部完成了,详细的数字判断代码没有贴出,都比较简单。这种方法对光线有一定要求,曝光过高或者强光直射摄像头在通道分离时会出现一些问题,但是对于日常环境还是可以适应的,对于数字判断的准确性在考虑的情况较为充足的情况下是比较准确的,当然对于这种一成不变,有固定特征的图形进行识别,机器学习方法应该是最好的选择。