基于OpenCV的数码管数字识别

利用OpenCV可实现工业仪表设备的读数识别。仪表一般可分为两:数字式仪表和指针式仪表,本博文主要介绍一下数字式仪表识别的关键技术。下图是用软件模拟的数码管图片,本文识别的也就是图中的数字。待识别数字

一、图像定位
在实际的应用场景中,拍摄到的仪表区域很有可能会包含多余的背景部分,一个比较简单的解决方法是在拍摄时先行设定一个边界区域,提醒拍摄者将待识别的内容限制在区域中。后期识别时直接提取边界区域内的信息进行识别。

二、图像预处理
图像预处理的内容包括灰度化、二值化、腐蚀(或膨胀)、轮廓提取以及数字分割等。

1.灰度化
灰度化的目的是将图片从RGB的格式转为单通道,像素值为~255范围内的灰度图。

#define picture   "test4.png"  // filepath 
...
Mat image_org = imread(picture, IMREAD_COLOR);
imshow("image_org", image_org);  // read RGB image
Mat image_gry = imread(picture, IMREAD_GRAYSCALE);
if (image_gry.empty()) // read RGB image
    return -1;
imshow("image_gry", image_gry);

如下图所示:
这里写图片描述

2.二值化
二值化操作将灰度图变为像素值为0或者255的二值化图像,阈值可以根据图片的实际需求设定,要求是能将背景和数字分开。

Mat image_bin;
threshold(image_gry, image_bin, 50, 255, THRESH_BINARY); // convert to binary image
imshow("image_bin", image_bin);

二值化效果如下,threshold()函数中第二个形参选取的是THRESH_BINARY,因此图像变成黑底白字的效果。注意:此时图片背景文字的颜色直接影响后期的处理。
此形参的取值详见: cv::ThresholdTypes
这里写图片描述

3.腐蚀/膨胀
数字式仪表大部分采用八段式数码管,因此数字是不连续的。因此,在数字分割提取之前需要采取一定的操作使得数字的笔画连接起来,以防止数字被割裂而无法识别。腐蚀膨胀操作就可以解决这个问题。需要注意的是,腐蚀膨胀是对于白色部分而言的,膨胀就是图像中的高亮部分进行膨胀,“领域扩张”,效果图拥有比原图更大的高亮区域。腐蚀就是原图中的高亮部分被腐蚀,“领域被蚕食”,效果图拥有比原图更小的高亮区域。
现在的字体是白色的,如果想要让字体连续,就需要进行膨胀。 但也不能过度膨胀,否则会使得相邻数字连接起来,无法分割。

Mat image_dil;
Mat element = getStructuringElement(MORPH_RECT, Size(20, 20)); // 膨胀
dilate(image_bin, image_dil, element);
imshow("image_dil", image_dil);

膨胀的效果如下:
这里写图片描述

4.轮廓提取
每个数字连通后,即可进行轮廓提取,找个每个数字的轮廓位置信息。轮廓信息都存储在contours_out中。然后根据轮廓拟合成矩形轮廓。但是注意位置信息存储的顺序不是按照实际的坐标位置存储的,需要重新排序。本文是根据轮廓所在列信息(x)进行重排。

vector<vector<Point> > contours_out;
vector<Vec4i> hierarchy;
findContours(image_dil, contours_out, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);  
// re-arrange location according to the real position in the original image 
const size_t size = contours_out.size();
vector<Rect> num_location;
for (int i = 0; i < contours_out.size(); i++)
{    
    num_location.push_back(boundingRect(Mat(contours_out[i])) );// 转换为矩形轮廓
}
sort(num_location.begin(), num_location.end(), cmp); // 重排轮廓信息

bool cmp(const Rect& a, const Rect& b)
{
    if (a.x < b.x)
        return true;
    else
        return false;
}

查找到的轮廓如下图所示:
这里写图片描述
在实际的应用场景中,图像不可避免地存在一些噪声部分,此时噪声部分也可能被提取出来,因此在得到所有轮廓后还需进行滤波处理,除去噪声轮廓。上图中包括噪声图像为数字3、4以及7、8之间的白色部分,需要滤除。

5.数字分割
根据提取的矩形轮廓信息,可分割出单独的数字进行识别。

for (int i = 0; i < contours_out.size(); i++)
{
    if (!IsAllWhite(image_dil(num_location.at(i)))) // 是否为数字
    {
        tube.push_back(image_dil(num_location.at(i)));
        imshow(string(_itoa(tube_num, rectnum, 10)), tube.at(tube_num));
        tube_num++;
    }
}

分割出的数字效果如下:
这里写图片描述

