最近脱更一段时间,真是不好意思。不是忙,是没素材!😆! 公司最近有需要做技术性的文章支持,我是责无旁贷滴要出把力了!
增强现实(AR)这几年都比较热门,相对于虚拟现实(VR)来说,我本人认为增强现实的实用性更高,不然苹果也不会在这方面花很多功夫。目前市面上的我们见到的app扫描二维码,扫描条形码,扫描识别文字、图片,扫描卡片、证件,大到人脸识别(face激萌,美拍... , iPhone X面部解锁),游戏找精灵,等等一些列应用都用到了增强现实技术。特别是扫描文字打印和人脸识别最近还挺火。
那今天就给大家分享一下ios基于AVFoundation的身份证识别应用。这次做的是身份证识别,在开发中为了增加识别精准度,我们也采用了人脸识别。
Demo地址:https://github.com/Soldoros/SSScanningCard
222.PNGAVFoundation是苹果在iOS和OS X系统中用于处理基于时间的媒体数据的框架,所以有任何媒体数据处理的东西,都可以采用这套框架来处理。或许你在处理视频、图片的时候有 AVPlayer 、UIImage等框架来使用,但是你要做视频采集的时候还得用上它,比如微信短视频。
111.png从图可以看出,AVFoundation是基于一套核心框架(core系列)封装的媒体库,实际上它还封装了音频库Core Avdio。核心库的代码大部分用C语言完成,比较底层了,大家有兴趣可以自己研究。
以前在学校学计算机的时候,大家应该还记得我们上网的时候需要一个设备,叫‘调制解调器’,英文名‘ modems’ ,直接音译过来就成为‘猫’。这个玩意儿是很多网络高手的最爱,特别是黑客级别的!它很大一个作用就是将网络中传输进来的模拟信号转换成数字信号,模拟信号是正余弦波,计算机无法直接识别这些信号。而数字信号是二进制的。计算机可以直接识别,然后处理成人类可以识别的信息。
AVFoundation其实很大一部分工作跟调制解调器类似,它配合移动设备上的硬件工具,对媒体做数字化处理。比如从摄像头获取现实世界中的媒体,包括图片,音频,流媒体(视频)。摄像头直接捕捉到的媒体是元数据,就是模拟信号。AVFoundation里面的控件会将模拟信号进行数字转换,生成应用能理解的数据。这个过程可以称之为‘采样’。
对媒体内容进行数字化主要有两种方式。第一种:时间采样,这种方法捕捉一个信号周期内的变化;第二种:空间采样,一般用于在图片数字化和其他可视化媒体内容数字化的过程中。这些内容还包括:数字媒体压缩、色彩的二次抽样、编解码器压缩等等。
身份证识别的原理还是采用底层OpenGL来对静态图片进行光学处理,将彩色图片黑白化,黑色部分是内容,白色部分是空白。然后对黑色部分根据frame进行读取,并通过识别器翻译成文字。整个过程就包括:
灰度化处理
333.png
二值化处理
444.png
腐蚀处理
555.png
轮廓检测
666.png
文字识别技术处理
777.png
我们无需去做这么复杂的处理流程,AVFoundation框架已经把它封装起来了,直接使用就是。当前的身份证识别工具应用到了人脸扫描,识别精度就更高。我们开工吧!
我们需要AVFoundation里面的部分控件:
AVCaptureDevice
AVCaptureSession
AVCaptureVideoDataOutput
AVCaptureMetadataOutput
AVCaptureVideoPreviewLayer
AVCaptureDeviceInput。
如果我们把device比如成一辆车,那么session就好比司机。device通过previewLayer展示的界面获取现实世界的信息,并适配input,通过session建立起input和output之间的桥梁。并把数据传递给output。AVCaptureVideoDataOutput+ AVCaptureMetadataOutput就充当接收输出流和元数据的角色,执行输入设备和输出设备之间的数据传递 有很多Device的input和很多类型的Output,分为MovieFile(输出成movie文件)、VideoData(适用各个Frame的处理)、AudioData(声音采集)、StillImage(静态图像拍照)几种output,它们都继承与AVCaptureOutput,都通过session来控制进行传输。即:device适配input,通过session来输入到output中,这样就达到了从设备到文件持久传输的目的(如从相机设备采集图像到UIImage中) 我们再定义一个全局变量NSNumber来确定视频输出的格式,在定义一个全局的队列dispatch_queue_t来处理人脸识别的元数据。
// 摄像头设备
@property (nonatomic,strong) AVCaptureDevice *device;
// AVCaptureSession对象来执行输入设备和输出设备之间的数据传递
@property (nonatomic,strong) AVCaptureSession *session;
// 输出格式
@property (nonatomic,strong) NSNumber *outPutSetting;
// 出流对象
@property (nonatomic,strong) AVCaptureVideoDataOutput *videoDataOutput;
// 元数据(用于人脸识别)
@property (nonatomic,strong) AVCaptureMetadataOutput *metadataOutput;
// 队列
@property (nonatomic,strong) dispatch_queue_t queue;
// 预览图层
@property (nonatomic,strong) AVCaptureVideoPreviewLayer *previewLayer;
// 人脸检测框区域
@property (nonatomic,assign) CGRect faceDetectionFrame;
// 是否打开手电筒
@property (nonatomic,assign) BOOL torchOn;
@property (nonatomic,strong) UIButton *rightBtn;
设备在采样的时候是以视屏流的形式获取数据,为了截流,我们通过本地沙盒路径做个状态初始化,后面截流的时候再取到这个状态,然后读取视频的当前帧,并生成一张高清的图片。
//在viewDidLoad初始化ret 这里的ret值为后期做截屏判断用的
const char *thePath = [[[NSBundle mainBundle] resourcePath] UTF8String];
int ret = EXCARDS_Init(thePath);
if (ret != 0) {
NSLog(@"初始化失败:ret=%d", ret);
}
//后面截取视频的帧
unsigned char pResult[1024];
int ret = EXCARDS_RecoIDCardData(buffer, (int)width, (int)height, (int)rowBytes, (int)8, (char*)pResult, sizeof(pResult));
if (ret <= 0) {
NSLog(@"ret=[%d]", ret);
} else {
NSLog(@"ret=[%d]", ret);
//做帧图片数字化处理的相关代码
}
AVCaptureDevice是摄像头设备,我们做视屏流采集和文字识别,所以这里参数设置:平滑对焦+自动持续对焦+自动持续曝光+自动持续白平衡
//在viewDidLoad里面初始化摄像头设备
_device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([_device lockForConfiguration:nil]) {
// 平滑对焦
if ([_device isSmoothAutoFocusSupported]){
_device.smoothAutoFocusEnabled = YES;
}
// 自动持续对焦
if ([_device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]){
_device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
}
// 自动持续曝光
if ([_device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure ]){
_device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
}
// 自动持续白平衡
if ([_device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]){
_device.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance;
}
[_device unlockForConfiguration];
}
接下来在viewDidLoad设置视频输出格式,初始化队列,设置人脸元数据并加入队列,初始化输出流对象。
//设置输出格式
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v',表示输出的视频格式为NV12;范围: (luma=[16,235] chroma=[16,240])
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange = '420f',表示输出的视频格式为NV12;范围: (luma=[0,255] chroma=[1,255])
// kCVPixelFormatType_32BGRA = 'BGRA', 输出的是BGRA的格式
_outPutSetting = @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
//将元数据加入队列
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_metadataOutput = [[AVCaptureMetadataOutput alloc]init];
[_metadataOutput setMetadataObjectsDelegate:self queue:_queue];
//初始化输出流对象
_videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
_videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
_videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey:_outPutSetting};
接下来我们继续在viewDidLoad里面初始化AVCaptureSession,这个老司机的角色十分重要,它直接控制了摄像头运行和停止,没有它,我们没法完成对摄像头的操作。
NSError *error = nil;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error];
_session = [[AVCaptureSession alloc] init];
//设置分辨率 高清
_session.sessionPreset = AVCaptureSessionPresetHigh;
if (error) NSLog(@"没有摄像头");
else{
if ([_session canAddInput:input]) {
[_session addInput:input];
}
if ([_session canAddOutput:_videoDataOutput]) {
[_session addOutput:_videoDataOutput];
}
// 输出格式要放在addOutPut之后
if ([_session canAddOutput:_metadataOutput]) {
[_session addOutput:_metadataOutput];
_metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeFace];
}
}
摄像头扫描现实世界需要获取一个扫描界面AVCaptureVideoPreviewLayer,这个界面的数据由AVCaptureSession来处理。继续在viewDidLoad里面初始化AVCaptureVideoPreviewLayer。
//初始化预览图层 保持纵横比+填充层边界 这一层直接像我们展示录制的画面
_previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];
_previewLayer.frame = self.view.frame;
_previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer addSublayer:_previewLayer];
如果单独做身份证识别,这些就够了。不过我们为了增加精准度,规定让用户在头像对焦后才获取当前视频流的帧,并截图。这里采用三方写的一个身份证镂空的界面,这个界面也可以让设计师处理,我们这里直接用第三方LHSIDCardScaningView,并通过一个全局变量获取头像的区域。把区域添加到人脸识别的元数据上做参数。
// 添加自定义的扫描界面
LHSIDCardScaningView *IDCardScaningView = [[LHSIDCardScaningView alloc] initWithFrame:self.view.frame];
_faceDetectionFrame = IDCardScaningView.facePathRect;
[self.view addSubview:IDCardScaningView];
//设置人脸识别区域 并在此区域获取元数据
self.metadataOutput.rectOfInterest = [_previewLayer metadataOutputRectOfInterestForRect:_faceDetectionFrame];
至此,所有对象的初始化已经完成,我们老司机AVCaptureSession开车之前在导航栏增加一个按钮来控制闪光灯
_rightBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[_rightBtn setImage:[UIImage imageNamed:@"nav_torch_off"] forState:UIControlStateNormal];
[_rightBtn setImage:[UIImage imageNamed:@"nav_torch_on"] forState:UIControlStateSelected];
_rightBtn.bounds = CGRectMake(0, 0, 40, 35);
[_rightBtn addTarget:self action:@selector(rightBtnClick:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *refreshItem = [[UIBarButtonItem alloc] initWithCustomView:_rightBtn];
self.navigationItem.rightBarButtonItem = refreshItem;
//默认关闭手电筒
-(instancetype)init{
if(self = [super init]){
_torchOn = NO;
}
return self;
}
//打开或关闭闪光灯
-(void)rightBtnClick:(UIButton *)sender{
_torchOn = !_torchOn;
// 判断是否有闪光灯
if ([_device hasTorch]){
// 请求独占访问硬件设备
[_device lockForConfiguration:nil];
if (_torchOn) {
sender.selected = YES;
[_device setTorchMode:AVCaptureTorchModeOn];
} else {
sender.selected = NO;
[_device setTorchMode:AVCaptureTorchModeOff];
}
// 请求解除独占访问硬件设备
[_device unlockForConfiguration];
}else {
NSLog(@"设备不支持闪光功能!");
}
}
老司机要开车了 我们把start和stop的方法封装起来做个判断,因为是在界面展示时开启,界面消失时停止,所以我们在viewWillAppear里面调用start,在viewWillDisappear里面调用stop
//输入设备和输出设备开始数据传递
- (void)runSession {
if (![_session isRunning]) {
dispatch_async(self.queue, ^{
[self.session startRunning];
});
}
}
//输入设备和输出设备结束数据传递
-(void)stopSession {
if ([self.session isRunning]) {
dispatch_async(self.queue, ^{
[self.session stopRunning];
});
}
}
//界面展示时 手电筒恢复关闭 摄像头开始扫描
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
_torchOn = NO;
_rightBtn.selected = NO;
[self runSession];
}
//界面消失时 摄像头停止扫描
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self stopSession];
}
摄像头的数据通过代理回调接收的,我们需要添加两个代理,一个是从元数据中捕捉人脸 AVCaptureMetadataOutputObjectsDelegate,一个从视屏流信息中捕捉一帧图片AVCaptureVideoDataOutputSampleBufferDelegate。后面这个捕捉帧图片的速度很快,首先在人脸头像对齐之后,通过videoDataOutput设置代理来回调这个方法。
#pragma mark - AVCaptureMetadataOutputObjectsDelegate 从元数据中捕捉人脸
//检测人脸是为了获得“人脸区域”,做“人脸区域”与“身份证人像框”的区域对比,
//当前者在后者范围内的时候,才能截取到完整的身份证图像
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
if (metadataObjects.count>0) {
AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
// 只有当人脸区域的确在小框内时,才再去做捕获此时的这一帧图像
if (metadataObject.type == AVMetadataObjectTypeFace) {
AVMetadataObject *transformedMetadataObject = [_previewLayer transformedMetadataObjectForMetadataObject:metadataObject];
CGRect faceRegion = transformedMetadataObject.bounds;
NSLog(@"是否包含头像:%d, facePathRect: %@, faceRegion: %@",CGRectContainsRect(_faceDetectionFrame, faceRegion),NSStringFromCGRect(_faceDetectionFrame),NSStringFromCGRect(faceRegion));
// 为videoDataOutput设置代理
if (CGRectContainsRect(_faceDetectionFrame, faceRegion)) {
if (!_videoDataOutput.sampleBufferDelegate) {
[_videoDataOutput setSampleBufferDelegate:self queue:_queue];
}
}
}
}
}
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
#pragma mark 从输出的数据流捕捉单一的图像帧
// AVCaptureVideoDataOutput获取实时图像,
//这个代理方法的回调频率很快,几乎与手机屏幕的刷新频率一样快
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v',表示输出的视频格式为NV12;范围: (luma=[16,235] chroma=[16,240])
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange = '420f',表示输出的视频格式为NV12;范围: (luma=[0,255] chroma=[1,255])
// kCVPixelFormatType_32BGRA = 'BGRA', 输出的是BGRA的格式
//这个格式在初始化outPutSetting的时候设置了
if ([_outPutSetting isEqualToNumber:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]] || [_outPutSetting isEqualToNumber:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]]) {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if ([captureOutput isEqual:_videoDataOutput]) {
[self getImageBufferRef:imageBuffer];
// 身份证信息识别完毕后,就将videoDataOutput的代理去掉,防止频繁调用
if (_videoDataOutput.sampleBufferDelegate) {
[_videoDataOutput setSampleBufferDelegate:nil queue:_queue];
}
}
} else {
NSLog(@"输出格式不支持");
}
}
在解码回调中,传递过来的帧数据由CVImageBufferRef指向。如果需取出其中像素数据作进一步处理,得访问其中真正存储像素的内存。 getImageBufferRef方法就封装对CVImageBufferRef的处理。解码后的数据并不能直接给CPU访问,需要先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,否则调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。具体的解码和文字识别操作大家自行百度吧,这就偏底层了。读取后的信息我们用模型保存并传给需要界面。
#pragma mark - 身份证信息识别
- (void)getImageBufferRef:(CVImageBufferRef)imageBufferRef {
//读取CVImageBufferRef对象
CVBufferRetain(imageBufferRef);
// 解码后的数据并不能直接给CPU访问,需要先用CVPixelBufferLockBaseAddress()锁定地址才能从主存访问,否则调用CVPixelBufferGetBaseAddressOfPlane等函数则返回NULL或无效值。然而,用CVImageBuffer -> CIImage -> UIImage则无需显式调用锁定基地址函数。
if (CVPixelBufferLockBaseAddress(imageBufferRef, 0) == kCVReturnSuccess) {
size_t width= CVPixelBufferGetWidth(imageBufferRef);
size_t height = CVPixelBufferGetHeight(imageBufferRef);
NSLog(@"%zu",width);
NSLog(@"%zu",height);
// CVPixelBufferIsPlanar可得到像素的存储方式是Planar或Chunky。若是Planar,则通过CVPixelBufferGetPlaneCount获取YUV Plane数量。通常是两个Plane,Y为一个Plane,UV由VTDecompressionSessionCreate创建解码会话时通过destinationImageBufferAttributes指定需要的像素格式(可不同于视频源像素格式)决定是否同属一个Plane,每个Plane可当作表格按行列处理,像素是行顺序填充的。下面以Planar Buffer存储方式作说明。
//
// CVPixelBufferGetPlaneCount得到像素缓冲区平面数量,然后由CVPixelBufferGetBaseAddressOfPlane(索引)得到相应的通道,一般是Y、U、V通道存储地址,UV是否分开由解码会话指定,如前面所述。而CVPixelBufferGetBaseAddress返回的对于Planar Buffer则是指向PlanarComponentInfo结构体的指针,
CVPlanarPixelBufferInfo_YCbCrBiPlanar *planar = CVPixelBufferGetBaseAddress(imageBufferRef);
size_t offset = NSSwapBigIntToHost(planar->componentInfoY.offset);
size_t rowBytes = NSSwapBigIntToHost(planar->componentInfoY.rowBytes);
unsigned char* baseAddress = (unsigned char *)CVPixelBufferGetBaseAddress(imageBufferRef);
unsigned char* pixelAddress = baseAddress + offset;
static unsigned char *buffer = NULL;
if (buffer == NULL) {
buffer = (unsigned char *)malloc(sizeof(unsigned char) * width * height);
}
memcpy(buffer, pixelAddress, sizeof(unsigned char) * width * height);
unsigned char pResult[1024];
int ret = EXCARDS_RecoIDCardData(buffer, (int)width, (int)height, (int)rowBytes, (int)8, (char*)pResult, sizeof(pResult));
if (ret <= 0) {
NSLog(@"ret=[%d]", ret);
} else {
NSLog(@"ret=[%d]", ret);
// 播放一下“拍照”的声音,模拟拍照
AudioServicesPlaySystemSound(1108);
if ([self.session isRunning]) {
[self.session stopRunning];
}
char ctype;
char content[256];
int xlen;
int i = 0;
_model = [SSCardModel new];
ctype = pResult[i++];
while(i < ret){
ctype = pResult[i++];
for(xlen = 0; i < ret; ++i){
if(pResult[i] == ' ') { ++i; break; }
content[xlen++] = pResult[i];
}
content[xlen] = 0;
if(xlen) {
NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSLog(@"%@",[NSString stringWithCString:(char *)content encoding:gbkEncoding]);
if(ctype == 0x21) {
_model.num = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x22) {
_model.name = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x23) {
_model.gender = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x24) {
_model.nation = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x25) {
_model.address = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x26) {
_model.issue = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
} else if(ctype == 0x27) {
_model.valid = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
}
}
}
// 读取到身份证信息 并返回
if (_model){
dispatch_async(dispatch_get_main_queue(), ^{
ViewController *vc = [ViewController new];
vc.model = self.model;
[self.navigationController pushViewController:vc animated:YES];
});
}
}
CVPixelBufferUnlockBaseAddress(imageBufferRef, 0);
}
CVBufferRelease(imageBufferRef);
}
以上就是身份证识别的主要代码,身份证识别在汉字上面有些误差,英文字母和数字目前正确率为100%,如果用在项目中,可以只需要取出身份证号,用第三方将身份证号的其他信息取出。
因为图片界面和文字识别用到了C99标准,我们导入第三方库libexidcardios.a , 并把Build Setting里的Enable Bitcode设置为NO,bitcode是被编译程序的一种中间形式的代码,不设置的话编译无法通过!本人习惯了用YYKit的工具,所以也引入了。以下是需要的头文件:
#import "excards.h"
#import "YYKit.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
本项目旨在对当前流行的技术做个了解,之前用的二维码条形扫描都用第三个,AVFoundation是ios7.0以后的框架,某些工具出现得更晚一些。现在用这些做媒体处理比第三方快多了,比如这个身份证视频流截图速度就是超级快!
后续用AVFoundation可以做更多事情,比如文字扫描打印,人脸识别登录等等,感觉都是很棒的!在这里感谢IDCardRecognition身份证识别项目的支持,市面上很多的身份证识别代码不是过于复杂就是很陈旧,感谢这些开源的技术人员!
Demo地址:https://github.com/Soldoros/SSScanningCard
欢迎关注我的简书!!!
网友评论