实验室门牌号识别

概述

这是2017年4月份走在实验室楼道间萌生的一个想法,就想着能否做个识别每个实验室门牌号的检测器。

数据采集

训练数据是自己在各个实验室门口拍的100多张图片,拍摄的时候会有不同的拍摄角度、光线亮暗的变化等;

如下图所示:

我的思路

需要特别指出的是:在筛选字符的矩形时,采用了矩形框的统计特征,即选择矩形高的残差最小的3个。

详细的流程图如下所示:

中间结果

门牌号数字区域分割结果图:

字符分割结果图:

最终识别结果

因为采集的数据图片有限,对一些数据量缺乏的数字以及不常见的图片的识别会识别错误。

部分识别结果如下:

检测部分源代码

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
#include <opencv2/opencv.hpp>
#include <iostream>
#include <fstream>
#include <math.h>

#include "OCR.h"

using namespace cv;
using namespace std;

//--------------门牌号尺寸为350*220---------------
//---------------蓝色矩形为340*50-----------------
//--------数字4尺寸为22*19 数字1尺寸为22*12-------
//------------------------------------------------
int main( int argc,char** argv)
{
ifstream file("E:\\vs project\\实验室门牌号识别\\实验室门牌号识别\\test\\imageName.txt"); //输入图像的目录

int img_index = 1;

while (!file.eof())
{
char txt_cont[1000];
file.getline(txt_cont,1000);//读出输入流file中imageName.txt的每一行

char img_file[1000],save_file[1000],save_file1[1000],save_file2[1000],save_file3[1000],save_file4[1000];
sprintf(img_file, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别\\test\\%s", txt_cont);//输入图像的目录

sprintf(save_file, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别 predict\\实验室门牌号识别\\test\\test result\\%d.jpg", img_index);//保存输入图像
sprintf(save_file1, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别 predict\\实验室门牌号识别\\test\\test result\\%d[1].jpg", img_index);//保存第一个分割数字
sprintf(save_file2, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别 predict\\实验室门牌号识别\\test\\test result\\%d[2].jpg", img_index); //保存第二个分割数字
sprintf(save_file3, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别 predict\\实验室门牌号识别\\test\\test result\\%d[3].jpg", img_index); //保存第三个分割数字
sprintf(save_file4, "E:\\vs project\\实验室门牌号识别\\实验室门牌号识别 predict\\实验室门牌号识别\\test\\test result\\%d[reslut].jpg", img_index);//保存预测结果图像

cout<<"--------------------------------------------"<<endl;
cout<<" 识别第"<<img_index<<"张门牌号"<<endl;
cout << img_file << endl;
cout<<"--------------------------------------------"<<endl;

Mat src_original = imread(img_file);
//imwrite(save_file,src_original);//保存输入图像
img_index++;

// Mat src_original=imread("1.jpg");
if(src_original.empty())
{
cout<<"图片读取错误"<<endl;
char key;
cin>>key;
if(key='q')
return -1;
}
resize(src_original,src_original,Size(500,375));//原图太大,resize到500*375像素
Mat src=src_original.clone();
imshow("src",src);

Mat src_HSV;
cvtColor(src,src_HSV,CV_BGR2HSV);
Scalar lowerb = Scalar(110, 43, 46);
Scalar upperb = Scalar(124, 255, 255);
inRange(src_HSV, lowerb, upperb, src_HSV);
imshow("src_HSV",src_HSV);//HSV通道限制范围之后,图像已经变成8位单通道了???(具体还有待查阅)以及split该如何操作呢? inRange是阈值化么?

Mat HSV_contours;
src_HSV.copyTo(HSV_contours);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(HSV_contours,contours,hierarchy,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);//findContours大写,输入为8位单通道(所以应先二值化再找轮廓,因为查找的都是白色像素)

vector<RotatedRect> rRects(contours.size()); //定义包围点集的最小矩形vector
//float area_pixel=500*375;//要找的是非零像素数最大的图片 所以一开始赋初值最大
int iteria=0;
//float area_ROI=500*375;

//vector<Mat> ROI_regions(contours.size());//要加上contours.size()
Mat drawing=src.clone();
vector<RotatedRect> rect_Area;//HSV提取蓝色后,轮廓面积大于阈值的矩形数目(正常情况下只有两个)
Mat img_Area[2];

for(int i=0; i<contours.size();++i)
{
rRects[i] = minAreaRect( Mat(contours[i]) );

Point2f rect_points[4];
rRects[i].points(rect_points);
for( int j = 0; j < 4; j++ )
line( drawing, rect_points[j], rect_points[(j+1)%4], Scalar(0,0,255), 2, 8 );
imshow("drawing",drawing);//调试用

cout<<"候选ROI矩形面积为"<<contourArea(contours[i])<<endl;//面积阈值调试用

if(contourArea(contours[i])>2000)
{ rect_Area.push_back(rRects[i]); ++iteria; }
//下面是以y坐标较小者为数字蓝色区域,并保存在rect_Area[0]里
//法2:以左边一半区域非零像素较小者(用boundingRect返回rect类做)(SVM分类也可以)
if(rect_Area.size()==2)
{
//此处getRectSubPix直接用rect_Area[0].size即可。不需要比较宽高的大小并交换,而RotatedRect宽高也没法知道具体是哪个
//因为此处未旋转,未改变整体图像的角度和尺寸。(RotatedRect的宽高因角度不同而不同)
if(rect_Area[0].center.y > rect_Area[1].center.y)
swap(rect_Area[0],rect_Area[1]);
getRectSubPix(src_HSV,rect_Area[0].size,rect_Area[0].center,img_Area[0]);//注:此处size不可以用size(),这里表示的是Size尺寸,而不是size()个数
}

}

/* //下面是以面积较小者为ROI。

if( area_ROI > contourArea(contours[i]) && contourArea(contours[i])>2000)
{
area_ROI=contourArea(contours[i]);
index=i;
++iteria;
cout<<"候选ROI矩形面积为"<<area_ROI<<endl;
}
*/

/* 这段是利用轮廓内的非零像素的方法
getRectSubPix(src_HSV,Size(rRects[i].size.width,rRects[i].size.height),rRects[i].center,ROI_regions[i]);//因为countNonZero函数参数为Mat类,所以此处先将Rotatedect转化为Mat
//area_pixel=countNonZero(ROI_regions[i]);
//cout<<area_pixel<<" "<<contourArea(contours[i])<<endl;
cout<<"轮廓面积为"<<contourArea(contours[i])<<endl;
if( area_pixel > countNonZero(ROI_regions[i]) && contourArea(contours[i])>2000)
{
area_pixel=countNonZero(ROI_regions[i]);
index=i;
++iteria;
cout<<"非零像素数为"<<area_pixel<<endl;
}
*/

if(iteria==0)
{
cout<<"未找出目标区域"<<endl;
return -4;
}

//---------------仿射变换、裁剪得到图像-------------------
//如果直接从img_HSV中裁剪出来的图像,用imshow显示是一片黑色 不懂为啥????!!
//(因为:findContours会改变原图像)
//下面是从原图裁剪的ROI,然后二值化、分割字符、分类字符、识别字符就可以了
//getRectSubPix(src_HSV,Size(340,50),rect_Area[0].center,a);
//imshow("img_ROI",a);

//------------------------------------
//--------旋转并裁剪数字区 ROI_crop
//-------------------------------------
//求旋转矩阵
int index;
float r= (float)rect_Area[0].size.width / (float)rect_Area[0].size.height;
float angle=rect_Area[0].angle;
if(r<1)
angle=90+angle;
Mat rotmat= getRotationMatrix2D(rect_Area[0].center, angle,1);//旋转矩阵

Mat img_rotated;
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
//imshow("img_rotated",img_rotated);

//因为RotatedRect的宽高因角度不同而不同。
//此处是在旋转后,将长的作为宽,短的作为高(旋转后必须这样调整。如果未旋转,裁剪直接用rotatedrects[i].size即可,见上面getRectSubPix用法)
Size rect_size=rect_Area[0].size;
if(r < 1)
swap(rect_size.width, rect_size.height);
Mat img_crop;
getRectSubPix(img_rotated, rect_size, rect_Area[0].center, img_crop);

Mat result_resized;
result_resized.create(50,340, CV_8UC3);//含数字的蓝色矩形归一化为340*50
resize(img_crop, result_resized, result_resized.size(), 0, 0, INTER_CUBIC);
imshow("result_resized",result_resized);

//--------------------------------------------------
//选取蓝色块的右侧“宽2/5的区域”为含数字的ROI区域
Mat ROI_crop;
getRectSubPix(result_resized,Size(136,50),Point2f(272,25),ROI_crop);
imshow("ROI_crop",ROI_crop);


/*
//下面是利用投射变换、几何关系画出来的ROI区域,效果跟裁剪是一样的,自己推导了下很繁!关键是浪费了好多时间!!!!
//----------------------------------------------------------------------
Point2f vertices[4];
rect_Area[0].points(vertices);
cout<<vertices[0]<<vertices[1]<<vertices[2]<<vertices[3];//显示四个顶点坐标

for (int i = 0; i < 4; i++)
line(src, vertices[i], vertices[ (i+1)%4 ], Scalar(0,0,255),2);//画四个边 式中(i+1)%4 使得当i=0,1,2时(i+1)%4分别为1,2,3 i=3时,(i+1)%4为0
imshow("ROI区域",src);

int index_start,a0,a1,a2,a3;
if(float( (vertices[0].y-vertices[2].y )/( vertices[0].x-vertices[2].x )) >0)
{
if( vertices[0].y<vertices[2].y )
{index_start=0; a0=0; a1=1; a2=2; a3=3;}//起始点vertices[0]为点0,vertices中vertices[0]表示0,vertices中排序为0,1,2,3
else
{index_start=2; a0=2; a1=3; a2=0; a3=1;}//起始点vertices[0]为点2,vertices中vertices[2]表示0,vertices中排序为2,3,0,1
}
else
if( vertices[0].y < vertices[2].y )
{index_start=1; a0=3; a1=0; a2=1; a3=2;}//起始点vertices[0]为点1,vertices中vertices[3]表示0,vertices中排序为1,2,3,0
else
{index_start=3; a0=1; a1=2; a2=3; a3=0;}//起始点vertices[0]为点3,vertices中vertices[1]表示0,vertices中排序为3,0,1,2

vector<Point2f> corner;//定义了点集vector容器,getPerspectiveTransform参数要求是vector 而上述顶点是一维数组(也可以一开始定义成vector,相应的操作方式也要更改)
corner.push_back(vertices[a0]);
corner.push_back(vertices[a1]);
corner.push_back(vertices[a2]);
corner.push_back(vertices[a3]);
cout<<vertices[a0]<<vertices[a1]<<vertices[a2]<<vertices[a3];//显示四个顶点坐标

vector<Point2f> corner_PerspectiveTransform;//透视变换后的顶点 (注意顶点顺序)
corner_PerspectiveTransform.push_back(Point(0, 0));
corner_PerspectiveTransform.push_back(Point(rect_Area[0].size.width, 0));
corner_PerspectiveTransform.push_back(Point(rect_Area[0].size.width, rect_Area[0].size.height ));
corner_PerspectiveTransform.push_back(Point(0, rect_Area[0].size.height ));

//获取变换矩阵
Mat M = getPerspectiveTransform(corner, corner_PerspectiveTransform);
cout << "透视变换矩阵为: " << endl << M << endl;

Mat out;
warpPerspective(src_original, out, M,Size(rect_Area[0].size.width, rRects[index].size.height ), 1, 0, 0);

*/


/*
Mat result_resized;
result_resized.create( 220,350,CV_8UC3 );
resize( img_ROI,result_resized,result_resized.size());
imwrite( "result_resized.jpg" ,result_resized);*/

Mat ROI_gray;
cvtColor(ROI_crop,ROI_gray,CV_BGR2GRAY);
Mat ROI_gaussian;
GaussianBlur(ROI_gray,ROI_gaussian,Size(5,5),0,0);
Mat ROI_threshold;
threshold(ROI_gaussian,ROI_threshold,90,255,CV_THRESH_BINARY);
//threshold(ROI_gray,ROI_threshold,0,0,CV_THRESH_OTSU+CV_THRESH_BINARY);
imshow("ROI_threshold",ROI_threshold);

//----------------------------------------
//腐蚀 (有利于腐蚀汉字和噪点,但是对数字0、8等可能会腐蚀断开0、8。)
//----------------------------------------
/* Mat ROI_erode;
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3) );
morphologyEx(ROI_threshold, ROI_erode,MORPH_ERODE, element);
imshow("ROI_erode",ROI_erode);
*/
//--------------------------------
//----注意!!!findContours时,一定要从复制的新图像找轮廓。
//----因为找完轮廓后,原图会变化,原来亮的区域(白色区域)都变成了黑色
Mat imgROI_contours;
ROI_threshold.copyTo(imgROI_contours);

vector<vector<Point>> ROI_contours;
vector<Vec4i> ROI_hierarchy;
findContours(imgROI_contours,ROI_contours,ROI_hierarchy,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE);//findContours大写,输入为8位单通道(所以应先二值化再找轮廓,因为查找的都是白色像素)

vector<Rect> rect_character(ROI_contours.size());
vector<int> index_rect_character;//保存的是符合条件的字符矩形的rect_character[i]中的i值

for(int i=0;i<ROI_contours.size();++i)
{
rect_character[i]=boundingRect(ROI_contours[i]);
rectangle(ROI_crop,rect_character[i].tl(),rect_character[i].br(),Scalar(0,255,0),1,8);
cout<<rect_character[i].width<<" "<<rect_character[i].height<<endl;

//------数字4宽高19*22 数字1宽高12*22
//------数字蓝色牌子实际尺寸340*50归一化的像素也是340*50------
/*if(rect_character[i].size.height!=0 && rect_character[i].size.height>5)//高要不为零,1的宽度最小为12,考虑到倾斜 设宽最小为5
{
if(rect_character[i].size.width/rect_character[i].size.height>0 && rect_character[i].size.width/rect_character[i].size.height<1)
{
//index_rect_character.push_back(i);//index_rect_character保存的是符合条件的字符矩形rect_character[i]中的i值
//cout<<i<<endl<<index_rect_character[0]<<endl;


}
}*/
}
imshow("ROI_crop数字分割",ROI_crop);

//---------------------------------------------
//------------筛选数字矩形框-------------------
//---------------------------------------------
vector<Rect>::const_iterator it = rect_character.begin();
float aver_height=0;
vector<float> subtract;//矩形框的高与平均高度的差

if(rect_character.size()==3)
{
//对三个矩形框位置按数字顺序排序
for(int i=0;i<3;++i )
{ for(int j=i+1;j<3;++j)
if( rect_character[i].x > rect_character[j].x )
swap(rect_character[i],rect_character[j]);
}
}
else
if(rect_character.size()>3)
{
//-----除去尺寸较为极端的非字符矩形框-----
while(it != rect_character.end())
{
if(it->height<8 || it->width<6 || it->width>30 || it->height>30)
{ it = rect_character.erase(it); }
else
{ ++it; }
}//下面如果用itc循环操作的话,注意重新赋值rect_character.begin()因为此处itc值已变为rect_character.end()

cout<<"除去极端非字符矩形框后的矩形个数为:"<<rect_character.size()<<endl;

//检查除去极端非字符矩形框后的矩形vector的size是否为 3
if(rect_character.size()==3)
{
//对三个矩形框位置按数字顺序排序
for(int i=0;i<3;++i )
{ for(int j=i+1;j<3;++j)
if( rect_character[i].x > rect_character[j].x )
swap(rect_character[i],rect_character[j]);
}

/* rectangle(ROI_crop,rect_character[0].tl(),rect_character[0].br(),Scalar(0,255,255),1,8);
rectangle(ROI_crop,rect_character[1].tl(),rect_character[1].br(),Scalar(0,255,0),1,8);
rectangle(ROI_crop,rect_character[2].tl(),rect_character[2].br(),Scalar(0,0,255),1,8);
imshow("ROI_crop调试",ROI_crop);
*/
}
else
if(rect_character.size()>3)//除去极端非字符矩形框后的矩形vector的size若>3
{
for(int i=0;i<rect_character.size();++i)
{ aver_height+=rect_character[i].height;}
aver_height=aver_height/rect_character.size();
//求每个矩形框的高与平均高的差
for(int i=0;i<rect_character.size();++i )//.size()
{
subtract.push_back(fabs(rect_character[i].height-aver_height));//fabs是求浮点型数据的绝对值
index_rect_character.push_back(i);
}//index_rect_character中的i与rect_character、subtract中的i对应
//此处或者这样写img_character.push_back(rect_character[i]); img_character为定义的vector容器

//对subtract中的数值(即高与平均高的差)由小到大排序(前三个就是数字矩形框)
for(int i=0;i<subtract.size();++i)
{
for(int j=i+1;j<subtract.size();++j)
{
if( subtract[i] > subtract[j] )
{
swap(subtract[i],subtract[j]);
swap(rect_character[index_rect_character[i]],rect_character[index_rect_character[j]]);
}
}
//对前三个矩形框位置按数字顺序排序
for(int i=0;i<3;++i )
{ for(int j=i+1;j<3;++j)
if( rect_character[i].x > rect_character[j].x )
swap(rect_character[i],rect_character[j]);
}
}
cout<<"高的残差有小到大顺序为:"<<subtract[0]<<"\t"<<subtract[1]<<"\t"<<subtract[2]<<"\t"<<endl;
}
else
{cerr<<"经筛选后的数字矩形框小于3个"<<endl;return -2;}
}
else
{cerr<<"数字矩形框提取错误,没有三个数字"<<endl; return -3;}

int charSize=20;//分割出的字符归一化尺寸
Mat img_character[3];//保存裁剪出的字符图像
Mat out[3];//保存归一化后的字符输出图像
//下面也可以这样写Mat auxROI(ROI_threshold,rect_character[i])
for(int i=0;i<3;++i)
{
getRectSubPix(ROI_threshold, rect_character[i].size(), Point2f(rect_character[i].x+rect_character[i].width/2,rect_character[i].y+rect_character[i].height/2), img_character[i]);

//----------仿射变换进行预处理-----------
int h=img_character[i].rows;//Mat类只有cols和rows,没有height和width成员
int w=img_character[i].cols;
Mat transformMat=Mat::eye(2,3,CV_32F);
int m=max(w,h);
transformMat.at<float>(0,2)=m/2 - w/2;
transformMat.at<float>(1,2)=m/2 - h/2;

Mat warpImage(m,m, img_character[i].type());
warpAffine(img_character[i], warpImage, transformMat, warpImage.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0) );

//归一化为charSzie 此处为20*20
resize(warpImage, out[i], Size(charSize, charSize) );
}

imshow("out[0]",out[0]);
imshow("out[1]",out[1]);
imshow("out[2]",out[2]);

//imwrite(save_file1,out[0]);
//imwrite(save_file2,out[1]);
//imwrite(save_file3,out[2]);

//字符ANN训练
OCR ocr("OCR.xml");
char chars[3];
for(int i=0;i<3;++i)
{
//提取特征、预测
Mat f=ocr.features(out[i],15);//此处应与OCR::OCR(string trainFile)中的数据集选择对应
int result_col=ocr.classify(f);//所预测的结果在output中的哪一列
chars[i]=ocr.strCharacters[result_col];
}
cout<<"实验室门牌号为:"<<chars[0]<<chars[1]<<chars[2]<<endl<<endl;

//利用输入输出流把门牌号字符转化成字符串 putTex
stringstream ss;
string labNumber;
ss<<chars[0]<<chars[1]<<chars[2]<<endl;
ss>>labNumber;

//------画出数字蓝色区域------
Point2f rectArea_points[4];
rect_Area[0].points(rectArea_points);
for( int j = 0; j < 4; j++ )
line( src_original, rectArea_points[j], rectArea_points[(j+1)%4], Scalar(0,255,0), 2, 8 );

putText(src_original,labNumber,Point(src_original.cols/2,src_original.rows/7),CV_FONT_HERSHEY_SCRIPT_SIMPLEX,1,Scalar(0,0,255),2 );

//imshow("门牌号识别结果",src_original);
imwrite(save_file4,src_original);

}//这个是前面批处理操作 while (!file.eof()) {} 的括号
waitKey();
}