三、数字识别

1.穿线法
数字式仪表的数字都是八段数码管式数字,都是横平竖直的笔画,没有弧度,可以考虑用割线进行识别,原理图如下。将数字区域(数字1除外)分割成六个部分,扫描个部分的像素点,判断该区域内是否存在笔画(a,b,c,d,e,f,g),最后根据二进制的规则可推断出数字的值。除数字1外,剩余数字分割后图像长宽比都接近,唯独数字1图像的长宽比相对要大一些,可设定合理的阈值来确定数字1的图像。
这里写图片描述

int TubeIdentification(Mat inputmat) // 穿线法判断数码管a、b、c、d、e、f、g、
{
    int tube = 0;
    int tubo_roi[7][4] =
    {
        { inputmat.rows * 0 / 3, inputmat.rows * 1 / 3, inputmat.cols * 1 / 2, inputmat.cols * 1 / 2 }, // a
        { inputmat.rows * 1 / 3, inputmat.rows * 1 / 3, inputmat.cols * 2 / 3, inputmat.cols - 1     }, // b
        { inputmat.rows * 2 / 3, inputmat.rows * 2 / 3, inputmat.cols * 2 / 3, inputmat.cols - 1     }, // c
        { inputmat.rows * 2 / 3, inputmat.rows - 1    , inputmat.cols * 1 / 2, inputmat.cols * 1 / 2 }, // d
        { inputmat.rows * 2 / 3, inputmat.rows * 2 / 3, inputmat.cols * 0 / 3, inputmat.cols * 1 / 3 }, // e
        { inputmat.rows * 1 / 3, inputmat.rows * 1 / 3, inputmat.cols * 0 / 3, inputmat.cols * 1 / 3 }, // f
        { inputmat.rows * 1 / 3, inputmat.rows * 2 / 3, inputmat.cols * 1 / 2, inputmat.cols * 1 / 2 }, // g
    };

    if (inputmat.rows / inputmat.cols > 2)   // 1 is special, which is much narrower than others
    {
        tube = 6;
    }
    else
    {
        for (int i = 0; i < 7; i++)
        {

            if (Iswhite(inputmat, tubo_roi[i][0] , tubo_roi[i][1], tubo_roi[i][2], tubo_roi[i][3]))
                tube = tube + (int)pow(2, i);
        }
    }

    switch (tube)
    {
        case  63: return 0;  break;
        case   6: return 1;  break;
        case  91: return 2;  break;
        case  79: return 3;  break;
        case 102: return 4;  break;
        case 109: return 5;  break;
        case 125: return 6;  break;
        case   7: return 7;  break;
        case 127: return 8;  break;
        case 111: return 9;  break;

        default: return -1;
    }
}

2.KNN算法
使用K近邻法对数字图像进行分类,若采用此方法,首先需要收集数码管数字的数据集。需要注意的时,建立KNN模型时,对于数据集进行了一些操作,因此需要对待分类的图像进行相同的操作,否则识别的准确率不高。特别要注意数据集和待识别图像中背景和字体的颜色是否一致。

char trainfile[100];
Mat traindata, trainlabel, tmp;
for (int i = 0; i < TRAINDATANUM; i++)
{
    sprintf(trainfile, "%s\\%d.jpg", TRAINPATH, i); // TRAINPATH可能需要根据实际修改
    tmp = imread(trainfile, IMREAD_GRAYSCALE);   // 读取数据集图像信息
    threshold(tmp, tmp, 50, 255, THRESH_BINARY);
    resize(tmp, tmp, Size(NORMWIDTH, NORMHEIGHT));
    traindata.push_back(tmp.reshape(0, 1));
    trainlabel.push_back(i);  // 附件标签信息
}
traindata.convertTo(traindata, CV_32F);

int K = 1;
Ptr<TrainData> tData = TrainData::create(traindata, ROW_SAMPLE, trainlabel);
Ptr<KNearest> knn = KNearest::create();
knn->setDefaultK(K);
knn->setIsClassifier(true);
knn->train(tData);
for (int i = 0; i < tube_num; i++)
{
    resize(tube.at(i), tube.at(i), Size(NORMWIDTH, NORMHEIGHT));
    tube.at(i) = tube.at(i).reshape(0, 1);
    tube.at(i).convertTo(tube.at(i), CV_32F);
    int r = knn->predict(tube.at(i));   //对所有行进行预测
    cout << r << endl;
}

代码链接:
数码管数字识别–穿线法
数码管数别字识–KNN算法

猜你喜欢

转载自blog.csdn.net/MengchiCMC/article/details/73176295