燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

概括

国际标准书号ISBN由13位数字组成。前三位数字代表图书,中间的9个数字分为三组,分表示组号、出版社号和书序号,最后一个数字是校验码从1968年英国的“标准书号”(SBN)开始。其优点主要体现在:国际标准书号是机读的编码,从图书的生产到发行、销售始终如一,对图书的发行系统起了很大的作用:它的引入使图书的定购、库存控制、账目和输出过程等任何图书业的分支程序都简化了。我们小组设计出的这个ISBN编号识别系统利用机器视觉图像处理技术可以识别不同的 ISBN 号并将其读取出来,为事后相应的管理统筹工作提供可靠的辅助。

本系统开发采用了 VS2019+opencv的开发环境,C++语言实现。该项目流程为:读取->转化为灰度图->中值滤波去噪->二值化->水平投影确定行->竖直投影确定列 ->模板匹配字符识别->输出。

[关键词] 课程实践项目;数字识别;灰度变换;中值滤波降噪;二元变换;字符分割

前言

随着现代社会的高速发展,人民的生活水平也在不断提高,阅读书籍的需求也在不断上涨,这对书籍管理水平提出了更高要求,而ISBN编号能极大的方便对书籍的管理,随之而来的问题是:人工识别ISBN码的效率太低了,需要计算机来代替我们做这些工作。但计算机不能直接识别图片中的内容,所以要求我们对图片进行一系列的操作,使它变得易于被识别。这些操作包括:转化为灰度图,中值滤波去噪,二值化,水平投影分割行,竖直投影切割列,找最小矩形框,最后得到单个字符的形式,之后进行数字识别。这里数字识别主要方法有:模板匹配法,神经网络法以及划线法。本小组采用模板匹配法来对字符进行识别。下面就分别介绍对图片的一系列操作和模板匹配的方法。

文本

一、研究内容的基本原则

本次的ISBN编号识别采用模板匹配的方法,在进行识别之前需要对图片进行一系列预处理:原图转灰度图 灰度图转二值图 对二值图进行切割 找到切割后的最小矩形框。模板匹配这种方法具体来说,就是把所有可能出现的每一个字符情况都找一定数量(我们用的有2~4个)的模板,当要识别未知字符时需要与所有模板一一比对,找到最接近的模板进行匹配。这里就有一个衡量标准的问题,即:怎样算“最接近”?我们用的是“找不同的”方法,即先将模板与待识别的字符图片调成一样大小(40X60),再对比对应位置的像素值是否相同,统计两幅图片不同的像素点的个数。如果不同点的数量越少,就认为该模板与待识别的字符越接近;因而最接近自然就是不同的像素点个数最少的模板。

2. 使用的研究方法和相关工具

拿到项目后,我们小组一起讨论、摸索,摸清了项目的重点和难点。我们采用组长分工、组员讨论的方式开展项目。分别完成各自部分的功能设计和代码实现,最后将它们一起精心修改成整个大项目,即先划分、再治理、最后整合的过程。
我们采用的工具是 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是奇数时是右边界。

7.找最小矩形框

void findMinRectangle(Mat inputImg, int& st, int& ed, int& let, int& righ)
{
//inputImg是输入的二值图
    for (int i = 0;i < inputImg.rows;i++)
    {
        int j;
        for (j = 0;j < inputImg.cols;j++)
        {
            if (inputImg.at<uchar>(i, j) != 0)
            {
                st = i;
                break;
            }
        }//找到第一行不全为0的行,并将该行作为截取的上界
        if (inputImg.at<uchar>(i, j) != 0)
        {
            break;
        }
    }
    for (int i = inputImg.rows-1;i >= 0;i--)
    {
        int j;
        for (j = 0;j < inputImg.cols;j++)
        {
            if (inputImg.at<uchar>(i, j) != 0)
            {
                ed = i;
                break;
            }
      }
//对矩阵的行反向遍历找到最后一行不全为0的行,并将该行作为截取的下界
        if (inputImg.at<uchar>(i, j) != 0)
        {
            break;
        }
    }
    for (int j = 0;j < inputImg.cols;j++)
    {
        int i;
        for (i = 0;i < inputImg.rows;i++)
        {
            if (inputImg.at<uchar>(i, j) != 0)
            {
                let = j;
                break;
            }
}//找到第一列不全为0的列,并将该列作为截取的左界
        if (inputImg.at<uchar>(i, j) != 0)
        {
            break;
        }
    }
    for (int j = inputImg.cols - 1;j >= 0;j--)
    {
        int i;
        for (i = 0;i < inputImg.rows;i++)
        {
            if (inputImg.at<uchar>(i, j) != 0)
            {
                righ = j;
                break;
            }
        }//对矩阵的列反向遍历找到最后一列不全为0的列,并将该列作为截取的右界
        if (inputImg.at<uchar>(i, j) != 0)
        {
            break;
        }
    }
}

