当前位置: 代码迷 >> 综合 >> C++代码+opencv2.4.13+vs2013实现身份证识别,拍身份证照然后读出数字
  详细解决方案

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

热度:37   发布时间:2023-10-12 04:56:34.0

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

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

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

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

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].x<left){left = drawsquare2.pt0[i].x;}if (drawsquare2.pt0[i].y<high){high = drawsquare2.pt0[i].y;}if (drawsquare2.pt0[i].y>low){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<uchar>(x, y) = imggrey.at<uchar>(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].x<left){left = drawsquare3.pt0[i].x;}if (drawsquare3.pt0[i].y<high){high = drawsquare3.pt0[i].y;}if (drawsquare3.pt0[i].y>low){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<uchar>(x, y) = imgStand.at<uchar>(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<vector<int> > vec_charimgposition;vector<int> temp;for (int i = 0; i <contours.size(); i++){right = contours[i][0].x;left = contours[i][0].x;high = contours[i][0].y;low = contours[i][0].y;for (int j = 0; j < contours[i].size(); j++){if (right < contours[i][j].x){right = contours[i][j].x;}if (left > contours[i][j].x){left = contours[i][j].x;}if (high>contours[i][j].y){high = contours[i][j].y;}if (low<contours[i][j].y){low = contours[i][j].y;}}temp.push_back(left);temp.push_back(high);temp.push_back(right);temp.push_back(low);vec_charimgposition.push_back(temp);temp.clear();/**//**/}/**/sortvector sortvec1;sortvec1.sortvect(vec_charimgposition);sortvec1.vec_sort;if (sortvec1.vec_sort.size() < 18){cout << "识别失败了,您拍的照片不太好,请重新拍一张" << endl;system("pause");exit(0);}

上面这段代码主要是做连通域检测,将身份证号码的二值化图做一个连通域检测。目的是为了分割每一个字符。身份证号码默认是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" << " ";elsecout << Class<<" ";}system("pause");

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

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