QT opencv 万用表 数码管数字识别
首先,设备及配置
1、qt5.12 + msvc2013
用MinGW 也没问题
2、opencv3.0.0
3、摄像头是普通USB无驱
一、配置.pro文件
pro文件添加以下内容
按照自己的路径更改
INCLUDEPATH +=D:/opencv/opencv_300/opencv/build/include/opencv \
D:/opencv/opencv_300/opencv/build/include/opencv2 \
D:/opencv/opencv_300/opencv/build/include
CONFIG(debug, debug|release) {
LIBS += -LD:/opencv/opencv_300/opencv/build/x64/vc12/lib \
-lopencv_world300
-lopencv_world300d
}
CONFIG(release, debug|release) {
LIBS += -LD:/opencv/opencv_300/opencv/build/x64/vc12/lib \
-lopencv_world300
-lopencv_world300d
}
二、添加界面配置
getimg 按钮: 获取当前画面截图
saveimg按钮: 保存已经获取的截图
load template按钮: 加载要匹配的模板图片
start 按钮: 模板加载后,点击,开始识别程序
comboBox 控件: 选择模板匹配所需的模式
三个label控件: 一个显示摄像头画面,一个显示截图,一个显示模板图片
三、主要处理步骤
1、模板匹配并切割
/*Method
0: SQDIFF 平方差匹配
1: SQDIFF NORMED 标准平方差匹配
2: TM CCORR 相关匹配
3: TM CCORR NORMED 标准相关匹配
4: TM COEFF 相关匹配
5: TM COEFF NORMED" 标准相关匹配
对于方法 SQDIFF 和 SQDIFF_NORMED,
越小的数值代表更高的匹配结果. 而对于其他方法, 数值越大匹配越好*/
//开始匹配
Mat originimg = frame.clone();
cv::matchTemplate(originimg,thTemplateImage,result,match_method);
cv::normalize(result,result,0,1,NORM_MINMAX,-1,Mat()); //归一化数据
//取出最佳匹配结果
double Val;
double minVal,maxVal;
Point minLoc,maxLoc,matchLoc;
minMaxLoc(result,&minVal,&maxVal,&minLoc,&maxLoc,Mat());
if(match_method == 0 ||match_method == 1){
matchLoc = minLoc;
Val = minVal;
}
else {
matchLoc = maxLoc;
Val = maxVal;
}
//截取图像
Rect rect(matchLoc.x,
matchLoc.y,
thTemplateImage.cols,
thTemplateImage.rows);
2、轮廓提取
//边缘检测
Canny(thresholdImg,cannyImg,50,200,3);
//提取最大轮廓
vector<vector<Point>> contours; //储存轮廓
vector<Vec4i> hierarchy;
findContours(cannyImg, contours, hierarchy,
RETR_CCOMP, CHAIN_APPROX_SIMPLE); //获取轮廓
3、矩形包围
从以上轮廓中查找最大的轮廓,多边形逼近获得矩形轮廓。
//提取最大面积轮廓 矩形包围
vector<vector<Point>> polyContours(contours.size());
int maxArea = 0;
for (int index = 0; index < contours.size(); index++){
if (contourArea(contours[index]) > contourArea(contours[maxArea]))
maxArea = index;
//多边形逼近
approxPolyDP(contours[index], polyContours[index], 100, true);
}
//过滤掉面积gauss小的轮廓
if(contourArea(contours[maxArea]) <= 28000)
{
qDebug()<<"未检测到合适面积"<<contourArea(contours[maxArea]);
return;
}
//画出矩形
drawContours(orgImg, polyContours, maxArea, Scalar(0,0,255), 2);
4、定位矩形四点
参考博文:利用OpenCV提取图像中的矩形区域(PPT屏幕等)
凸包检测,在图片中画出,获取四个点的位置参数
//凸包检测 获取矩形四个点
vector<int> hull;
convexHull(polyContours[maxArea], hull, false); //检测该轮廓的凸包
if(hull.size() != 4)
{
qDebug()<<"检测到非矩形的点";
return;
}
//画出点
for (int i = 0; i < hull.size(); ++i){
circle(orgImg, polyContours[maxArea][i], 10, Scalar(255, 0, 0), 3);
}
效果如图
5、透视变换(将捕捉的图像矫正)
参考博文:利用OpenCV提取图像中的矩形区域(PPT屏幕等)
透视变换之前要对图像做一系列的处理工作,减少干扰
特别注意的是,调整腐蚀和膨胀,使小数点和左右的数字分离,否则较难识别
Mat orgImg,cannyImg,grayImg,thresholdImg,turnedImg,gaussImg;
orgImg = src.clone();
turnedImg = src.clone();
//灰度图
cvtColor(orgImg,grayImg,cv::COLOR_BGR2GRAY);
//高斯模糊
GaussianBlur(grayImg,gaussImg,Size(3,3),1.5);
//二值化
threshold(grayImg,thresholdImg,120,255,THRESH_BINARY_INV);
//开运算 白色区域先腐蚀后膨胀
Mat element = getStructuringElement(MORPH_RECT,Size(3,3));
morphologyEx(thresholdImg,thresholdImg,MORPH_OPEN,element);
//腐蚀 分离小数点
Mat element1 = getStructuringElement(MORPH_RECT,Size(5,5));
erode(thresholdImg,thresholdImg,element1);
处理完成后,正式进入透视变换步骤:
首先,计算出凸包检测得出的矩形四点的相对位置,
得出矩形左上、右上、右下、左下的位置
根据四点位置与dstPoints四点,获得变换矩阵
根据变换矩阵即可得到校正后的图形
效果如下图
因为原图像本身变形不大,所以透视变换的效果有限,
20度以内的旋转形变,矫正是没问题的
//目标坐标 我这里按照实际情况向内侧延伸了一部分,排除边缘的影响
Point2f srcPoints[4], dstPoints[4]; //左上,右上,右下, 左下
dstPoints[0] = Point2f(-30, 0);
dstPoints[1] = Point2f(5*62, 0);
dstPoints[2] = Point2f(5*62, 7*34);
dstPoints[3] = Point2f(-30, 7*34);
//对检测到的矩形四个点进行排序 分出左上 右上 右下 左下
bool sorted = false;
int n = 4;
while (!sorted){
for (int i = 1; i < n; i++){
sorted = true;
if (polyContours[maxArea][i-1].x > polyContours[maxArea][i].x){
swap(polyContours[maxArea][i-1], polyContours[maxArea][i]);
sorted = false;
}
}
n--;
}
if (polyContours[maxArea][0].y < polyContours[maxArea][1].y){
srcPoints[0] = polyContours[maxArea][0];
srcPoints[3] = polyContours[maxArea][1];
}
else{
srcPoints[0] = polyContours[maxArea][1];
srcPoints[3] = polyContours[maxArea][0];
}
if (polyContours[maxArea][2].y < polyContours[maxArea][3].y){
srcPoints[1] = polyContours[maxArea][2];
srcPoints[2] = polyContours[maxArea][3];
}
else{
srcPoints[1] = polyContours[maxArea][3];
srcPoints[2] = polyContours[maxArea][2];
}
//获取变换矩阵 坐标变换
Mat transMat = getPerspectiveTransform(srcPoints,dstPoints);
warpPerspective(thresholdImg , outputImg, transMat,
thresholdImg.size(),INTER_LINEAR// INTER_LINEAR | WARP_INVERSE_MAP
);
6、垂直投影法分割字符
参考博文:Opencv2.4学习::垂直投影法分割字符
如图,上图是直方图,下图是按直方图分割后的图片
Mat orgImg,thorgImg,cannyImg;
int src_width = inputsrc.cols;
int src_height = inputsrc.rows;
int* projectValArry = new int[src_width]();//创建用于储存每列白色像素个数的数组
//取列白色像素个数
for (int i = 0; i < src_height; i++){
for (int j = 0; j < src_width; j++){
if (inputsrc.at<uchar>(i, j)){
projectValArry[j]++;
}
}
}
/**************将每列白色像素数目绘制成直方图***************************/
//定义画布 绘制垂直投影下每列白色像素的数目
Mat verticalProjectionMat(src_height, src_width, CV_8UC1, Scalar(0));
for (int i = 0; i< src_width; i++){
for (int j = 0; j < projectValArry[i]; j++){
verticalProjectionMat.at<uchar>(src_height-j-1,i) = 255;
}
}
imshow("verticalProjectionMat", verticalProjectionMat);
/*********根据每列白色像素数目设置截取起始和截止列***********************/
//定义Mat vector ,存储图片组
vector<Mat> split_src;
//定义标志,用来指示在白色像素区还是在全黑区域
bool white_block = 0, black_block = 0;
//定义列temp_col_forword temp_col_behind,记录字符截取起始列和截止列
int temp_col_forword=0,temp_col_behind = 0;
Mat split_temp;
//遍历数组projectValArry
for (int i = 0; i < src_width; i++){
if (projectValArry[i]){//表示区域有白色像素
white_block = 1;
black_block = 0;
}
else{ //若无白色像素(进入黑色区域)
if (white_block == 1){//若前一列有白色像素
temp_col_behind = i;//取当前列为截止列
split_temp=inputsrc(Rect(temp_col_forword, 0, temp_col_behind - temp_col_forword, src_height)).clone();
split_src.push_back(split_temp);
}
temp_col_forword = i;//记录最新黑色区域的列号,记为起始列
black_block = 1;//表示进入黑色区域
white_block = 0;
}
}
7、穿线法识别数字
参考博文:OpenCV识别数码管穿线法(基础版)
小数点识别也可以参照此方法
//识别中间行的单个数字
void MyThread::myDiscern(Mat n)
{
//1的图像,使用穿线会是8。应该从它的尺寸入手,高远大于宽,这里我们选取3倍比.
if(3*n.cols<n.rows)
{
emit thSetNumber(1);
qDebug()<<"数字:1";
return;
}
//竖线
int x_half=n.cols/2;
//上横线
int y_one_third=n.rows/3;
//下横线
int y_two_third=n.rows*2/3;
//每段数码管,0灭,1亮
int a=0,b=0,c=0,d=0,e=0,f=0,g=0;
//竖线识别a,g,d段
for(int i=0;i<n.rows;i++)
{
uchar *data=n.ptr<uchar>(i);
if(i<y_one_third)
{
if(data[x_half]==255) a=1;
}
else if(i>y_one_third&&i<y_two_third)
{
if(data[x_half]==255) g=1;
}
else
{
if(data[x_half]==255) d=1;
}
}
//上横线识别:
for(int j=0;j<n.cols;j++)
{
uchar *data=n.ptr<uchar>(y_one_third);
//f
if(j<x_half)
{
if(data[j]==255) f=1;
}
//b
else
{
if(data[j]==255) b=1;
}
}
//下横线识别:
for(int j=0;j<n.cols;j++)
{
uchar *data=n.ptr<uchar>(y_two_third);
//e
if(j<x_half)
{
if(data[j]==255) e=1;
}
//c
else
{
if(data[j]==255) c=1;
}
}
//七段管组成的数字
if(a==1 && b==1 && c==1 && d==1 && e==1 && f==1 && g==0)
{
emit thSetNumber(0);
qDebug()<<"数字:0";
return;
}
else if(a==0 && b==1 && c==1 && d==0 && e==0 && f==0 && g==0)
{
emit thSetNumber(1);
qDebug()<<"数字:1";
return;
}
else if(a==1 && b==1 && c==0 && d==1 && e==1 && f==0 && g==1)
{
emit thSetNumber(2);
qDebug()<<"数字:2";
return;
}
else if(a==1 && b==1 && c==1 && d==1 && e==0 && f==0 && g==1)
{
emit thSetNumber(3);
qDebug()<<"数字:3";
return;
}
else if(a==0 && b==1 && c==1 && d==0 && e==0 && f==1 && g==1)
{
emit thSetNumber(4);
qDebug()<<"数字:4";
return;
}
else if(a==1 && b==0 && c==1 && d==1 && e==0 && f==1 && g==1)
{
emit thSetNumber(5);
qDebug()<<"数字:5";
return;
}
else if(a==1 && b==0 && c==1 && d==1 && e==1 && f==1 && g==1)
{
emit thSetNumber(6);
qDebug()<<"数字:6";
return;
}
else if(a==1 && b==1 && c==1 && d==0 && e==0 && f==0 && g==0)
{
emit thSetNumber(7);
qDebug()<<"数字:7";
return;
}
else if(a==1 && b==1 && c==1 && d==1 && e==1 && f==1 && g==1)
{
emit thSetNumber(8);
qDebug()<<"数字:8";
return;
}
else if(a==1 && b==1 && c==1 && d==1 && e==0 && f==1 && g==1)
{
emit thSetNumber(9);
qDebug()<<"数字:9";
return;
}
else
{
//printf("[error_%d_%d_%d_%d_%d_%d_%d]",a,b,c,d,e,f,g);
return;
}
}