一文详解opencv摄像头数字识别
本文的目标是实现识别摄像头图像中的数字。实际应用场景包括车牌号识别,部分竞赛的A4纸打印数字识别。项目实现结果如下,完整工程文件点此下载:
摄像头数字识别分为两个步骤:
- 提取图像中的ROI区域,如截取车牌的矩形区域,或截取A4纸的图像。
- 对ROI区域进行数字识别。
数字识别相对来说较为简单,先介绍数字识别的方法和原理。
一、数字识别的两种方式
1.1 轮廓提取法
实现思路为对ROI区域进行轮廓提取,然后将所有找到的轮廓与模板逐一匹配识别,相似度大于所设阈值,可视为识别成功。
寻找轮廓所使用的函数为findContours(),利用此函数将所有寻找到的轮廓保存在contours中,然后使用循环画出包围每一个轮廓的最小矩形。
利用每一个小矩形,提取图像中的每一个轮廓图像,将其与模板做差,如果差值越小,说明像素越接近,相似程度越高,以此来实现数字匹配。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47//轮廓提取主函数
int main()
{
//读取一张图像,转换为灰度图并进行二值化处理
Mat srcImage = imread("E://Program//OpenCV//vcworkspaces//ogr_test//images//txt.jpg"); //读取图片
Mat dstImage, grayImage, binImage;
srcImage.copyTo(dstImage); //将读取到的图片,深拷贝为dstImage
cvtColor(srcImage, grayImage, COLOR_BGR2GRAY); //转换灰度图
threshold(grayImage, binImage, 100, 255, cv::THRESH_BINARY_INV); //转换二值图,设置阈值,高于100认为255
//寻找轮廓
vector<vector<Point>> contours; //定义轮廓和层次结构
vector<Vec4i> hierarchy;
findContours(binImage, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); //寻找轮廓
int i = 0;
vector<vector<Point>>::iterator It;
Rect a4rect[15]; //假设最多不会超过15个轮廓
for (It = contours_rec.begin(); It < contours_rec.end(); It++) { //画出包围数字的最小矩形
a4rect[i].x = (float)boundingRect(*It).tl().x;
a4rect[i].y = (float)boundingRect(*It).tl().y;
a4rect[i].width = (float)boundingRect(*It).br().x - (float)boundingRect(*It).tl().x;
a4rect[i].height = (float)boundingRect(*It).br().y - (float)boundingRect(*It).tl().y;
if ((a4rect[i].height > 80) && (a4rect[i].width > 50) && (a4rect[i].height < 300) && (a4rect[i].width < 300)) {
rectangle(dstImage, a4rect[i], Scalar(0, 0, 255), 2, 8, 0); //在原图像中用红框画出识别到的各轮廓
rectangle(binImage, a4rect[i], Scalar(0, 0, 0), 0, 8, 0);
i++;
}
}
imshow("dstImage", dstImage);
//将图像轮廓逐一与模板匹配
Mat num[15];
int matchingNum = 0; //匹配到的数字
int matchingRate = 0; //相似率
for (int j = 0; j < i; j++) {
a4binImg(a4rect[j]).copyTo(num[j]); //提取包围数字的矩形区域至num[j]
imgMatch(num[j], matchingRate, matchingNum); //数字匹配
if (matchingRate < 400000) {
cout << "识别数字:" << matchingNum << "\t匹配率:" << matchingRate << endl;
//imwrite(to_string(matchingNum) + ".jpg", num[j]);
}
}
system("pause");
return 0;
}
两图像相减之前,需要先制作一张模板,你可以自己在记事本里敲0-9的数字,截图,使用上面的函数imwrite出来一份模板。也可以到我的github中下载,其中0.jpg-9.jpg就是模板文件。
1 | //获取所有像素点和,用于求两图像相减后所得图像的所有像素之和 |
1.2 行列扫描法
此方法主要参考opencv 数字识别详细教程这篇文章,在此感谢LTG01大佬的无私分享。
基本过程为:
- 将图像二值化处理,使数字部分为白色,其余部分为黑色。
- 对一个图像先逐行扫描求和,如果第一行像素和为0,则继续向下扫描,直到碰到像素和不为0的行,将行数记下来,此为数字的顶部。
- 继续向下扫描,此时会从上到下逐渐扫描数字所在的每一行,当行像素和再次为0时,再将行数记录下来,代表已经到了数字的底部,将顶部与底部之间的区域截取出来。
- ,对截取出来的图像进行逐列扫描求和,过程同上,记录出数字的左右列号,根据左右列号即可从刚才截取出的图像中,取出包含数字的最小图像。
- 利用此最小图像与模板匹配。
1 | int main() |
有关扫描法识别数字的完整代码见我的Github的scan分支。
二、提取图像中的ROI区域
提取ROI区域的步骤如下:
- 读取摄像头每一帧图像
- 对图像进行二值化处理
- 对图像进行形态学处理
- 设置限制条件寻找目标区域,并框选(这一步是重点)
2.1 读取摄像头图像
摄像头的读取原理在之前的文章中已有介绍《摄像头视频的读取与存储》。主要使用函数为 capture.read()
,此函数用于捕获视频的每一帧,并返回刚刚捕获的帧。示例程序如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26int main()
{
VideoCapture capture(0); //创建VideoCapture类,打开电脑默认摄像头传参0,如果有外置摄像头参数为1
int frame_width = capture.get(CAP_PROP_FRAME_WIDTH); //获取摄像头的宽、高、帧数、FPS
int frame_height = capture.get(CAP_PROP_FRAME_HEIGHT);
Mat frame; //定义Mat对象用于存储每一帧数据
while (capture.isOpened()) {
capture.read(frame); //逐帧读取视频
//flip(frame, frame, 1); //将读取的视频左右反转
if (frame.empty()) { //如果视频结束或未检测到摄像头则跳出循环
break;
}
imshow("Video", frame); //每次循环显示一帧图像,frame就是每帧图像
char k = waitKey(333); //两帧读取的间隔时间
if (k == 'q') { //按下q键退出循环
break;
}
}
capture.release(); //释放视频
system("pause");
return 0;
}
2.2 对图像进行二值化处理
通过每个像素的颜色分量将图片进行二值化。正常曝光情况下A4纸的BGR均为215左右,车牌的颜色信息大约为B=138,G=63,R=23。但是在不同环境下颜色信息可能会有偏差,因此需要将条件在一定程度上放宽,再通过其他一些条件来准确查找目标区域。
1 | //图像二值化 |
2.3 形态学处理
可以看出二值画处理后已经比较明显完整的显示出A4纸区域,但是仍然存在一些噪点,此时进行形态学处理,以消除这些噪点干扰。对图像先膨胀再腐蚀,可以填充细小空间,连接临近物体和平滑边界。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//形态学处理
void morphTreat(Mat& binImg) {
Mat BinOriImg; //形态学处理结果图像
Mat element = getStructuringElement(MORPH_RECT, Size(5, 5)); //设置形态学处理窗的大小
GaussianBlur(binImg, binImg, Size(5, 5), 11, 11);
dilate(binImg, binImg, element); //进行多次膨胀操作
dilate(binImg, binImg, element);
dilate(binImg, binImg, element);
dilate(binImg, binImg, element);
dilate(binImg, binImg, element);
erode(binImg, binImg, element); //进行多次腐蚀操作
erode(binImg, binImg, element);
erode(binImg, binImg, element);
erode(binImg, binImg, element);
erode(binImg, binImg, element);
//imshow("形态学处理后", BinOriImg); //显示形态学处理之后的图像
cvtColor(binImg, binImg, CV_BGR2GRAY); //将形态学处理之后的图像转化为灰度图像
threshold(binImg, binImg, 100, 255, THRESH_BINARY); //灰度图像二值化
}
矩形窗的大小与膨胀腐蚀的次数会影响处理结果,处理完的结果大致如下。
2.4 设置限制条件寻找目标区域
经过形态学处理,图像中已经可以明显看到A4纸所在的区域,但是图像中仍然不可避免存在其他与A4纸颜色接近的物体,在这里也会显示为白色。这时就需要我们根据A4纸区域的特点设置限制条件,从这些白色区域中找到代表A4纸所在的区域。
在这里我使用的限制条件主要有以下几个:
- 矩形面积在一定范围内
- 长宽比A4纸为1.414,一定程度放宽后作为限制条件
- 短边长度在一定范围内
首先寻找图像中的轮廓,利用轮廓面积初步判断,对轮廓面积符合条件的进一步获取其外接矩形。计算此矩形的各个参数(顶点坐标、长宽、面积、倾斜角度等),然后根据限制条件对此矩形进行判别。
如果矩形区域符合条件,那么就需要将其截取出来,并根据先前计算的倾斜角度将A4纸图像摆正,便于后续对其中的数字进行识别。旋转图像的函数需要一些数学知识,旋转前后的图像的长宽有一定函数关系。(h’、w’为旋转后图像高、宽)
1 | //图像旋转 |
1 | /************************** 提取A4纸区域并识别数字 *****************************/ |