iOS开发基于OpenCV处理实现身份证号码识别

/ Java / 没有评论 / 1840浏览

iOS开发基于OpenCV处理实现身份证号码识别

实现效果展示

1


技术要点分析:此次项目中主要的技术划分为身份证号码区域提取和光学字符识别。

  1. 身份证号码区域的提取涉及有:
  • 图像灰度化
  • 阀值二值化
  • 腐蚀
  • 轮廓检测,获取身份证号码区域 这里的处理则需要使用到OpenCV开源库。
  1. 光学字符识别主要集成TesseractOCRiOS框架。

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android,iOS和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法,开发中一般用来做图片视频的处理,图形识别,机器学习等应用。 OpenCV目前分为两个版本:OpenCV2.4.x和OpenCV3.x,并且这两个版本都在持续更新中(最新为3.3.1和2.4.13.4)。根据网上的资料,OpenCV3.x是一个阉割后的版本,OpenCV3.x把重要的nonfree模块去掉了。本例中集成的是3.x版本,至于强大的2.4.x版本也有对应的framework可供下载,如果有成功编译2.4.x版本的朋友也可分享一下相关教程!

一. OpenCV的环境搭建

最开始是准备直接使用CocoaPods集成,pod search 是 'OpenCV', '~> 3.2.0'版本,但是好像包挺大的,install几次都失败了,无奈才去官网下载framework自己手动集成编译了

这是因为OpenCV是基于C++编写,如果要使用C++类或者特性就需要把该文件扩展名改为.mm。

.m & .mm:
.m :源代码文件。这是典型的源代码文件扩展名,可以包含Objective-C和C代码。 
.mm :源代码文件。带有这种扩展名的源代码文件,除了可以包含Objective-C和C代码以外还可以包含C++代码。仅在你的Objective-C代码中确实需要使用C++类或者特性的时候才用这种扩展名

再build 就OK了。

二. 开始图像的第一步处理了 -- 图像灰度化:

将彩色图像转化成为灰度图像的过程称为图像的灰度化处理。 灰度化,在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。 彩色图像中的每个像素的颜色有R、G、B三个分量决定,而每个分量有255中值可取,这样一个像素点可以有1600多万(255255255)的颜色的变化范围。而灰度图像是R、G、B三个分量相同的一种特殊的彩色图像,其一个像素点的变化范围为255种,所以在数字图像处理中一般先将各种格式的图像转变成灰度图像以使得后续图像的计算量变得少一些。灰度图像的描述与彩色图像一样仍然反映了整幅图像的整体和局部的色度和亮度等级的分布和特征。一般有四种方法对彩色图像进行灰度化处理:分量法、最大值法、平均值法、加权平均法。

这里直接调用函数:

void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
C++:void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
参数解释:
InputArray src –-输入图像即要进行颜色空间变换的原图像,可以是Mat类
OutputArray dst –-输出图像即进行颜色空间变换后存储图像,也可以Mat类
int code –-转换的代码或标识
int dstCn = 0 –-目标图像通道数,如果取值为0,则由src和code决定

灰度化处理处理后的效果为: 5 灰度化之后请勿再次灰度化,会直接crash

三. 阀值二值化

把一幅多灰度值的图像(Gray Level Image)转化为只有黑(前景文字部分)白(背景部分)分布的二值图像(Binary Image)的工作叫做二值化处理(Binariztion)。对于一般256级灰度的灰度图,0级灰度对应于黑色,255级对应于白色。二值化后0对应于黑色前景文字,1对应于白色背景。二值化方法可以分为: 全局二值化 和 局部自适应二值化。

  1. 全局固定阀值二值化:可直接使用固定函数完成
C++:double cvThreshold(constCvArr* src, CvArr* dst, double threshold, double max_value,int threshold_type)

参数说明:

