综合前面图像处理的相关知识,处理身份证的简单识别.
使用Java实现.
一.身份证识别概述
1.思路
身份证中关键字段为黑色,
直接按颜色过滤像素点二值化,过滤偏黑灰色,及rgb 为 (111,111,111)至(0,0,0)的颜色,设置为黑色.
其他颜色转换为白色,在图片比较标准,无亮点,光照均有的情况下,基本可以完美识别出文字.
然后计算文字的左右投影
根据投影切割行数据,
再对每一行,计算文字的上下垂直投影,计算文字宽度区域。
根据每一行合并区域得出最后的所有字段的区域。
根据区域切割图片。
对每一个切割的图片进行文字识别即可.
2.关键字段自动定位-实现结果
图片来源于网络。
折腾了几天,终究基本实现.算是优化了一下吧。。哈哈.
下方为第三行的垂直投影。
二.身份证识别详细过程
1.图片二值化
直接按颜色过滤像素点二值化,过滤偏黑灰色,及rgb 为 (111,111,111)至(0,0,0)的颜色,设置为黑色.
其他颜色转换为白色
直接根据图像黑色过滤.
//空图片
int imgheight = rectM.height();
int imgwidth = rectM.width();
Mat hist2 = new Mat(imgheight, imgwidth, CvType.CV_8UC1);
double[] v2 = new double[imgwidth * imgheight];
for (int j = 0; j < imgwidth * imgheight; j++) {
v2[j] = 255;
}
hist2.put(0, 0, v2);
//过滤黑色
double max = imgheight;// 0;
for (int j = 0; j < imgwidth; j++) {
for (int i = 0; i < imgheight; i++) {
try {
// if (rectM.get(i, j)[0] <= 100+param && rectM.get(i, j)[1] <= 100+param &&
// rectM.get(i, j)[2] <= 100+param) {
// 对彩色图片过滤黑色阈值
if (rectM.get(i, j)[0] <= param) {
hist2.put(i, j, 0);
} else {
// int k = 0;
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
其中param即黑色的像素值,越黑越保留,
关于这个值的计算,对不同的图片做了一下判断,需要判断图片的整体亮度,是否有亮点,对比是否清晰等,目前来说这个判断大体可用. 一些普通的图片都可以识别
//获取灰度图的中间1/3区域宽度的平均灰度值
double avlihghtThird = getAvThirdWidthGray(bgr);
// 获取输入bgr图片的 平均灰度值
double avlihght = getAvGray(bgr);
// 获取灰度图的1/3区域高度的最大灰度值
double maxlihght = getMaxthresh(bgr);
double param = (255 - maxlihght) * 2;
// 确定过滤的灰色阈值
if (Math.abs(avlihght - avlihghtThird) < 20) {
// 中间区域与全图平均灰度差别不大,无高亮/图片颜色分布均衡
if (val < 35) // 对比度比较低,整体偏亮
{
if (avlihghtThird < 150) // 平均灰度第,颜色清晰
param = 115; // 过滤偏黑灰色.
else if (avlihghtThird >= 150 && avlihghtThird < 180) // 平均灰度第,颜色清晰
param = 135; // 过滤偏黑灰色.
else
param = 149; // //平均灰度第,颜色不太清晰
} else if (val >= 35 && val < 61) // 正常图片,颜色分布均衡
{
param = 110; // 过滤偏黑灰色.
if (maxlihght < 200)
param = 85; // 过滤偏黑灰色.
} else if (val >= 61 && val < 91) // 对比度比较高,色彩区分度高
param = 60; // 过滤偏黑色.
else if (val >= 91) // 对比度超高,图片有亮度不均衡或者部分区域高亮
{
param = 60; // 过滤偏黑色.
}
}
原图:
根据黑色二值化处理结果
2.图像水平垂直投影,定位关键数据位置
其实,第一版我是直接按照身份证的图,使用xy,初略给每个区域设定一个范围,然后直接截取图片,
但是每个图片拍摄的都不一定完整,导致切割的非常不准,
投影计算旨在不管图片怎么拍,都能把关键字段的位置定位出来.
原理:扫描全图的像素,统计y轴上的每一行的总像素值.然后将统计结果转化为直方图显示.根据直方图计算出每一行的位置.
然后对每一个行区域里面的数据做垂直投影,同上,统计所有x坐标上每一列的总像素,生成直方图,计算没一列的范围.
然后定位每一个区域.
水平投影行的像素计算:
//每一行的像素总和统计,只统计一半宽度,剔除右侧身份证头像区域
Mat hist = new Mat(1, imgheight, CvType.CV_32FC1);
Core.normalize(hist, hist, 0, hist.rows(), Core.NORM_MINMAX, -1, new Mat());
double[] v = new double[imgheight];
for (int j = 0; j < imgheight; j++) {
v[j] = 0;
}
hist.put(0, 0, v);
double max = imgheight;// 0;
for (int i = 0; i < imgheight; i++) {
for (int j = 0; j < imgwidth / 2; j++) {
if (input.get(i, j)[0] == 0) {
double num = hist.get(0, i)[0];
num++;
hist.put(0, i, num);
} else {
int k = 0;
}
}
double num = hist.get(0, i)[0];
if (num > max)
max = num;
}
生成直方图
//数值统计转换为直方图,根据像素值/画布总宽度画线
Mat hist2 = new Mat(imgheight, imgwidth, CvType.CV_8UC1);
Core.normalize(hist2, hist2, 0, hist2.rows(), Core.NORM_MINMAX, -1, new Mat());
double[] v2 = new double[imgwidth * imgheight];
for (int j = 0; j < imgwidth * imgheight; j++) {
v2[j] = 255;
}
hist2.put(0, 0, v2);
for (int j = 0; j < imgheight; j++) {
double y = (hist.get(0, j)[0] / max) * imgwidth;
// B component or gray image
Imgproc.line(hist2, new Point(0, j), new Point(y, j), new Scalar(0, 0, 0), 2, 8, 0);
}
Imgproc.cvtColor(hist2, hist2, Imgproc.COLOR_GRAY2BGR);
行区域启动判断
for (int j = 0; j < imgheight; j++) {
double num = hist.get(0, j)[0];
if (!isstart) {
// 顶部和底部的排除
if (num > 0 && j > 20 && j < imgheight - 20) {
if (start_val1 == 0)
start_val1 = num;
else {
start_val2 = num;
// 找到开始
if (start_val2 / start_val1 >= 1.4 && (start_val2 > 10)) {
isstart = true;
} else {
// 重新开始计算
start_val1 = num;
}
}
}
行结束判断
if (num >= 0) {
if (start_val1 == 0)
start_val1 = num;
else {
start_val2 = num;
// 找到结束,每一行至少>3%高度
if (start_val1 / start_val2 > 1.4 && (j - startj > imgheight * 0.035) && start_val2 < 15) {
isend = true;
start_val1 = 0;
} else {
// 重新开始计算
start_val1 = num;
}
}
}
水平投影:
第三行,垂直投影
后面根据行列特定,合并区域即可.
最后切割结果:
3.调整-非标准四边形证件识别
上面是针对标准四边形证件,方方正正的拍摄情况的,要是图片不是完整的,需要识别证件外边框,可能还需要进行透视变换为标准形状.
比如原图如下:有一定的透视,拍色视角非正正方方,并且还有外部背景.
这样处理.计算边框
this.image.copyTo(m);
// 滤波,模糊处理,消除某些背景干扰信息
Imgproc.blur(m, filtered, new Size(3, 3));
// savetoImg(filtered, "blur_33");
// 腐蚀操作,消除某些背景干扰信息
Imgproc.erode(filtered, filtered, new Mat(), new Point(-1, -1), 3);// 1, 1);
//这里这个边缘检测的阈值成了关键数据,目前还没有太好方法进行判定.
//上述使用的奥巴马的图片,使用值为17-17*3.
double threshmin = thresh;
double threshmax = threshmin * 3;
// 边缘检测
Imgproc.Canny(filtered, edges, threshmin, threshmax);
// 膨胀操作,尽量使边缘闭合
Imgproc.dilate(edges, dilated_edges, new Mat(), new Point(-1, -1), 3);// , 1, 1);
4.轮廓计算
List<MatOfPoint> contours = new ArrayList<>();// 每一组Point点集就是一个轮廓。
Mat hierarchy = new Mat();
// RETR_EXTERNAL:表示只检测最外层轮廓,对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1
// CHAIN_APPROX_SIMPLE:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息
// 寻找边界轮廓
Imgproc.findContours(dilated_edges, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
在轮廓的结果中寻找最外层的矩形,有4个点,并且为凸边形.,有一定的面积.
// For conversion later on
MatOfPoint2f approxCurve = new MatOfPoint2f();
Rect rect = new Rect();
// For each contour found
for (int i = 0; i < contours.size(); i++) {
// Convert contours from MatOfPoint to MatOfPoint2f
MatOfPoint2f contour2f = new MatOfPoint2f(contours.get(i).toArray());
// Processing on mMOP2f1 which is in type MatOfPoint2f
// 计算轮廓的长度
double approxDistance = Imgproc.arcLength(contour2f, true) * 0.02;
if (approxDistance > 1) {
// Find Polygons
// 连续轮廓折线化
Imgproc.approxPolyDP(contour2f, approxCurve, approxDistance, true);
// Convert back to MatOfPoint
MatOfPoint points = new MatOfPoint(approxCurve.toArray());
// Rectangle Checks - Points, area, convexity
// 矩形检测,4个顶点,凸边形,有一定的面积.
if (points.total() == 4 && Math.abs(Imgproc.contourArea(points)) > 1000
&& Imgproc.isContourConvex(points)) {
//外测的四边形
rect = Imgproc.boundingRect(points);
// 绘制边界轮廓
Imgproc.drawContours(outContouMat, contours, i, new Scalar(255, 255, 0), 3);
如下,外侧4个顶点
透视变换
计算矩形变换数据,矩形-》边框的变化.
MatOfPoint2f rectMat_dest = new MatOfPoint2f(
new Point[] { new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y),
new Point(rect.x + rect.width, rect.y + rect.height),
new Point(rect.x, rect.y + rect.height), });
// 确定 approxCurve 点的顺序
List<Point> pts = new ArrayList<>();
Point pt1 = new Point(approxCurve.get(0, 0));
Point pt2 = new Point(approxCurve.get(1, 0));
Point pt3 = new Point(approxCurve.get(2, 0));
Point pt4 = new Point(approxCurve.get(3, 0));
pts.add(pt1);
pts.add(pt2);
pts.add(pt3);
pts.add(pt4);
Point[] npts = sortRectPoints(pts);
MatOfPoint2f rectMat_src = new MatOfPoint2f(new Point[] { npts[0], npts[1], npts[2], npts[3] });
// 矩形变换 获取 规则->不规则
Mat tp = Imgproc.getPerspectiveTransform(rectMat_dest, rectMat_src);
tp.copyTo(transMat);
,然后对原图进行整体变换
// 轮廓ok,变换原图
Imgproc.warpPerspective(rectM2, realRect, transMat, rectM2.size(), Imgproc.INTER_LINEAR); // +
// Imgproc.WARP_INVERSE_MAP
这就是前文开始处理的图了>_<.
到此,所有的关键内容都切割出来了。下面就是识别的问题了.
本文基于CC BY-NC-ND 4.0 许可协议发布,作者:野生的喵喵。 固定链接: 【opencv-身份证识别-java处理】 转载请注明
相关文章: