C++代码+opencv2.4.13+vs2013实现身份证识别,拍身份证照然后读出数字

发布时间:2024-03-15 10:01

用到的技术主要包括,矩形检测,图像旋转变换,图像二值化,图像腐蚀膨胀,图像求连通域分割,SVM,BP神经网络,FCM模糊聚类算法

全部代码已上传 https://download.csdn.net/download/qq_34657707/11604190

首先看程序大概分了这么几个类

C++代码+opencv2.4.13+vs2013实现身份证识别,拍身份证照然后读出数字_第1张图片

angle类用于求直线夹角,矩形检测时候会用到,drawSquare类用于画出检测到的矩形,findSquares4是矩形检测类,Numimgbirth是生成分割的身份证号码数字,识别的时候预处理阶段会用到,predict类用于bp神经网络的识别,rotate类是图像旋转类,用于将检测到的矩形水平旋转。sortvector是vector排序类,用于将分割的身份证号码按从左到右编号。train类是训练类,用于将分割的字符训练。

下面从main函数的识别讲起,首先是图像检测和预处理阶段。这一阶段目的是将身份证从环境中检测出来。并且将身份证号码检测和分割出来,将身份证号码切割成一个个字符。

        IplImage* img0 = 0;
		CvMemStorage* storage = 0;
		int c;
		const char* wndname = "Square Detection Demo"; //窗口名称
		storage = cvCreateMemStorage(0);
		cvNamedWindow(wndname, 1);
		char *imageSrc = "C:\\Users\\g\\Desktop\\15.jpg";
		//img0 = cvLoadImage(imageSrc, 1);
		//resize(img0, img0, Size(60, 60), 0, 0, INTER_AREA);
		double Angle;
		cv::Mat dst, src;
		img0 = cvLoadImage(imageSrc, 1);
		CvSize Out_Img_size;
		int maxarea = 240000;
		int minarea = 2000;
		Out_Img_size.width = img0->width;
		Out_Img_size.height = img0->height;
		while (Out_Img_size.width*Out_Img_size.height <= maxarea)
		{
			Out_Img_size.width = Out_Img_size.width*1.05;
			Out_Img_size.height = Out_Img_size.height*1.05;
		}

