摘要
[摘 要]国际标准书号ISBN由13位数字组成。前三位数字代表图书,中间的9个数字分为三组,分表示组号、出版社号和书序号,最后一个数字是校验码从1968年英国的“标准书号”(SBN)开始。其优点主要体现在:国际标准书号是机读的编码,从图书的生产到发行、销售始终如一,对图书的发行系统起了很大的作用:它的引入使图书的定购、库存控制、账目和输出过程等任何图书业的分支程序都简化了。我们小组设计出的这个ISBN编号识别系统利用机器视觉图像处理技术可以识别不同的 ISBN 号并将其读取出来,为事后相应的管理统筹工作提供可靠的辅助。
该项目的开发利用了vs2019以及opencv4.55环境开发,用c++语言来进行编写,本项目的基本流程为:1.图片的读取。2.将读取的图片转化为灰度图片。3.得到中值并且滤波去噪。4.将图片二值化。5.水平投影确定行。6.竖直投影确定列。7.模板匹配,字符识别。8.将结果输出。
目录
结论 22
前言
由于现代社会的高速发展,人民对于书籍的阅读需求以及要求也在不断的提升和高级,这样的需求以及要求便对纸质书籍或者电子书籍的管理以及查询有了更高的要求。同时,ISBN号码是管理书籍的一个重要且高效快速的方式。但是目前为止,关于ISBN码的识别却常常是人工进行识别,然而人工识别无论速度还是效率,都十分的低,并不能满足现代社会对于信息查询的速度以及质量的要求,所以我们就需要计算机来辅助完成这样的工作。但是计算机是无法直接识别这些图片的,所以我们需要将这些图片进行一系列的操作,以便于计算机可以进行识别以及相关的操作。这些操作如下:将需要查询的图片转化为灰度图片,进行中值滤波降噪处理,二值化操作,然后利用水平投影来确定行,利用竖直投影来确定列,找到最小的矩形框,最后得到单个字符的形式,最后进行数字识别。在本项目中,我们利用神经网络法,模板匹配法等操作进行了数字识别,以下便是我们对于图片的具体操作方法。
正文
本次的ISBN编号识别采用模板匹配的方法,在进行识别之前需要对图片进行一系列预处理:原图转灰度图 灰度图转二值图 对二值图进行切割 找到切割后的最小矩形框。模板匹配这种方法具体来说,就是把所有可能出现的每一个字符情况都找一定数量(我们用的有2~4个)的模板,当要识别未知字符时需要与所有模板一一比对,找到最接近的模板进行匹配。这里就有一个衡量标准的问题,即:怎样算“最接近”?我们用的是“找不同的”方法,即先将模板与待识别的字符图片调成一样大小(40X60),再对比对应位置的像素值是否相同,统计两幅图片不同的像素点的个数。如果不同点的数量越少,就认为该模板与待识别的字符越接近;因而最接近自然就是不同的像素点个数最少的模板。
在收到项目课题的内容以及要求之后,我们小组成员进行了激烈且漫长的讨论,大家纷纷提出观点以及奇思妙想,在讨论中,我们首先是确定了在该项目中我们应该关注的重点,最后便确定,采用分工的方式来一同完成该项目的开发,分别完成各个模块的功能以及代码编写。
我们采用的工具是vs2019以及opencv4.55版本。
项目的整体方案设计采用了以下办法:
- 首先读取图片:利用opencv中自带的glob()函数来读取文件中每一张图片的路径,之后根据路径将图片保存到Mat对象中,然后进行没一张图片的后续处理。
- 首先,将图片均调整为统一大小以便后续的处理,并且要将原本的彩色图片转化为灰度图。
- 将灰度图进行中值滤波降噪处理,以便于提高辨识度,提高正确率。
- 将灰度图二值转化,采用迭代的方法来求取阈值。
- 然后将图片进行切割,将图片切割成只有最上面的一部分,也就是将ISBN号切割出来。
- 将第5步操作所切割出来的图片切割成一个个只有单个字符的图片,并且加以储存。
- 将6操作中所得到的字符识别出最小矩形,且截出最小矩形。
- 最后,将切割出的字符图片与准备好的数字模板进行一一匹配,匹配出插值最小的数字,然后输出最终的正确率以及准确率。
- 读取图片功能实现以及代码:
代码:
string testImgPath = "C:\\Users\\m'l's\\Desktop\\数据结构二级项目\\ISBN图片识别\\Project2\\image";
vector<String> testImgFN;//必须cv的String
glob(testImgPath, testImgFN);
int testImgNums = testImgFN.size();
代码讲解:
这部分代码主要功能便是将图片从文件夹中读取出来并且存放在动态数组当中。
其中读取文件我们利用了glob()函数来进行读取,并且利用c++中带有的vector<>容器来进行图片各个路径的存储。然后利用testImgFN.size()函数等得到图片的具体个数,以便于后续循环处理图片时确定循环次数。
- 将原本图片转化为灰度图片功能实现以及代码:
Mat src = imread(testImgFN[index]);//读取图片并且转化为Mat对象//读入矩阵
double width = 400;//定义像素值?
double height = width * src.rows / src.cols;
resize(src, src, Size(width, height));//为Mat对象分配内存
int Width, Height;
Width = src.cols;
Height = src.rows;
Mat dst;//定义一个新的Mat对象
src.copyTo(dst);//copyto函数得到一个和dst一样的对象,src
double degree;
degree = CalcDegree(src);
if (abs(degree) <= 1)//abs函数返回数值的绝对值
rotateImage(src, dst, degree);
Mat resImg = dst(Rect(0, 0, Width, Height)); //根据文本宽高进行裁剪
Mat gray_dst, result_dst;
cvtColor(resImg, gray_dst, COLOR_BGR2GRAY);
附自定义函数代码:
double CalcDegree(const Mat& srcImage)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
Mat midImage, dstImage;
Mat binImg;
cvtColor(srcImage, binImg, COLOR_BGR2GRAY);
threshold(binImg, dstImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
Sobel(dstImage, midImage, -1, 0, 1, 1);求y方向梯度并进行平滑处理
vector<Vec2f> lines;//利用霍夫线变换检测图像中的直线。可以设置多组参数进行检测,其中阈值越大,检测精度越大。
HoughLines(midImage, lines, 1, CV_PI / 180, 190, 0, 0);
float sum = 0;
for (size_t i = 0; i < lines.size(); i++)
{
float alpha = lines[i][0];
float theta = lines[i][1];
sum += theta;
double a1 = cos(theta);
double b1 = sin(theta);
double x1 = a1 * alpha;
double y2 = b1 * alpha;
//后面可以用a1 b1 x1 y1求两点的坐标,并确定一条直线,画出直线检测图
}
float average;
if (!lines.size())
{
average = CV_PI / 2;
//cout << "没检测到:" << average << endl;
}
else
{
average = sum / lines.size();
//cout << "检测到:" << average << endl;
}
//cout << "average theta:" << average << endl;
double angle = degreeTurn(average) - 90;
return angle;
}
//寻找每个字符对应位置
vector<float> FindColRanges(vector<int> inputImg, int max)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
vector<float> pts;
int left = 0;
vector<float> temp;
for (int j = 1; j < inputImg.size() - 1; j++) {
if (inputImg[j] > 0 && inputImg[j - 1] <= 0 && !left) {//左边缘
temp.push_back(j - 1);
left = 1;
}
else if (inputImg[j] > 0 && inputImg[j + 1] <= 0 && left) {//右边缘
temp.push_back(j + 1);
left = 0;
}
if (j == inputImg.size() - 2 && inputImg[j] == max)
{
temp.clear();
}
if (temp.size() == 2 && left == 0)
{
pts.push_back(temp[0]);
pts.push_back(temp[1]);
temp.clear();
}
}
return pts;
}
void rotateImage(Mat Img, Mat& rotateImg, double degree)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
//旋转中心为图像中心
Point2f center;
center.x = float(Img.cols / 2.0);
center.y = float(Img.rows / 2.0);
int length = 0;
length = sqrt(Img.cols * Img.cols + Img.rows * Img.rows);
Mat matrix = getRotationMatrix2D(center, degree, 1);//仿射变换矩阵
warpAffine(Img, rotateImg, matrix, Size(length, length), 1, 0, Scalar(255, 255, 255));//仿射变换
}
double CalcDegree(const Mat& srcImage)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
Mat midImage, dstImage;
Mat binImg;
cvtColor(srcImage, binImg, COLOR_BGR2GRAY);
threshold(binImg, dstImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
Sobel(dstImage, midImage, -1, 0, 1, 1);求y方向梯度并进行平滑处理
vector<Vec2f> lines;//利用霍夫线变换检测图像中的直线。可以设置多组参数进行检测,其中阈值越大,检测精度越大。
HoughLines(midImage, lines, 1, CV_PI / 180, 190, 0, 0);
float sum = 0;
for (size_t i = 0; i < lines.size(); i++)
{
float alpha = lines[i][0];
float theta = lines[i][1];
sum += theta;
double a1 = cos(theta);
double b1 = sin(theta);
double x1 = a1 * alpha;
double y2 = b1 * alpha;
//后面可以用a1 b1 x1 y1求两点的坐标,并确定一条直线,画出直线检测图
}
float average;
if (!lines.size())
{
average = CV_PI / 2;
//cout << "没检测到:" << average << endl;
}
else
{
average = sum / lines.size();
//cout << "检测到:" << average << endl;
}
//cout << "average theta:" << average << endl;
double angle = degreeTurn(average) - 90;
return angle;
}
//寻找每个字符对应位置
vector<float> FindColRanges(vector<int> inputImg, int max)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
vector<float> pts;
int left = 0;
vector<float> temp;
for (int j = 1; j < inputImg.size() - 1; j++) {
if (inputImg[j] > 0 && inputImg[j - 1] <= 0 && !left) {//左边缘
temp.push_back(j - 1);
left = 1;
}
else if (inputImg[j] > 0 && inputImg[j + 1] <= 0 && left) {//右边缘
temp.push_back(j + 1);
left = 0;
}
if (j == inputImg.size() - 2 && inputImg[j] == max)
{
temp.clear();
}
if (temp.size() == 2 && left == 0)
{
pts.push_back(temp[0]);
pts.push_back(temp[1]);
temp.clear();
}
}
return pts;
}
代码讲解:
本操作的核心意义在于将原图片转化为灰度图片,首先我们将读取出来的图片定义为一个个Mat对象(本操作Mat数据结构而不选择IpIImage数据结构是因为考虑到IpIImage需要自行分配内存,容易发生内存泄漏的情况),然后通过copyTo()函数来得到一个与原图大小相同的Mat对象scr,然后利用自定义的函数 CalcDegree()将原图的倾斜角度矫正,并且将原图利用Rect进行裁剪,得到ISBN码,最后将裁剪得出的利用opencv中的库函数cvtColor()转化为灰度图像。
- 绘制直方图功能实现以及代码:
代码:
void drawHist(Mat& img)
{
int n = 0;int m = 0;
for (int i = 0; i < 2; i++){n++;m++;}
for (int i = 0; i < 2; i++){n--;m--;}
//为计算直方图配置变量
//首先是需要计算的图像的通道,就是需要计算图像的哪个通道(bgr空间需要确定计算 b或g货r空间)
int channels = 0;
//然后是配置输出的结果存储的 空间 ,用MatND类型来存储结果
MatND dstHist;
//接下来是直方图的每一个维度的 柱条的数目(就是将数值分组,共有多少组)
int histSize[] = { 256 }; //如果这里写成int histSize = 256; 那么下面调用计算直方图的函数的时候,该变量需要写 &histSize
//最后是确定每个维度的取值范围,就是横坐标的总数
//首先得定义一个变量用来存储 单个维度的 数值的取值范围
float midRanges[] = { 0, 256 };
const float* ranges[] = { midRanges };
calcHist(&img, 1, &channels, Mat(), dstHist, 1, histSize, ranges, true, false);
//calcHist 函数调用结束后,dstHist变量中将储存了 直方图的信息 用dstHist的模版函数 at<Type>(i)得到第i个柱条的值
//at<Type>(i, j)得到第i个并且第j个柱条的值
//开始直观的显示直方图――绘制直方图
//首先先创建一个黑底的图像,为了可以显示彩色,所以该绘制图像是一个8位的3通道图像
Mat drawImage = Mat::zeros(Size(256, 256), CV_8UC3);
//因为任何一个图像的某个像素的总个数,都有可能会有很多,会超出所定义的图像的尺寸,针对这种情况,先对个数进行范围的限制
//先用 minMaxLoc函数来得到计算直方图后的像素的最大个数
double g_dHistMaxValue;
minMaxLoc(dstHist, 0, &g_dHistMaxValue, 0, 0);
//将像素的个数整合到 图像的最大范围内
//遍历直方图得到的数据
for (int i = 0; i < 256; i++)
{
int value = cvRound(dstHist.at<float>(i) * 256 * 0.9 / g_dHistMaxValue);
line(drawImage, Point(i, drawImage.rows - 1), Point(i, drawImage.rows - 1 - value), Scalar(255, 255, 255));
}
}
代码讲解:
首先,在绘制直方图时,先要计算直方图的通道,并且为直方图配置空间,在这里我们利用MatND类型的数据进行存储,然后将每一个维度的数目进行存储,共有256种,calcHist 函数调用结束后,dstHist变量中将储存了 直方图的信息 用dstHist的模版函数 at<Type>(i)得到第i个柱条的值 at<Type>(i, j)得到第i个并且第j个柱条的值。
然后开始绘制直方图,首先要将图像创建为一个黑底的图像,将图像绘制成一个8位的3通道图像。最后遍历直方图得到信息。
- 利用迭代法求出阈值功能实现以及代码:
int DetectThreshold(Mat* src)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
uchar iThrehold;//阀值
try
{
int height = src->cols;
int width = src->rows;
int step = src->rows / sizeof(uchar);
uchar* data = src->data;
int iDiffRec = 0;
int F[256] = { 0 }; //直方图数组
int iTotalGray = 0;//灰度值和
int iTotalPixel = 0;//像素数和
uchar bt;//某点的像素值
uchar iNewThrehold;//新阀值
uchar iMaxGrayValue = 0, iMinGrayValue = 255;//原图像中的最大灰度值和最小灰度值
uchar iMeanGrayValue1, iMeanGrayValue2;
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
bt = data[i * step + j];
if (bt < iMinGrayValue)
iMinGrayValue = bt;
if (bt > iMaxGrayValue)
iMaxGrayValue = bt;
F[bt]++;
}
}
iThrehold = 0;
iNewThrehold = (iMinGrayValue + iMaxGrayValue) / 2;//初始阀值
iDiffRec = iMaxGrayValue - iMinGrayValue;
for (int a = 0; (abs(iThrehold - iNewThrehold) > 0.5); a++)//迭代中止条件
{
iThrehold = iNewThrehold;
for (int i = iMinGrayValue; i < iThrehold; i++)
{
iTotalGray += F[i] * i;//F[]存储图像信息
iTotalPixel += F[i];
}
iMeanGrayValue1 = (uchar)(iTotalGray / iTotalPixel);
//大于当前阀值部分的平均灰度值
iTotalPixel = 0;
iTotalGray = 0;
for (int j = iThrehold + 1; j < iMaxGrayValue; j++)
{
iTotalGray += F[j] * j;//F[]存储图像信息
iTotalPixel += F[j];
}
iMeanGrayValue2 = (uchar)(iTotalGray / iTotalPixel);
iNewThrehold = (iMeanGrayValue2 + iMeanGrayValue1) / 2; //新阀值
iDiffRec = abs(iMeanGrayValue2 - iMeanGrayValue1);
}
}
catch (cv::Exception e)
{
}
return iThrehold;
}
代码讲解:
这里利用迭代法来求取阈值,首先要先求灰度图的直方图,然后再得到每个像素值所对应的像素点的个数,用数组来存放,下标便为像素值大小,然后利用迭代法来求取阈值,首先现将0-255的中值作为初始阈值,然后z0就是大于初始阈值的像素值的总和,z1就是小于初始阈值的像素值的总和。Count0是大于初始阈值的个数,cunt1是小于初始阈值的个数,然后根据公式,新阈值t= (z0/count0 + z1/count1)/2,求出平均像素值,然后只有当t=t0时,得到该图像的阈值就是t0否则,依旧进行迭代。
- 数字识别功能实现以及代码:
void res(int& rtNum, int& accNums, int& sunNums, Mat binImg, string name)
{
int n = 0; int m = 0;
for (int i = 0; i < 2; i++) { n++; m++; }
for (int i = 0; i < 2; i++) { n--; m--; }
bitwise_not(binImg, binImg);//颜色反转
string cmpData = realISBN(name);
int st, ed;
findRows(st, ed, binImg);
Mat subImg = Mat(binImg, Range(st, ed), Range(0, binImg.cols));//截取原图相应部分
vector<int> colNums(subImg.cols, 0);
for (int i = 0; i < subImg.rows; i++) {
for (int j = 1; j < subImg.cols - 1; j++) {
if (subImg.at<uchar>(i, j) != 0) {
colNums[j]++;
}
}
}//寻找字符边界
vector<float>pts = FindColRanges(colNums, subImg.rows);
vector<Mat> unClear;
unClear.push_back(binImg);
unClear.push_back(subImg);
//截取字符并识别
string result = "";
for (int j = 0; j < pts.size() - 1 && pts.size()>0; j += 2) {//j 为左边界,j+1 为右边界//截取当前字符所在区域,方便后续操作
Mat roiImg = Mat(subImg, Range(0, subImg.rows), Range(pts[j], pts[j + 1]));
//寻找最小正矩形,并排除不满足条件的矩形
vector<vector<Point> >contours;
findContours(roiImg, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);//检索物体轮廓
for (int i = 0; i < contours.size(); i++) {
Rect temRect = boundingRect(contours[i]);//查找最小矩形
if (temRect.height < subImg.rows / 2) {//排除 -
continue;
}
Mat rectImg = Mat(roiImg, temRect);
resize(rectImg, rectImg, Size(40, 50));
unClear.push_back(rectImg);
char letters = CheckImg(rectImg, 5);
if (letters >= '0' && letters <= '9') {
result += letters;
}
}
}
int ind = result.find('9');
if (ind != -1 && ind <= 4)
result.erase(0, ind);
else
result.erase(0, 4);
if (result.length() > cmpData.length())
{
string tem = result.substr(result.length() - cmpData.length());
if (tem != cmpData)
{
tem = result.substr(0, cmpData.length());
}
result = tem;
}
else if (result.length() < cmpData.length())
{//有字符未被识别
int i;
for (i = 0; i < result.length(); i++)
{
if (result[i] != cmpData[i])
{
break;
}
}
string r1 = result.substr(0, i);
string r2 = result.substr(i);
string r3 = "";
for (int j = 0; j < cmpData.length() - result.length(); j++)
{
r3 += " ";
}
result = r1 + r3 + r2;
}
cout << "res" << result << endl << "cmp" << cmpData << endl;
sunNums += cmpData.length();
for (int i = 0; i < cmpData.length(); i++)//准确率计算
{
if (result[i] == cmpData[i])
{
accNums++;
}
}
if (result == cmpData)//正确率计算
{
rtNum++;
cout << "Yes" << endl;
}
else
{
for (int i = 0; i < num; i++)
{
imwrite("/unclear/" + to_string(num) + ".jpg", unClear[i]);
num++;
}
cout << "NO!" << endl;
}
cout << endl;
}
代码讲解:
字符识别用的是模板匹配这种方法实现的,具体来说就是把所有可能出现的每一个字符情况都找一定数量(我们用的在2~4个)的模板,当要识别未知字符时需要与所有模板一一比对,找到最接近的模板进行匹配。这里就有一个衡量标准的问题,即:怎样算“最接近”?我们用的是“找不同的”方法,即先将模板与待识别的字符图片调成一样大小(40X60),再对比对应位置的像素值是否相同,统计两幅图片不同的像素点的个数。如果不同点的数量越少,就认为该模板与待识别的字符越接近;因而最接近自然就是不同的像素点个数最少的模板。上面两个函数中absDi()就是用来统计不同的像素点个数的函数,而在 recognition()中就是通过找“最接近”达到模板匹配,字符识别的目的。
- 项目测试
- 图片的读取效果:
- 灰度图效果:
3.去噪后效果
4.二值化效果
5.旋转后效果:
6:截取所在行效果:
7.最小矩形效果:
8.二值化
- 最后结果:
- 研究结果并讨论
最终,我们不断的对代码进行改进和调试,才最终将准确率和正确率提升到了一个较令人满意的程度,我们这次在规定的时间下完成了这样的一个ISBN识别系统我们感到荣耀与自豪,在这个项目中,我们接触到了opencv这样的一个之前我们闻所未闻的开发环境,并且,我们将opencv与vs2019相结合,最终我们通过c++语言最终编写成功了这个系统,这样的成功是我们共同的努力的结果。
结论
简要总结项目的主要工作、主要结果、心得感受主要发现以及下一步应当开展的主要工作等。
在本项目中我们主要任务是开发ISBN识别系统,其中运用了简单的计算机视觉技术,以实现数字识别的功能。在我们小组成员的不断努力之后,我们对训练集的测试结果为:正确率达到了九成以上,识别效果可观,可以说是较高质量的完成了这套 ISBN 识别系统的。这样的效果离不开组内成员间的齐心协力,是我们互相的鼓励让我们不畏艰难险阻砥砺前行,在代码有BUG的时候互相支持。我们能够开发出这个ISBN识别系统,也源自我们永不放弃,越挫越勇的品质。下一步准备加一个从外界读取图片的系统,以达到自动识别书籍ISBN编号的功能