讲解:利用for循环对inputImg的行和列进行正向与反向的遍历,通过找到不全为0的行和列找到inputImg二值图的上界st,下界ed,左界let,右界righ,并根据st,ed,let,righ对inputImg进行截取,找到截取出的数字或字符的最小矩形框。

8.模板匹配 字符识别

//两张图做差(统计两张图片不同点像素点的个数)
double absDi(Mat inputImg, Mat sampleImg)
{
    //记录两张图不同的像素点的个数
	double diffNums = 0;double sameNums = 0;
    for (int i=0;i<inputImg.rows;i++)
    {
        for (int j=0;j<inputImg.cols;j++)
        {
            //对应位置的像素值不同 则diffnums++
			if (inputImg.at<uchar>(i, j) != sampleImg.at<uchar>(i, j))
				diffNums++;
			else sameNums++;
        }
    }
    return diffNums;
}
//字符识别
char recognition(Mat inputImg,int k)
{
    string sampleImgPath = "样例3";
    vector<String> sampleImgFN;
    glob(sampleImgPath, sampleImgFN, false);
    int sampleImgNums = sampleImgFN.size();
    vector<pair< double,int> >nums(sampleImgNums+1);
    for (int i = 0; i < sampleImgNums; i++) {
        nums[i].second= i;
        Mat numImg = imread(sampleImgFN[i], 0);
        //大小同一
        resize(numImg, numImg, Size(40, 60));
        resize(inputImg,inputImg,Size(40,60));
        nums[i].first = absDi(inputImg, numImg);
    }
	//imshow("图片", inputImg);
	//waitKey();
	//排序 越小说明匹配度越高
	sort(&nums[0], &nums[sampleImgNums]);
    int index= nums[0].second;
	//截取模板的函数
	splitSample(index,k,inputImg);
	return sampleImgFN[index][sampleImgPath.size() + 1];
}

讲解:字符识别用的是模板匹配这种方法实现的,具体来说就是把所有可能出现的每一个字符情况都找一定数量(我们用的在2~4个)的模板,当要识别未知字符时需要与所有模板一一比对,找到最接近的模板进行匹配。这里就有一个衡量标准的问题,即:怎样算“最接近”?我们用的是“找不同的”方法,即先将模板与待识别的字符图片调成一样大小(40X60),再对比对应位置的像素值是否相同,统计两幅图片不同的像素点的个数。如果不同点的数量越少,就认为该模板与待识别的字符越接近;因而最接近自然就是不同的像素点个数最少的模板。上面两个函数中absDi()就是用来统计不同的像素点个数的函数,而在 recognition()中就是通过找“最接近”达到模板匹配,字符识别的目的。

5. 项目测试

1.图片读取效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

2.原图转灰度图效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发
3.去噪效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

4.二值化效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

5.图片旋转效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

6.截取所在行效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

7.裂分割 最小矩形的效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

8.识别效果
燕山大学课程实践项目报告:ISBN号识别系统的设计与开发

六、研究成果与讨论

最终在不断测试用例,更换模板,调整代码之后,我们将正确率和准确率提升到了一个比较令人满意的程度,在规定时间内也完成了ISBN识别系统。这个过程中我们初步接触了 opencv+VS 的开发环境,提高了 C++语言的应用能力,也开始了解计算机视觉方面的技术以及其研究前景。明白了小组通力合作的重要性。相信在以后的项目中我们会活用这次经验来更好的迎接挑战。