上面这部分代码目的是读取身份证照片,maxarea表示检测到矩形最大面积,minarea表示最小面积,在这里为了防止将图片本身的矩形检测出来,做了一个判断,如果图片总面积小于检测矩形最大面积,就将图片放大,每次放大比例是1.05倍。

        IplImage*  Output_Img = cvCreateImage(Out_Img_size, img0->depth, img0->nChannels);
		cvResize(img0, Output_Img, CV_INTER_LINEAR);
		while (true)
		{
			/*resize(img0, img0, Size(480, 640));*/
			findSquares4 findsquares4;
			drawSquare drawsquare;
			findsquares4.findSquares(Output_Img, storage, minarea, maxarea, 80, 100);
			drawsquare.drawSquares(Output_Img, findsquares4.squares, wndname);
			cvClearMemStorage(storage);  //清空存储
			c = cvWaitKey(10);
			if (c == 27)
				break;
			drawsquare.vec_pt;
			drawsquare.pt0;
			//if (drawsquare.pt0.size())
			double len1 = sqrt((drawsquare.pt0[0].x - drawsquare.pt0[1].x)*(drawsquare.pt0[0].x - drawsquare.pt0[1].x) + (drawsquare.pt0[0].y - drawsquare.pt0[1].y)*(drawsquare.pt0[0].y - drawsquare.pt0[1].y));
			double len2 = sqrt((drawsquare.pt0[1].x - drawsquare.pt0[2].x)*(drawsquare.pt0[1].x - drawsquare.pt0[2].x) + (drawsquare.pt0[1].y - drawsquare.pt0[2].y)*(drawsquare.pt0[1].y - drawsquare.pt0[2].y));
			double len3 = sqrt((drawsquare.pt0[2].x - drawsquare.pt0[3].x)*(drawsquare.pt0[2].x - drawsquare.pt0[3].x) + (drawsquare.pt0[2].y - drawsquare.pt0[3].y)*(drawsquare.pt0[2].y - drawsquare.pt0[3].y));
			double len4 = sqrt((drawsquare.pt0[3].x - drawsquare.pt0[0].x)*(drawsquare.pt0[3].x - drawsquare.pt0[0].x) + (drawsquare.pt0[3].y - drawsquare.pt0[0].y)*(drawsquare.pt0[3].y - drawsquare.pt0[0].y));
			double ratio = len1 / len2;
			double ratio2 = len1 / len3;
			if (ratio> 1.2)
			{
				if (drawsquare.pt0[0].x > drawsquare.pt0[1].x)
				{
					double dx = drawsquare.pt0[0].x - drawsquare.pt0[1].x;
					double dy = drawsquare.pt0[0].y - drawsquare.pt0[1].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
				else
				{
					double dx = drawsquare.pt0[1].x - drawsquare.pt0[0].x;
					double dy = drawsquare.pt0[1].y - drawsquare.pt0[0].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
			}
			else if (ratio<0.8)
			{
				if (drawsquare.pt0[1].x > drawsquare.pt0[2].x)
				{
					double dx = drawsquare.pt0[1].x - drawsquare.pt0[2].x;
					double dy = drawsquare.pt0[1].y - drawsquare.pt0[2].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
				else
				{
					double dx = drawsquare.pt0[2].x - drawsquare.pt0[1].x;
					double dy = drawsquare.pt0[2].y - drawsquare.pt0[1].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
			}
			else if (ratio2 > 1.2)//进入此条比较时,说明len1和len2都是长边或都是短边,进入本比较时,说明len1是长边
			{
				if (drawsquare.pt0[0].x > drawsquare.pt0[1].x)
				{
					double dx = drawsquare.pt0[0].x - drawsquare.pt0[1].x;
					double dy = drawsquare.pt0[0].y - drawsquare.pt0[1].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
				else
				{
					double dx = drawsquare.pt0[1].x - drawsquare.pt0[0].x;
					double dy = drawsquare.pt0[1].y - drawsquare.pt0[0].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
			}
			else if (ratio2 < 0.8)//说明len1和len2都是短边,len3是长边
			{
				if (drawsquare.pt0[2].x > drawsquare.pt0[3].x)
				{
					double dx = drawsquare.pt0[2].x - drawsquare.pt0[3].x;
					double dy = drawsquare.pt0[2].y - drawsquare.pt0[3].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
				else
				{
					double dx = drawsquare.pt0[3].x - drawsquare.pt0[2].x;
					double dy = drawsquare.pt0[3].y - drawsquare.pt0[2].y;
					double sin = dy / sqrt(dy*dy + dx*dx);
					Angle = asin(sin) * 180 / 3.141592653;
				}
			}
		}

上面这段代码主要是检测出图片中的矩形,检测面积小于maxarea,大于minarea,且夹角范围是80-100度的矩形,在这之前要对图像进行resize操作,这样做的目的是尽可能让设定的面积阈值符合实际。在这里len1,len2,len3,len4是求矩形四个边长,目的是为了判断矩形旋转的标准是长边被旋转为水平方向。pt0[4]是drawsquare的成员变量。主要存储检测到的目标矩形四个角的坐标平均值。Angle是求出应该旋转的角度,后面做矩形旋转会用到。

src = imread(imageSrc, 1);
		resize(src, src, cv::Size(Out_Img_size.width, Out_Img_size.height), INTER_AREA);
		rotateimg rotateimg;
		rotateimg.rotate_arbitrarily_angle(src, dst, Angle);//90表示旋转角度,正为逆时针旋转,负数为顺时针旋转
		char rotimgname[100];
		sprintf_s(rotimgname, "1.jpg");
		cv::imwrite(rotimgname, dst);
		//
		findSquares4 findsquares2;
		drawSquare drawsquare2;
		while (true)
		{
			img0 = cvLoadImage(rotimgname, 1);
			findsquares2.findSquares(img0, storage, minarea, maxarea, 80, 100);
			drawsquare2.drawSquares(img0, findsquares2.squares, wndname);
			cvClearMemStorage(storage);  //清空存储
			c = cvWaitKey(10);
			if (c == 27) /*判断若按Esc键则退出循环*/
				break;
		}
		int right, left, high, low;
		right = drawsquare2.pt0[0].x;
		left = drawsquare2.pt0[0].x;
		high = drawsquare2.pt0[0].y;
		low = drawsquare2.pt0[0].y;
		for (int i = 0; i < 4; i++)
		{
			if (drawsquare2.pt0[i].x>right)
			{
				right = drawsquare2.pt0[i].x;
			}
			if (drawsquare2.pt0[i].xlow)
			{
				low = drawsquare2.pt0[i].y;
			}
		}//得到边界
		Mat imggrey = imread("1.jpg", 1);
		cvtColor(imggrey, imggrey, CV_BGR2GRAY);
		//GaussianBlur(imggrey, imggrey, Size(3,3), 1, 1);//高斯滤波
		Mat imgStand = cv::Mat::zeros(cv::Size(right - left + 1, low - high + 1), CV_8U);
		Mat imgthreshold;
		int x = 0, y = 0;
		for (int i = high; i <= low; i++)
		{
			y = 0;
			for (int j = left; j <= right; j++)
			{
				imgStand.at(x, y) = imggrey.at(i, j);
				y++;
			}
			x++;
		}
		cv::resize(imgStand, imgStand, Size(634, 400), 0, 0, INTER_AREA);
		cv::imwrite("身份证灰度图.png", imgStand);
		FuzzyCmeans fuzzycmeans1;
		fuzzycmeans1.imgFuzzyCmeans(imgStand);
		int a = (fuzzycmeans1.claster1 + fuzzycmeans1.claster2) / 2;
		threshold(imgStand, imgthreshold, a, 255, CV_THRESH_BINARY);//二值化 
		//threshold(src, dest, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);//CV_THRESH_OTSU参数有自适应的作用,二值化不用考虑阈值的选取,计算速度更快,CV_THRESH_BINARY_INV参数是反二值化,越接近白色的画为黑色。越接近黑色的画为白色,用此参数后续不用再反置操作
		cv::imwrite("二值化图.png", imgthreshold);

上面这段代码主要目的是将原图读取并旋转到水平,然后通过检测到的身份证四个角边界,将身份证截取并且做二值化操作,并且存储为二值化图.png。在这里二值化阈值的选取我用了FCM算法聚类聚出了2个类别,取他们的平均值做分解线,后来了解到opencv自带的二值化函数有自适应参数,可以不用设置分解值。读者可直接用opencv的threshold(src, dest, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY)函数搞定。

cv::imwrite("二值化图.png", imgthreshold);
		//imshow("二值化图", imgthreshold);
		//waitKey(0);
		Mat element = getStructuringElement(MORPH_RECT, Size(18, 18)); //进行腐蚀操作,获取自定义核
		Mat dstImage;
		erode(imgthreshold, dstImage, element);//从src输入,由dst输出
		//imshow("[效果图]腐蚀操作", dstImage);//显示效果图
		cv::imwrite("腐蚀图.png", dstImage);
		//waitKey(0); //等待任意按键按下
		findSquares4 findsquares3;
		drawSquare drawsquare3;
		img0 = cvLoadImage("腐蚀图.png", 1);
		IplImage* img1 = 0;
		img1 = cvLoadImage("身份证灰度图.png", 1);
		while (true)
		{
			findsquares3.findSquares(img0, storage, 8000, 18000, 60, 120);
			drawsquare3.drawSquares(img1, findsquares3.squares, wndname);
			cvClearMemStorage(storage);  //清空存储
			c = cvWaitKey(10);
			if (c == 27) /*判断若按Esc键则退出循环*/
				break;
		}
		cvReleaseImage(&img1);
		cvClearMemStorage(storage);
		cvDestroyWindow(wndname);
		right = drawsquare3.pt0[0].x;
		left = drawsquare3.pt0[0].x;
		high = drawsquare3.pt0[0].y;
		low = drawsquare3.pt0[0].y;
		for (int i = 0; i < 4; i++)
		{
			if (drawsquare3.pt0[i].x>right)
			{
				right = drawsquare3.pt0[i].x;
			}
			if (drawsquare3.pt0[i].xlow)
			{
				low = drawsquare3.pt0[i].y;
			}
		}//得到边界
		Mat IDimgStand = cv::Mat::zeros(cv::Size(right - left + 1, low - high + 1), CV_8U);
		x = 0, y = 0;
		for (int i = high; i <= low; i++)
		{
			y = 0;
			for (int j = left; j <= right; j++)
			{
				IDimgStand.at(x, y) = imgStand.at(i, j);
				y++;
			}
			x++;
		}
		cv::imwrite("身份证号码灰度图.png", IDimgStand);
		//imshow("身份证号码灰度图", IDimgStand);

上面这段代码目的是把已经得到的身份证灰度图做腐蚀操作,目的是将身份证号码连成一片,然后在做矩形检测,检测出身份证号码,在利用其边界将身份证号码截取出来。

cvtColor(oppimg, oppimg, CV_BGR2GRAY);
		findContours(IDimgpst, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE, Point());
		vector > vec_charimgposition;
		vector temp;
		for (int i = 0; i  contours[i][j].x)
				{
					left = contours[i][j].x;
				}
				if (high>contours[i][j].y)
				{
					high = contours[i][j].y;
				}
				if (low

上面这段代码主要是做连通域检测,将身份证号码的二值化图做一个连通域检测。目的是为了分割每一个字符。身份证号码默认是18位,因此连通域个数也是18.如果小于18,说明图像有的字符连在了一起。还要做进一步操作。

我做的操作是进行膨胀操作。膨胀后明显改善了这一现象。

最后当然是进行识别了,

	for (int i = 0; i < sortvec1.vec_sort.size(); i++)
		{
			char filename[100];
			sprintf_s(filename, "%d.png", i);
			Mat imgpre=imread(filename, 0);
		/*	cvtColor(imgpre, imgpre, CV_BGR2GRAY);*/
			predict predict1;
			int Class=predict1.predictimage(imgpre);
			if (Class == 10)
				cout << "X" << " ";
			else
			cout << Class<<" ";
		}
		system("pause");

上面这段代码表示将分割后的每个字符放入神经网络中识别。

目前代码中问题是如果拍照环境出现大小和身份证类似的矩形,会出bug。因此还需要加入SVM用于判断检测到的矩形是不是为身份证,如果不是在忽略它。SVM部分的代码目前也已经实现,这个工程写的时候还没加入这部分,想要的人可以留言哈。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号