define CV_THRESH_BINARY-- 0 --value = value > threshold ?max_value : 0 
define CV_THRESH_BINARY_INV-- 1 -- value = value > threshold ? 0 : max_value    
define CV_THRESH_TRUNC-- 2 --value = value > threshold ?threshold : value  
define CV_THRESH_TOZERO-- 3 --value = value > threshold ? value : 0        
define CV_THRESH_TOZERO_INV-- 4 --value = value > threshold ? 0 : value 
define CV_THRESH_MASK        7
define CV_THRESH_OTSU-- 8 --use Otsu algorithm to choose the optimal threshold value
 combine the flag with one of the above CV_THRESH_values

值得一说的是threshold_type可以使用CV_THRESH_OTSU类型,这样该函数就会使用大律法OTSU得到的全局自适应阈值来进行二值化图片,而参数中的threshold不再起作用 在这里需要注意src必须是单通道图像,如果直接操作彩色图会crash

关于图像通道:
一个图形的通道数是N,就表明每个像素点处有N个数,一个axb的N通道图像,图像矩阵实际上是b行Nxa列的数字矩阵。
OpenCV中图像的通道可以是1,2,3,4。
1. 通道的是灰度图RGB555和RGB565。
2. 通道的图像是RGB555和RGB565。2通道图在程序处理中会用到,如傅里叶变换,可能会用到,一个通道为实数,一个通道为虚数,主要是编程方便。RGB555是16位的,2个字节,5+6+5,第一字节的前5位是R,后三位+第二字节是G,第二字节后5位是B,可见对原图像进行压缩了。
3. 通道的是彩色图像,比如RGB图像。
4. 通道的图像是RGBA,是RGB加上一个A通道,也叫alpha通道,表示透明度。PNG图像是一种典型的4通道图像。alpha通道可以赋值0到1,表示透明到不透明。
  1. 局部自适应阀值二值化:
void cvAdaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C )

参数说明:

函数cvAdaptiveThreshold将灰度图像转换为二值图像,采用以下共识:

threshold_type=CV_THRESH_BINARY:
dst(x,y) = max_value, if src(x,y)>T(x,y)
0, otherwise
threshold_type=CV_THRESH_BINARY_INV:
dst(x,y) = 0, if src(x,y)>T(x,y)
max_value, otherwise

其中 TI 是为每一个象素点单独计算的阈值

下面是两种阀值二值化的效果对比:

四. 腐蚀(Erode)

腐蚀:图像处理操作中的最基本形态学操作有二:腐蚀与膨胀(Erosion 与 Dilation),它们的应用非常广泛:

腐蚀在形态学操作家族里是膨胀操作的孪生姐妹。它提取的是内核覆盖下的相素最小值。进行腐蚀操作时,将内核划过图像,将内核覆盖区域的最小相素值提取,并代替锚点位置的相素。进行腐蚀操作的函数是erode。 它接受了三个参数:

获取自定义核,以及进行腐蚀操作
cv::Mat erodeElement = getStructuringElement(cv::MORPH_RECT, cv::Size(5,5));
cv::erode(cvImage, cvImage, erodeElement);

腐蚀处理效果图效果图: 9 OK...要的就是这种效果,接着进行轮廓检测。这里有必要说明一点:腐蚀和膨胀是对白色部分(高亮部分)而言的,不是黑色部分。

五. 轮廓检测

使用函数:

findContours(InputOutputArray image,OutputArrayOfArrays contours,OutputArray hierarchy, int mode, int method,Point offset-Point())

参数说明:

  1. InputOutputArray image -- 单通道图像矩阵,可以是经过Canny,拉普拉斯等边缘检测算子处理过的二值图像,我这里使用的是经过腐蚀处理的二值图
  2. OutputArrayOfArrays contours -- 定义为vector<vector>类型,是一个向量,并且是一个双重向量,向量内每个元素保存了一组由连续的Point点构成的点的集合的向量,每一组Point 点集就是一个轮廓。
  3. OutputArray hierarchy -- 定义为vector类型,Vec4i是Vec<int,4>的别名,定义了一个'向量内每一个元素包含了4个int型变量’的向量。所以从定义看,hierarchy也是一个向量,向量内每个元素保存了一个包含4个int整型的数组。向量hierarchy内的元素和轮廓向量contours内的元素是一一对应的,向量的内容相同。hierarchy向量内每一个元素的4个int型变量hierarchy[i][0] ~ hierarchy[i][3],分别表示i个轮廓的后一个轮廓,前一个轮廓,父轮廓,内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓或内嵌轮廓的话,则hierarchy[i][0] ~hierarchy[i][3]的相应位被设置为默认值-1。
  4. int modeint -- 可以定义轮廓的检索模式:
  1. int method -- 定义轮廓的近似方法:
    • CV_CHAIN_APPROX_NONE:保存物体边界上所有连续的轮廓点到contours向量内
    • CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
    • CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
  2. Point offset -- 偏移量,所有轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测点的轮廓点上加上该偏移量,并且Point可以是负值。

使用之前先定义容器参数,用以储存检测到的所有轮廓数据。

/*---------轮廓检测---------*/
- (IBAction)nextAction:(UIButton *)sender {
    std::vector<std::vector<cv::Point>> contours; //定义一个容器来存储所有检测到的轮廊
    std::vector<cv::Vec4i> hierarchy;
    // 轮廓检测函数
    cv::findContours(cvImage, contours,hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE, cvPoint(0, 0));

    long max_Index = contours.size();    
    if (index < max_Index-1) {
        ++index;
    }
    [self takePictureForRectWith:contours];
}

容器内数据较多,这里只打印小部分: 10

六. 提取身份证号码区域

在获取到图片中所有内容的轮廓集合之后,可以利用数据定位到身份证号码所在的区域,然后在原图上进行截取。截取成功之后转换为灰度图,再二值化处理,最后进行识别。 11 容器内数据定位到的区域(省略了部分),依据上述处理过程,我们一步步去实现。

  1. 首先遍历容器contours,然后使用boundingRect函数获取矩形边界框。
/*-------取出身份证号码区域-------*/
- (void)takePictureForRectWith:(std::vector<std::vector<cv::Point>>)contours{
    
    // 取出对应的区域
    std::vector<cv::Rect> rects;
    cv::Rect numberRect = cv::Rect(0,0,0,0);
    std::vector<std::vector<cv::Point>>::const_iterator itContours = contours.begin();
    
    long max_Index = contours.size();
    
    if (max_Index > 0) {
        cv::Rect rect = cv::boundingRect(itContours[index]);
        numberRect = rect;
        printf("X:%d - Y:%d - width:%d - height:%d \n",rect.x,rect.y,rect.width,rect.height);
    }
    
    if (numberRect.width != 0 && numberRect.height != 0) {
        
        [self showUIImageWithRect:numberRect];
    }
}
  1. 这里定位身份证号码区域的算法原理:如果新的区域范围宽度大于已赋值区域宽度,并且宽度为高度的三倍则赋予新值。如下图身份证号码区域: 12 定位到目标区域之后,可以从原图截取了,之后进一步做灰度化,二值化处理,然后显示,并进行识别。
/*-------截取目标图--------*/ 
- (void)showUIImageWithRect:(cv::Rect)numberRect {

    // 目标图像
    cv::Mat resultImage;

    // 原图 -> Mat
    cv::Mat matImage;
    UIImageToMat(image, matImage);

    // 取到对应Rect的目标图像
    resultImage = matImage(numberRect);

    // 将目标图像灰度处理
    cv::cvtColor(resultImage, resultImage, cv::COLOR_BGR2GRAY);
    // 二值化
    cv::adaptiveThreshold(resultImage, resultImage, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 31, 40);

    [self cv_MatToUIImageWithMat:resultImage];
}

/*-------显示目标图--------*/ 
- (void)cv_MatToUIImageWithMat:(cv::Mat)Mat {
    // Mat -> UIImage
    UIImage *newImage = MatToUIImage(Mat);

    // 供识别的Image赋值
    dis_Img = newImage;

    // 显示UIImage图
    CGFloat width = newImage.size.width;
    CGFloat height;
    CGFloat scale = newImage.size.height/newImage.size.width;
    width = width > [UIScreen mainScreen].bounds.size.width ? [UIScreen mainScreen].bounds.size.width: width;
    height = width * scale;
    CGRect imgRect = CGRectMake(([UIScreen mainScreen].bounds.size.width - width) * 0.5, 200, width, height);
    self.imageView.frame = imgRect;
    self.imageView.image = newImage;
}