综上所述

在本项目中我们主要任务是开发ISBN识别系统,其中运用了简单的计算机视觉技术,以实现数字识别的功能。在我们小组成员的不断努力之后,我们对训练集的测试结果为:正确率达到了九成以上,识别效果可观,可以说是较高质量的完成了这套 ISBN 识别系统的。这样的效果离不开组内成员间的齐心协力,是我们互相的鼓励让我们不畏艰难险阻砥砺前行,在代码有BUG的时候互相支持。我们能够开发出这个ISBN识别系统,也源自我们永不放弃,越挫越勇的品质。下一步准备加一个从外界读取图片的系统,以达到自动识别书籍ISBN编号的功能。

附录

开发环境

编译环境:Windows10
编译工具:VS2017
OpenCV库版本:3.4.1

源代码

#include<opencv.hpp>
#include<opencv2/opencv.hpp>
#include<iostream>
#include<vector>
#include<string>
using namespace cv;
using namespace std;
void splitSample(char ch,int k,Mat inputImg)
{
	string st =  to_string(k) + ".jpg";
    imwrite("D:\\二级项目图片\\"+to_string(ch) +"."+ st, inputImg);
		
} 
//计算修正角度
double GetTurnTheta(Mat inputImg) {
	//计算垂直方向导数
	Mat yImg;
	Sobel(inputImg, yImg, -1, 0, 1, 5);
	//直线检测
	vector<Vec2f>lines;
	HoughLines(yImg, lines, 1, CV_PI / 180, 180);

	//计算旋转角度
	float thetas = 0;
	for (int i = 0; i < lines.size(); i++) {
		float theta = lines[i][1];
		thetas += theta;
	}

	if (lines.size() == 0) {//未检测到直线
		thetas = CV_PI / 2;
	}
	else {//检测到直线,取平均值
		thetas /= lines.size();
	}
	return thetas;
}
/*--------------------------------------------------------------------------------------------------------------------------
 Function: originalImgToGrayImg
 Description:将原图转化成灰度图
 ---------------------------------------------------------------------------------------------------------------------------
 Calls:NONE
 Called By: main
 Table Accessed: NONE
 Table Updated:  NONE
 ---------------------------------------------------------------------------------------------------------------------------
  Input:
			第一个参数:原图,Mat
			第二个参数:转出的灰度图,Mat
  Output : 灰度图
  Return :  None
  Others:    NONE
--------------------------------------------------------------------------------------------------------------------------*/
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);
		}
	}
}
//冒泡排序对非边界值与其八邻域的进行排序,找到中值
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)
{
	grayImg = Mat(gray1.rows, gray1.cols, CV_8UC1);
	vector<int>temp(9); //定义九个方向
	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[k] = gray1.at<uchar>(i + dx[k], j + dy[k]);
				}
				grayImg.at<uchar>(i, j) = BubbletoMedian(temp);
			}
		}
	}
}
//迭代法求阈值
void getThreshold(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-10;
}
// 根据灰度直方图得到二值图
Mat toBinaryGraph( Mat grayImg)
{
    int theThreshold;
    //求阈值
	getThreshold(grayImg, theThreshold);
    Mat binG=Mat(grayImg.rows,grayImg.cols, CV_8UC1);
	for (int i = 0;i < grayImg.rows;i++)
	{
		for (int j = 0;j < grayImg.cols;j++)
		{
              if (grayImg.at<uchar>(i, j) < theThreshold)binG.at<uchar>(i, j) = 255;
               else binG.at<uchar>(i, j) = 0;
		}
	}
    return binG;
}
//水平投影找到行
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;
}
pair<int, int> SelectRow(Mat inputImg)
{
	pair<int, int>a;vector<int>arr(inputImg.rows);
	bool find = false;
	for (int i = 0; i < inputImg.rows; i++)
	{
		for (int j = 0; j < inputImg.cols; j++)
		{    //0表示黑 255表示白
			if (inputImg.at<uchar>(i, j) == 255)
			{
				a.first = i;
				find = true;
				break;
			}
			if (find)break;
		}
	}
	for (int i = a.first + 1; i < inputImg.rows; i++)
	{
		for (int j = 0; j < inputImg.cols; j++)
		{
			//0表示黑 255表示白
			if (inputImg.at<uchar>(i, j) == 255)
				arr[i]++;//这里是在水平投影
		}
		if (arr[i] == 0)
		{
			a.second = i;break;
		}
	}
	//有些图片不规则,图中没有全零行模糊处理
	if (a.second - a.first <= 10) a.second = a.first + 32;
	return a;
}
//竖直投影分割列
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;
        }
	}
    
}
//找最小矩形框
void findMinRectangle(Mat inputImg, int& st, int& ed, int& le, int& ri)
{
    vector<int>hProjectoin(inputImg.rows);
    vector<int>vProjectoin(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) { hProjectoin[i]++;vProjectoin[j]++; }
        }
    }
   st = le = 0;ed = inputImg.rows;ri = inputImg.cols;
	for (int i = 0;i < inputImg.rows;i++)
		if (hProjectoin[i] != 0) ed = i;
	for (int i = 0;i < inputImg.cols;i++)
		if (vProjectoin[i] != 0)ri = i;
	for (int i = inputImg.rows - 1;i >= 0;i--)
		if (hProjectoin[i] != 0) st = i;
	for (int i = inputImg.cols - 1;i >= 0;i--)
		if (vProjectoin[i] != 0)le = i;

}
//两张图做差(统计两张图片不同点像素点的个数)
double absDi(Mat inputImg, Mat sampleImg)
{
    //记录两张图不同的像素点的个数
	double diffNums = 0;double sameNums = 0;
    for (int i=0;i<inputImg.rows;i++)
    {
        for (int j=0;j<inputImg.cols;j++)
        {
            //对应位置的像素值不同 则diffnums++
			if (inputImg.at<uchar>(i, j) != sampleImg.at<uchar>(i, j))
				diffNums++;
			else sameNums++;
        }
    }
    return diffNums;
}
//字符识别
char recognition(Mat inputImg,int k)
{
    string sampleImgPath = "样例3";
    vector<String> sampleImgFN;
    glob(sampleImgPath, sampleImgFN, false);
    int sampleImgNums = sampleImgFN.size();
    vector<pair< double,int> >nums(sampleImgNums+1);
    for (int i = 0; i < sampleImgNums; i++) {
        nums[i].second= i;
        Mat numImg = imread(sampleImgFN[i], 0);
        //大小同一
        resize(numImg, numImg, Size(40, 60));
        resize(inputImg,inputImg,Size(40,60));
        nums[i].first = absDi(inputImg, numImg);
    }
	imshow("图片", inputImg);
	waitKey();
	//排序 越小说明匹配度越高
	sort(&nums[0], &nums[sampleImgNums]);
    int index= nums[0].second;
	//截取模板的函数
	//splitSample(sampleImgFN[index][sampleImgPath.size() + 1],k,inputImg);
	return sampleImgFN[index][sampleImgPath.size() + 1];
   /* if (index >= 0 && index <= 9)
        return index+'0';
    else if (index == 10) return 'B';
    else if (index == 11) return 'I';
    else if (index == 12) return 'N';
    else if (index == 13) return 'S';
    else if (index == 14) return 'X';
    else return ' ';*/
}
int main()
{
    int rtNums = 0, accNums = 0, sunNums = 0;//分别代表:正确的数量,被准确识别的字符的数量,要识别的字符的总和
   //读取 ISBN 图片
    string testImgPath = "数据集B/*";
    vector<String> testImgFN;//必须cv的String
    glob(testImgPath, testImgFN, false);
    int testImgNums = testImgFN.size();

    for (int index =12; index < testImgNums; index++) {
        Mat src = imread(testImgFN[index]);
        //图片大小统一
        double width = 700;
        double height = width * src.rows / src.cols;
        resize(src, src, Size(width, height));
        imshow("原图",src);
        Mat gray1;
        //原图转灰度图
        originalImgToGrayImg(src, gray1);
        imshow("灰度图", gray1);

        //去噪处理
        denoising(gray1,gray1);
        imshow("去噪后的图片", gray1);
        //二值化
        Mat binImg = toBinaryGraph(gray1);
        imshow("二值图", binImg);
        //计算调整角度
        double thetas = GetTurnTheta(binImg);
        thetas = 180 * thetas / CV_PI - 90;

        //旋转二值图像
        Mat bin;
        Mat M = getRotationMatrix2D(Point(width / 2, height / 2), thetas, 1);
        warpAffine(binImg, bin, M, binImg.size());
        imshow("旋转后的二值图", bin); 
		//waitKey();
		
        //旋转原图
		Mat bin2;
		warpAffine(src, bin2, M, src.size());
        imshow("旋转的原图", bin2);

        //从原图上截取ISBN所在行
        Mat temp = Mat(bin2, Range(SelectRow2(bin).first, SelectRow2(bin).second+1), Range(0, bin.cols));
        imshow("原图上的所在行",temp);
        //大小统一一
        resize(temp,temp, Size(60 * (temp.cols / temp.rows), 60));
        //转灰度图
        Mat gray2;
        originalImgToGrayImg(temp, gray2);
        //去噪处理
        denoising(gray2,gray2);
        //二值化
		Mat binImg2 = toBinaryGraph(gray2);
        imshow("原图所在行二值化 ", binImg2); 
	     //waitKey();
        
        //分割ISBN所在行
        vector<pair<int, int> >a;//{(110,125),(135,150)。。。。}
        sliptCol(binImg2,a);

        int st, ed, le, ri;
        string result = "";
        for (int i = 0;i < a.size();i++)
        {
              //做列分割
              Mat subImg = Mat(binImg2, Range(0, binImg2.rows), Range(a[i].first, a[i].second+1));
              findMinRectangle(subImg, st, ed, le, ri);
              //ed - st > subImg.rows / 2如果最小矩形的高小于原来的一半 判断为‘-’宽过小判断为杂纹
              if (ed - st > subImg.rows / 2&&ri-le>3)
              {
                  //切出最小矩形的
                  Mat minImg = Mat(subImg, Range(st, ed+1), Range(le, ri+1));
                  imshow("最小矩形", minImg);
                  //模板匹配得到字符
                  char ch = recognition(minImg,index);

				  if (ch >= '0' && ch <= '9' || ch == 'X') {
					  result += ch;
					  cout << ch << " ";
				  }
                
              }
              
          }  cout << endl;
		  //确定正确的 ISBN 号,来跟识别出来的 ISBN 做对比
		  string cmpData = "";
		  for (int i = 0; i < testImgFN[index].length(); i++) {
			  if (testImgFN[index][i] >= '0' && testImgFN[index][i] <= '9' || testImgFN[index][i] == 'X' ){//|| testImgFN[index][i] == 'I' || testImgFN[index][i] == 'S' || testImgFN[index][i] == 'B' || testImgFN[index][i] == 'N') {
				  cmpData += testImgFN[index][i];
			  }
		  }
          cout << cmpData << endl;
          sunNums += cmpData.length();
          cout << result <<endl<<index << endl;
          int times = min(cmpData.length(), result.length());
          bool flag = true;
          for (int i=0;i<times;i++)
          {
              if (cmpData[i] != result[i])
                  flag = false;
              else accNums++;
          }

          if (flag&&result.length()==cmpData.length()) { rtNums++; cout << "Yes" << endl; }
          else cout << "No" << endl;


    }
	printf("正确个数:%4.d 正确率:%f\n", rtNums, rtNums * 1.0 / testImgNums);
	printf("准确个数:%4.d 准确率:%f\n", accNums, accNums * 1.0 / sunNums);
	waitKey(0);
} 

版权声明:本文为博主流楚丶格念原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_45525272/article/details/123221140

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2022年3月3日 下午2:20
下一篇 2022年3月3日 下午2:41

相关推荐