工程文件详见 :z199912230/-VS-: 数字图像处理;图像识别 (github.com)
要求
- 根据题目要求,利用图像处理相关知识,完成印刷数字图像的采集及识别,确定具体数字。
- 采集印刷体数字图像。识别0-9两种以上字体的数字图像识别。
- 熟悉Visaul Studio2019软件开发平台,Opencv及相关功能的配置。
- 利用Opencv库函数,驱动笔记本电脑自带的摄像头进行图像采集及显示。
- 整个系统的设计过程包括:模板制作,图像采集及预处理,图像识别。
- 通过调试、运行,查看数字识别的结果,进行识别率的分析。
系统总体设计
基于Visaul Studio2019软件开发平台,搭载OpenCv库的印刷数字识别系统的设计,需要识别不同字体的印刷数字,其中涉及到了Opencv库以及相关的操作。
OpenCV图像处理库,它轻量级而且高效,由一系列C函数和少量C++类构成,实现了图像处理和计算机视觉方面的很多通用算法。因此本次课题将使用OpenCv库函数实现以下功能:模板制作、灰度转换、二值化、图像采集、鼠标回调函数操作、图像分割、模板匹配等。最终实现不同印刷数字的识别。系统总体框图如图1所示。
配置环境
- 添加环境变量,将OpenCv动态库的路径添加到Path环境变量中。
- 在VS2015中新建空项目,配置包含目录:添加OpenCv头文件的路径。
- 配置库目录:添加OpenCv动态库的路径。
- 配置链接器的输入:添加OpenCv的动态库。
模板制作模块程序设计
首先准备一张有0~9数字的图片,然后将图片进行灰度处理。
之后将灰度图进行二值化,阈值设为100,这样可以将图片变成黑纸白字。利用边缘检测的原理将每个数字分割开制成模板。
void makeModel()
{
//Mat src = imread("model.png", CV_LOAD_IMAGE_GRAYSCALE);
//threshold(src, src, 100, 255, CV_THRESH_BINARY_INV); // 二值化
Mat src = imread("model.png");//读取模板
paintGrey(src, src);//对模板进行灰度化处理
//imshow("grey", src);
towNum(src, src, 150, 255, 0);//二值化
//imshow("tow", src);//显示图像
imwrite("tow.jpg", src);
for (int i = 0; i <20; i++)
{
char fileName[21];
Mat rImg, dst;
cutLeft(src, dst, rImg);
sprintf_s(fileName, "%d.png", i);//生成0~19.png
imwrite(fileName, dst);//
src = rImg;
}
}
// 二值化
void towNum(Mat &src, Mat &dst, int thresh, int maxVul, bool flag)
{
Mat tmp = Mat(src.size(), CV_8U);//获取大小
for (int i = 0; i < src.rows; i++)//行
{
for (int j = 0; j < src.cols; j++)//列
{
uchar num = src.at<uchar>(i, j);
if (flag == 0)
{
if (num <= thresh)
tmp.at<uchar>(i, j) = 255;
else
tmp.at<uchar>(i, j) = 0;
}
else
{
if (num <= thresh)
tmp.at<uchar>(i, j) = 0;
else
tmp.at<uchar>(i, j) = 255;
}
}
}
dst = tmp;
}
// 灰度转换
void paintGrey(Mat &src, Mat &dst)
{
Mat temp = Mat(src.size(), CV_8U);
int row = src.rows;
int col = src.cols;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
uchar b = src.at<Vec3b>(i, j)[0] * 0.144;
uchar g = src.at<Vec3b>(i, j)[1] * 0.587;
uchar r = src.at<Vec3b>(i, j)[2] * 0.299;
temp.at<uchar>(i, j) = saturate_cast<uchar>(b + g + r);
//防止溢出
if ((b + g + r) > 255)
temp.at<uchar>(i, j) = 255;
else
temp.at<uchar>(i, j) = b + g + r;
}
}
dst = temp;
}
//切掉左右边空白和数字切割//行
int cutLeft(Mat& src, Mat& leftImg, Mat& rightImg)
{
int left, right;
left = 0;
right = src.cols;//;列
int i;
for (i = 0; i < src.cols; i++)
{
int colValue = getColSum(src, i);
if (colValue > 0)
{
left = i;
break;
}
}
if (left == 0)
return 1;
for (; i < src.cols; i++)
{
int colValue = getColSum(src, i);
if (colValue == 0)
{
right = i;
break;
}
}
int width = right - left;
Rect rect(left, 0, width, src.rows);
leftImg = src(rect).clone();//
Rect rectRight(right, 0, src.cols - right, src.rows);//下x,y坐标长宽
rightImg = src(rectRight).clone();
cutTop(leftImg, leftImg);
return 0;
}
//统计所有列的二值总和
int getColSum(Mat src, int col)
{
int sum = 0;
int height = src.rows;
int width = src.cols;
for (int i = 0; i < height; i++)
{
sum = sum + src.at<uchar>(i, col);
}
return sum;
}
//切掉图片的上下空白
void cutTop(Mat& src, Mat& dstImg)
{
int top, bottom;
top = 0;
bottom = src.rows;
int i;
for (i = 0; i < src.rows; i++)
{
int colValue = getRowSum(src, i);
if (colValue > 0)
{
top = i;//统计高度
break;
}
}
for (; i < src.rows; i++)
{
int colValue = getRowSum(src, i);
if (colValue == 0)
{
bottom = i;
break;
}
}
int height = bottom - top;
Rect rect(0, top, src.cols, height);
dstImg = src(rect).clone();
}
int getRowSum(Mat src, int row)//统计所有行的总和
{
int sum = 0;
int height = src.rows;
int width = src.cols;
for (int i = 0; i < width; i++)
{
sum = sum + src.at<uchar>(row, i);
}
return sum;
}
图像采集模块程序设计
首先使用OpenCv库函数启动IP摄像头,采集图像,然后用鼠标选择要识别的具体区域,此处使用到了OpenCv库的鼠标操作回调函数,之后将识别区域的图片进行灰度转换与二值化处理。
Mat getPicture()
{
int c;
VideoCapture capture("“http://账号:密码@广域网IP:端口号”");//调用摄像头
while (true)
{
capture.read(src);
imshow("origion", src);
c = waitKey(30);//延时
if (c == 13)
{
capture.release();
break;
}
}
/*VideoCapture v(0);//调用电脑摄像头
int c;
while (v.read(src))
{
imshow("origin", src);
c = waitKey(30);//延时
if (c == 13)
{
v.release();
break;
}
}*/
//imshow("test", src);
//src = imread("test.png");
imshow("t", src);
// 设置鼠标事件回调函数
setMouseCallback("t", mouse_callback);
while (char(waitKey(1)) != 13)
{
}
paintGrey(crop, crop);
towNum(crop, crop, 150, 255, 0);
return crop;
}
// 鼠标事件回调函数
void mouse_callback(int event, int x, int y, int, void*)
{
// 当鼠标左键按下时,记录其状态和坐标
if (event == EVENT_LBUTTONDOWN)
{
ldown = true;
corner1.x = x;
corner1.y = y;
cout << "Corner 1 recorded at" << corner1 << endl;
}
// 当鼠标左键放开时,记录其状态和坐标
if (event == EVENT_LBUTTONUP)
{
// 判断选取的区域是否大于20个像素
if (abs(x - corner1.x) > 20 && abs(y - corner1.y) >20)
{
lup = true;
corner2.x = x;
corner2.y = y;
cout << "Corner 2 recorded at" << corner2 << endl << endl;
}
else
{
cout << "Please select a bigger region" << endl;
ldown = false;
}
}
//当移动鼠标时, 更新选择区域, 并绘制矩形选择区域图形
if (ldown == true && lup == false)
{
Point pt;
pt.x = x;
pt.y = y;
Mat local_img = src.clone();
rectangle(local_img, corner1, pt, Scalar(0, 0, 255));
imshow("t", local_img);
}
// 定义感兴趣区域,并对原图进行剪裁
if (ldown == true && lup == true)
{
box.width = abs(corner1.x - corner2.x);
box.height = abs(corner1.y - corner2.y);
box.x = min(corner1.x, corner2.x);
box.y = min(corner1.y, corner2.y);
// 对原图进行剪裁, 生成新图
crop = 0;
crop = src(box);
namedWindow("Crop", CV_WINDOW_AUTOSIZE);
imshow("Crop", crop);
ldown = false;
lup = false;
imshow("t", src);
}
}
具体启动IP摄像头的教程参考
通过局域网访问手机摄像头数据流 (baidu.com)
利用OpenCV4调用安卓手机摄像头、电脑摄像头的实现 (baidu.com)
图像识别模块程序设计
图像匹配有很多方法:比如模板匹配、直方图匹配等,本次课题中采用的方法是模板匹配。
在匹配图像之前需要将模板与待识别图像设定为同样大小得像素值,用到了图像缩放,实习中用的双线性插值法。双线性插值法的原理:当求出的分数地址与像素点不一致时,求出其与周围4个像素点的距离比,根据该比例,由4个邻域的像素灰度值进行双线性插值。
图像匹配的原理:将待识别的二值图像的像数值与20个模板的二值图像的像数值一一对应,求其差的绝对值。
void getPXSum(Mat &src, int &a) //计算图像最小距离
{
towNum(src, src, 150, 255, 1);
a = 0;
for (int i = 0; i < src.rows; i++)//行像素
{
for (int j = 0; j < src.cols; j++)//列像素
{
a += src.at<uchar>(i, j);//第i行第j列的的值.at()是一个返回值
}
}
}
// 求绝对值
void my_abs(Mat &src, Mat &dst, Mat &result)
{
Mat tmp = Mat(src.size(), CV_8U);//无符号整数
for (int i = 0; i < src.rows; i++)
{
for (int j = 0; j < src.cols; j++)
{
uchar a = src.at<uchar>(i, j);
uchar b = dst.at<uchar>(i, j);
tmp.at<uchar>(i, j) = a > b ? a - b : b - a;//如果a>b成立则a-b,否则b-a
}
}
result = tmp;
}
int getSubtract(Mat &src, int TemplateNum) //用于识别数字
{
Mat img_result = Mat(src.size(), CV_8U);;
int min = 1000000;
int serieNum = 0;
int serienum = 0;
int num[20];
for (int p = 0; p < 20; p++)
num[p] = 0;
for (int i = 0; i <= TemplateNum; i++)
{
char name[20];
sprintf_s(name, "%d.png", i);
Mat Template = imread(name);//读取图片文件
paintGrey(Template, Template);//灰度转换
//threshold(src, src, 100, 255, CV_THRESH_BINARY);
towNum(Template, Template, 150, 255, 1);//二值化test模板
towNum(src, src, 150, 255, 1);
resize(src, src, Size(50, 50), 0, 0, CV_INTER_LINEAR);//缩放的大小为50*50,0,height/row,0,,双线性插值
resize(Template, Template, Size(50, 50), 0, 0, CV_INTER_LINEAR);
my_abs(Template, src, img_result);//结果保存到img_result
int diff = 0;
getPXSum(img_result, diff);//距离
num[i] = diff;
/*if (diff < min)
{
min = diff;
serieNum = i;
serienum = i;
if (serienum >= 10)
serienum =serienum -10;
}*/
}
for (int i = 0; i < 20; i++)
{
int cin;
cin >> num[i];
if (num[i] < num[serieNum])
{
serieNum = i;
serienum = i;
if (serienum >= 10)
serienum = serienum - 10;
}
}
//cout << "最小距离是" << min << ",";
cout << "最小距离是" << num [serienum] << ",";
cout << "匹配的是第" << serieNum << "个模板,匹配的数字是" << serienum << endl;
return serieNum;
}
int main()
{
// 建模板
makeModel();
//用于识别
Mat src = imread("test.png", CV_LOAD_IMAGE_GRAYSCALE);
Mat test = getPicture();
while (char(waitKey(0))!='q')//无返回值,必须有按键
{
imshow("test", test);//
Mat leftImg, rightImg;
int res = cutLeft(test, leftImg, rightImg);//切割
while (res == 0)//切割完成后
{
Mat srcTmp = rightImg;
getSubtract(leftImg, 19 );
res = cutLeft(srcTmp, leftImg, rightImg);
}
while (char(waitKey(1)) != 13)
{
}
paintGrey(crop, crop);
imshow("crop", crop);
towNum(crop, crop, 150, 255, 0);
test = crop;
}
waitKey(0);
return 0;
}
结果
采集到的图像
选取测试图片
识别结果