显示截取目标图 13

到这里,基于OpenCV的前期处理已经完成,接下来是另外一个框架的使用了:光学字符识别(Tesseract),一款由HP实验室开发由Google维护的开源OCR(Optical Character Recognition , 光学字符识别)引擎。Tesseract十分强大,但有以下几点局限性:

七. TesseractOCRiOS

首先还是环境搭建了,直接pod就可以,import头文件 <TesseractOCR/TesseractOCR.h>,build: 14

这是缺少依赖库了,一个个导入吧: 15

依赖库,再build一下,没问题。可以开始识别了,贴上代码及注释:

  1. 初始化tesseract为一个新的G8Tesseract对象
  2. Tesseract将从.traineddata文件中寻找你在该参数中指定的语言,指定为eng和fra将从"eng.traineddata" 和 "fra.traineddata"包含的数据中分别检测英文和法文,法语转换数据(trained data)已经被包含到该工程中了,因为本教程中你将使用的示例诗词中包含一部分法语(Très romantique!),法语中的重读符号不在英语字母集中,因此为了能展示出这些重读符号,你需要连接法语的.traineddata文件。将法语数据包含进来也是很好的,因为.traineddata中有一部分涉及到了语言词汇。
G8Tesseract* tesseract = [[G8Tesseract alloc] initWithLanguage:@"eng"];
  1. 你可以指定三种不同的OCR工作模式:.TesseractOnly是最快但最不精确的方法;.CubeOnly要慢一些,但更精确,因为它使用了更多的人工智能;.TesseractCubeCombined同时使用.TesseractOnly和.CubeOnly来提供最精确的结果,不过这也导致了它成为三种工作方式中最慢的一种。
tesseract.engineMode = G8OCREngineModeTesseractCubeCombined;
  1. Tesseract假定处理的文字是均匀的一段文字,但是你的样例诗中分了多段。Tesseract的pageSegmentationMode可以让它知道文字是怎么样被划分的。所以这里设置pageSegmentationMode为.Auto来支持自动页划分(automatic page segmentation),这样Tesseract就有能力识别段落分割了。
tesseract.pageSegmentationMode = G8PageSegmentationModeAuto;
  1. 这里你通过设定maximumRecognitionTime来限制Tesseract识别图片的时间为一有限的时间。不过这样设定以后,只有Tesseract引擎被限制了,如果你正在使用.CubeOnly 或 .TesseractCubeCombined工作模式,那么即使Tesseract已经达到了maximumRecognitionTime指定的时间,立体引擎(Cube engine)依然会继续处理。
tesseract.maximumRecognitionTime = 30.0;
// 开始识别
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
  1. 如果文字和背景相差很大,那么你将得到Tesseract处理的最好结果。Tesseract有一个内置的滤镜,g8_blackAndWhite(),降低图片颜色的饱和度,增加对比度,减少亮度。这里你在Tesseract图像识别过程开始之前,将滤镜处理后的图像赋值给Tesseract对象的image属性。
tesseract.image = [dis_Img g8_blackAndWhite];
NSString *dis_str = tesseract.recognizedText;
// 识别结束
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();

最后的识别效果就是文章开头所呈现的,在本例中,对于二值化处理,目标区域的提取算法都有一定的局限性,实用条件较为苛刻。如果在实际开发中运行,还需要结合其它一些技术,对图片进行预处理,以提高识别的成功率及准确率,这篇身份证号码识别就讲到这。

分享一下运用TesseractOCRiOS对整张图片进行文字识别的效果:

img 最后如果有需求本次源码的点击下载,里面注释比较详细,如果文中有讲解不到位或者有错误的地方欢迎留言讨论,你的Star和Fork就是我的动力,谢谢!

参考