用到的技术主要包括,矩形检测,图像旋转变换,图像二值化,图像腐蚀膨胀,图像求连通域分割,SVM,BP神经网络,FCM模糊聚类算法
全部代码已上传 https://download.csdn.net/download/qq_34657707/11604190
首先看程序大概分了这么几个类
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部分的代码目前也已经实现,这个工程写的时候还没加入这部分,想要的人可以留言哈。