利用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;
}