摘要
国际标准书号ISBN由13位数字组成。前三位数字代表图书,中间的9个数字分为三组,分表示组号、出版社号和书序号,最后一个数字是校验码从1968年英国的“标准书号”(SBN)开始。其优点主要体现在:国际标准书号是机读的编码,从图书的生产到发行、销售始终如一,对图书的发行系统起了很大的作用:它的引入使图书的定购、库存控制、账目和输出过程等任何图书业的分支程序都简化了。我们小组设计出的这个ISBN编号识别系统利用机器视觉图像处理技术可以识别不同的 ISBN 号并将其读取出来,为事后相应的管理统筹工作提供可靠的辅助。
本系统开发采用了 VS2019+opencv的开发环境,C++语言实现。该项目流程为:读取->转化为灰度图->中值滤波去噪->二值化->水平投影确定行->竖直投影确定列 ->模板匹配字符识别->输出。
[关键词] 课程实践项目;数字识别;灰度转化;中值滤波降噪; 二值转化;字符分割
前言
随着现代社会的高速发展,人民的生活水平也在不断提高,阅读书籍的需求也在不断上涨,这对书籍管理水平提出了更高要求,而ISBN编号能极大的方便对书籍的管理,随之而来的问题是:人工识别ISBN码的效率太低了,需要计算机来代替我们做这些工作。但计算机不能直接识别图片中的内容,所以要求我们对图片进行一系列的操作,使它变得易于被识别。这些操作包括:转化为灰度图,中值滤波去噪,二值化,水平投影分割行,竖直投影切割列,找最小矩形框,最后得到单个字符的形式,之后进行数字识别。这里数字识别主要方法有:模板匹配法,神经网络法以及划线法。本小组采用模板匹配法来对字符进行识别。下面就分别介绍对图片的一系列操作和模板匹配的方法。
正文
一、研究内容的基本原理
本次的ISBN编号识别采用模板匹配的方法,在进行识别之前需要对图片进行一系列预处理:原图转灰度图 灰度图转二值图 对二值图进行切割 找到切割后的最小矩形框。模板匹配这种方法具体来说,就是把所有可能出现的每一个字符情况都找一定数量(我们用的有2~4个)的模板,当要识别未知字符时需要与所有模板一一比对,找到最接近的模板进行匹配。这里就有一个衡量标准的问题,即:怎样算“最接近”?我们用的是“找不同的”方法,即先将模板与待识别的字符图片调成一样大小(40X60),再对比对应位置的像素值是否相同,统计两幅图片不同的像素点的个数。如果不同点的数量越少,就认为该模板与待识别的字符越接近;因而最接近自然就是不同的像素点个数最少的模板。
二、所采用的研究方法及相关工具
我们小组在拿到课题后一起讨论弄,清楚了这个项目的重难点,采用了组长分工、小组成员讨论的方式进行该项目工程。分别完成各自部分的功能设计和代码实现,最后再一起细致的改入整个大项目中,也就是先分,再治,最后整合的过程。
我们采用的工具是 VS2019+opencv4.00,在工具统一的情况下进行工作和开发。
三、项目的方案设计
对于项目的整体设计方案,我们采用的是如下方法:
1,首先用glob()函数找到文件中每张图片的路径,根据路径找到将图片保存到Mat对象中,然后开始对每一张图片进行处理。
2,调整图像为统一大小以便进行处理,并将原彩色图片转化成灰度图。
3,对灰度图进行中值滤波降噪处理,以提高识别率。
4,灰度图二值化,采用迭代法求阈值。
5,对图像进行切割。将图像切割成只有最上面一部分的情况,也就是只有 ISBN 号的图片。
6,将第5步切出来的图片进一步切割成为单个数字图像的图片,并进行储存。
7,从上一步得到的单个字符的图片得到最小矩形框,并截出最小矩形的。
8,将切割好的数字与准备好的数字模板进行比对,匹配差值最小的数字。最后输出正确率和准确率。
四、核心代码实现
1.读取图片
//读取 ISBN 图片 string testImgPath = "数据集/*"; vector<String> testImgFN;//必须String glob(testImgPath, testImgFN, false); int testImgNums = testImgFN.size(); for (int index =0; index < testImgNums; index++) { Mat src = imread(testImgFN[index]); }
讲解:这部分完成将图片从文件夹中读取出到Mat对象中的功能,后面对图片的操作就转为对Mat对象的操作。具体来说就是先将文件夹中图片的路径用golb()获取并放在数组vector testImgFN中,然后遍历该数组用imread()函数得到每一张图片。
2.将原图转化为灰度图
代码实现:
void originalImgToGrayImg(Mat inputImg, Mat& outputImg) { int row = inputImg.rows; int col = inputImg.cols; outputImg.create(row, col, CV_8UC1); for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { double sum = 0; //得到三个通道的像素值 int b = inputImg.at<Vec3b>(i, j)[0]; int g = inputImg.at<Vec3b>(i, j)[1]; int r = inputImg.at<Vec3b>(i, j)[2]; //利用灰度化公式将彩色图像三个通道的像素值转化为灰度图像单通道的像素值 sum = b * 0.114 + g * 0.587 + r * 0.299; outputImg.at<uchar>(i, j) = static_cast<uchar>(sum); } } }
代码讲解:
先用两个变量row和col分别来表示原图中每行每列的像素点,再用.create的方法创建一个和原图大小一样的图片,然后用两个循环语句根据灰度化公式将原彩色图像中每三个通道对应的像素点的像素值转化为灰度图中对应的像素点的像素值,由于定义的sum是double类型的,所以还需要使用 static_cast(sum)将sum转换为uchar类型,再赋值给灰度图的各个像素点,从而得到一个和原图大小一样的灰度图。
3.去噪处理
//冒泡排序对非边界值与其八邻域的进行排序,找到中值 int BubbletoMedian(vector<int>& a) { int median;//找到中值 for (int i = 0; i < 9; i++) { for (int j = 0; j < 9 - i - 1; j++) { if (a[j] > a[j + 1]) { int temp; temp = a[j + 1]; a[j + 1] = a[j]; a[j] = temp; } } } median = a[4]; return median; } //去噪处理 void denoising(Mat gray1, Mat& grayImg) { //声明一个与灰度图gray1行数,列数都相同的图grayImg grayImg = Mat(gray1.rows, gray1.cols, CV_8UC1); vector<int>temp(9); //声明动态数组temp[] //定义九个方向 int dx[9] = { 1,-1,1,-1,-1,0,1,0,0 }; int dy[9] = { 1,-1,-1,1,0,1,0,-1,0 }; for (int i = 0;i < gray1.rows;i++) { for (int j = 0;j < gray1.cols;j++) { //边缘部分不做处理 if (i == 0 || i == gray1.rows - 1 || j == 0 || j == gray1.cols - 1) { grayImg.at<uchar>(i, j) = gray1.at<uchar>(i, j); } else { for (int k = 0;k < 9;k++) { //将非边缘的像素点及其八邻域的像素点存入数组temp[]中 temp[k] = gray1.at<uchar>(i + dx[k], j + dy[k]); } //对数组temp[]中的九个值进行冒泡排序求出九个值中的中值 grayImg.at<uchar>(i, j) = BubbletoMedian(temp); } } } }
讲解:用中值滤波法对灰度图gray1进行降噪处理。对gray1图像的边缘不做处理赋值给grayImg;利用两个一维数组dx[],dy[],找到非边缘值的八邻域值,将这九个值存入动态数组temp[9]中,利用冒泡排序的方法将这九个值从小到大进行排序,并返回排序后的中值median,并且将中值median赋值给grayImg的相同位置。
4.迭代法求阈值
代码实现:
void getGrayHistogram(Mat grayImg, int &theThreshold) { //1.求灰度直方图 vector<int>histo(256); for (int i = 0;i < grayImg.rows;i++) { for (int j = 0;j < grayImg.cols;j++) { histo[grayImg.at<uchar>(i, j)]++; } } //2.根据上面的直方图 用迭代法求阈值 int count0, count1;//count0,count1分别是大于t0和小于t0的像素点的个数 int t0 = 127,t=0; //t0是初始的阈值,t是每一次经过迭代运算后的阈值 当t=t0时认为找到 int z0, z1; //z0,z1分别是大于t0和小于t0的像素值的总和 while (1) { count0=count1=z0 = z1= 0; for (int i=0;i<histo.size();i++) { if (i<=t0) { count0+= histo[i]; z0 += i * histo[i]; } else { count1 += histo[i]; z1 += i * histo[i]; } } t = (z0/count0 + z1/count1)/2; if (t0==t) break; else t0 = t; } theThreshold = t0; }
代码讲解:
此处用的是用迭代法求阈值,首先求灰度图的直方图,得到每个像素值对应的像素点的个数,用vectorhisto(256)数组来盛它,它的下标表示像素值,它的值表示每个像素值所对应的像素点的个数。然后再用迭代法来求得阈值,有六个变量,其中t0是初始的阈值,我们最初将像素值范围(0-255)的中心点127赋值给它,t表示经过一次迭代运算得到的阈值,z0表示大于t0的像素值的总和,z1表示小于t0的像素值的总和,count0表示像素值大于t0的像素点的个数,count1表示像素值小于t0的像素点的个数。每经过一次迭代运算,就会得到一个t( t = (z0/count0 + z1/count1)/2;z0/count0是像素值大于t0的所有像素点的平均像素值,z1/count1是像素值小于t0的所有像素点的平均像素值,而(z0/count0 + z1/count1)/2就是新得到的阈值。)将t和t0进行比较,若t=t0,则得到该图像的阈值就为t0,否则令t0等于t,继续进行迭代运算,直至t=t0。
5.水平投影确定行
//水平投影找到行 pair<int, int> SelectRow2(Mat inputImg) { pair<int, int>p; //记录ISBN所在的上界(p.first)和下届(p.second) vector<int>arr(inputImg.rows); //存储每行的水平投影的结果 for (int i = 0;i < inputImg.rows;i++) { //水平投影 for (int j = 0;j < inputImg.cols;j++) if (inputImg.at<uchar>(i, j) != 0)arr[i]++; //当此行的水平投影值大于指定阈值表示找到上届 if (arr[i] > 10) { p.first = i;break; } } for (int i = p.first;i < inputImg.rows;i++) { //水平投影 for (int j = 0;j < inputImg.cols;j++) if (inputImg.at<uchar>(i, j) != 0)arr[i]++; //当此行的水平投影值小于指定阈值表示找到下届 if (arr[i] < 10) { p.second = i;break; } } //有些图片不规则,图中没有全零行单独处理 if (p.second-p.first<=10) p.second = p.first+32; return p; }
讲解:找到ISBN所在行的操作是通过先做水平投影来实现的,具体来说就是:在上一步得到的二值图的基础上,统计每一行像素值不为零的像素点的个数然后再对统计出来的数据进行处理。也就是找到开始出现字符的行(ISBN的上界)和在这之后的第一次出现全零行的情况(ISBN的下届)。这里还有一个判断,上下界之差<10则说明截取失败,需要先截取一个大致范围,再做处理。
6.竖直投影确定列
void sliptCol(Mat inputImg, vector<pair<int, int> >& a) { vector<int>theCol(inputImg.cols); //做竖直投影 for (int i=0;i<inputImg.rows;i++) { for (int j=0;j<inputImg.cols;j++) { if (inputImg.at<uchar>(i, j) != 0) theCol[j]++; } } //用nums区分走右边界,num为偶数则代表左边界 num 为奇数是右边界 int num = 0;pair<int, int>p; for (int j = 0;j < inputImg.cols;j++) { //用theCol[j] >= 3判断 适当把截取范围取大一点 if (theCol[j] >= 3 && num % 2 == 0) { num++; p.first = j;j += 2; } else if (theCol[j] == 0 && num % 2 != 0) { num++; p.second = j; a.push_back(p);j += 2; } } }
讲解:在上一步得到的ISBN所在行后,重新在原图上截取出ISBN所在行再经灰度转化,降噪处理,二值化后进行竖直投影。这里的竖直投影类比上一步的水平投影,我们找的是每一列像素值不为零的像素点的个数。然后根据二值图竖直方向投影结果的数字特征分辨出每一个字符的左边界和右边界。那么这种数字特征是什么?左右边界又如何区分呢?数字特征和上一步类似,即找全零列。正常情况下(我们组的二值图是黑底白字的,因此背景像素值是0,数字像素值是255)开始遇见的都是全0列,所以第一次出现的非零列就是第一个字符的左边界(num=0);紧接着第一次出现的全零行就是右边界(num=1),然后来到了第一个字符和第二个字符的间隙,又都是全0列;在此次出现非列0的就是第二个字符的左边界(num=2)…后面的以此类推,因而当num是偶数时为左边界,num是奇数时是右边界。