欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于opencv的数字识别

程序员文章站 2022-04-03 22:32:41
...

    最近学习了opencv,然后想通过其对图片上的数字进行识别,参考了网上几篇关于opencv数字识别的博客,我自己也写了一个程序玩玩。我是在vs2017和opencv3.4.1环境下实现的。

    这里先说一下我的思路和步骤:

  1. 加载需要识别的图片,然后将其转化为二值图
  2. 寻找数字的外轮廓,切记不可以找全部轮廓,否则一个数字将会有多个轮廓,识别起来就很麻烦了
  3. 对轮廓进行排序,因为使用findcontours函数寻找到的轮廓顺序不一定是我们想要的,因此需要对找到的数字轮廓根据输入的图片进行排序
  4. 按顺序将数字轮廓分割,一个一个提取出来
  5. 进行识别和匹配,将提取出来的数字与模板数字(需要自己创建)进行比较,这里采用两幅图片像素相减的方法,寻找出差值最小的图片即为最匹配的数字 

一、图片预处理

    将输入的图片转化为二值图以便寻找轮廓

        //输入要识别的图片,并显示
	Mat srcImage = imread("number.jpg");
	imshow("原图", srcImage);
	//对图像进行处理,转化为灰度图然后再转为二值图
	Mat grayImage;
	cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);
	Mat binImage;
	//第4个参数为CV_THRESH_BINARY_INV是因为我的输入原图为白底黑字
	//若为黑底白字则选择CV_THRESH_BINARY即可
	threshold(grayImage, binImage, 100, 255, CV_THRESH_BINARY_INV);

二、寻找数字轮廓

     

        //寻找轮廓,必须指定为寻找外部轮廓,不然一个数字可能有多个轮廓组成,比如4,6,8,9等数字
	Mat conImage = Mat::zeros(binImage.size(), binImage.type());
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	//指定CV_RETR_EXTERNAL寻找数字的外轮廓
	findContours(binImage, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	//绘制轮廓
	drawContours(conImage, contours, -1, 255);

 

 三、对轮廓进行排序

     在排序之前呢,我们需要定义一个类,用来存放轮廓的外接矩阵以及方便后续的排序

class myRect
{
public:
	myRect(){}
	~myRect(){}
	myRect(Rect &temp):myRc(temp){}
	//比较矩形左上角的横坐标,以便排序
	bool operator<(myRect &rect)
	{
		if (this->myRc.x < rect.myRc.x)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	//重载赋值运算符
	myRect operator=(myRect &rect)
	{
		this->myRc = rect.myRc;
		return *this;
	}
	//获取矩形
	Rect getRect()
	{
		return myRc;
	}
private:
	Rect myRc;//存放矩形
};

 

    有了这个类之后,我们可以将一个一个轮廓外接矩阵保存于该类中。而且类中重载了比较操作符,很容易对轮廓进行排序。

        //将每个数字,分离开,保存到容器中
	vector<myRect> sort_rect;
	for (int i = 0; i < contours.size(); i++)
	{
		//boundingRect返回轮廓的外接矩阵
		Rect tempRect = boundingRect(contours[i]);
		sort_rect.push_back(tempRect);
	}

    排序算法的话,采用比较简单的冒泡法

        //对矩形进行排序,因为轮廓的顺序不一定是数字真正的顺序
	for (int  i = 0; i < sort_rect.size(); i++)
	{
		for (int j = i + 1; j < sort_rect.size(); j++)
		{
			if (sort_rect[j] < sort_rect[i])
			{
				myRect temp = sort_rect[j];
				sort_rect[j] = sort_rect[i];
				sort_rect[i] = temp;
			}
		}
	}

    这样,sort_rect容器中的轮廓矩形是按我们输入的图片中的数字顺序存放的。

四、加载(新建)数字模板

    我们在进行数字匹配时,需要先加载模板进行比较。如果没有就要先新建一个。新建模板需要输入的是0-9的数字模板,不用修改程序,只需要添加以下代码。

        /*加载模板,若没有则需自己新建一个*/

	//新建,运行一次就好,不过制作模板的材料为0-9十个数字的图像
	for (int i = 0; i < 10; i++)
	{
		Mat ROI = conImage(sort_rect[i].getRect());
		Mat dstROI;
		resize(ROI, dstROI, Size(40, 50),0, 0, INTER_NEAREST);
		char name[64];
		sprintf(name, "C:/Users/Administrator/Desktop/number_recognition/number_recognition/image/%d.jpg", i);
		//imshow(str, dstROI);
		imwrite(name, dstROI);
	}

    制作模板其实就是将我们需要的ROI区域保存为图片,代码中的路径是我的路径,可以根据自己情况修改。    

    然后是加载模板,也就是从文件中将各个模板图片读入,路径根据自己的情况修改。

        //加载模板
	vector<Mat> myTemplate;
	for (int i = 0; i < 10; i++)
	{
		char name[64];
		sprintf(name, "C:/Users/Administrator/Desktop/number_recognition/number_recognition/image/%d.jpg", i);
		Mat temp = imread(name, 0);
		myTemplate.push_back(temp);
	}

五、分割数字 

    分割数字比较容易,即通过轮廓外接矩形在二值图片上寻找我们要使用的ROI,然后分别保存下来,以供识别。根据排好序的sort_rect可以分割出待识别的数字。

        //按顺序取出和分割数字
	vector<Mat> myROI;
	for (int i = 0; i < sort_rect.size(); i++)
	{
		Mat ROI;
		ROI = conImage(sort_rect[i].getRect());
		Mat dstROI = Mat::zeros(myTemplate[0].size(),myTemplate[0].type());
		resize(ROI, dstROI, myTemplate[0].size(), 0, 0, INTER_NEAREST);
		myROI.push_back(dstROI);
	}

 

六、比较和匹配

    我采用的比较和匹配方法是,将absdiff计算模板和待识别数字的差值,然后比较出差值最小的即为最匹配的数字,从而实现匹配。在匹配前我们需要定义一个getPiexSum函数以计算两幅图片的差值的像素之和。

//求图片的像素和
int getPiexSum(Mat &image)
{
	int sum = 0;
	for (int i = 0; i < image.cols; i++)
	{
		for (int j = 0; j < image.rows; j++)
		{
			sum += image.at<uchar>(j, i);
		}
	}
	return sum;
}

    下面进行匹配和输出

        //进行比较,将图片与模板相减,然后求全部像素和,和最小表示越相似,进而完成匹配
	vector<int> seq;//顺序存放识别结果
	for (int i = 0; i < myROI.size(); i++)
	{
		Mat subImage;
		int sum = 0;
		int min = 100000;
		int min_seq = 0;//记录最小的和对应的数字
		for (int j = 0; j < 10; j++)
		{
			//计算两个图片的差值
			absdiff(myROI[i], myTemplate[j], subImage);
			sum = getPiexSum(subImage);
			if (sum < min)
			{
				min = sum;
				min_seq = j;
			}
			sum = 0;
		}
		seq.push_back(min_seq);
	}

	//输出结果
	cout << "识别结果为:";
	for (int i = 0; i < seq.size(); i++)
	{
		cout << seq[i];
	}
	cout << endl;

七、总结

        识别的结果图片:

基于opencv的数字识别

    存在的不足:这种识别方式有个不好的点就是,模板的数字大小(比如我用的是48号字体大小)不能与待识别的数字的大小相差太多,否则会降低识别准确率。如果相差不是很大的话是可以准确识别的。然后该程序也只能识别单行数字,暂时还不能识别多行。但是可以在本程序的基础上做点修改就可以了。等以后有时间了再回来解决这两个问题。

    附上程序源码下载:https://download.csdn.net/download/m0_37543178/10662148

相关标签: opencv 数字识别