iOS小技能: OCR 之身份证识别 (正反面) 【 应用场景:物流类型app进行实名认证】

本文涉及的产品
票据凭证识别,票据凭证识别 200次/月
手机号三要素核验简版,10000次流量包 3个月
小语种识别,小语种识别 200次/月
简介: 1、功能:可自动快速读出中国二代身份证上的信息(姓名、性别、民族、住址、身份证号码)并截取到身份证图像2、应用场景:信用卡网申、商户进件、实名认证流程为了用户体验提供扫一扫证件识别身份证号码功能。

引言

1、功能:可自动快速读出中国二代身份证上的信息(姓名、性别、民族、住址、身份证号码)并截取到身份证图像

2、应用场景:信用卡网申、商户进件、实名认证流程为了用户体验提供扫一扫证件识别身份证号码功能。
3、原理:

3.1、自定义相机并利用第三方库SDK libexidcardios 进行识别

3.2、添加自定义的扫描界面(中间有一个镂空窗口和来回移动的扫描线)

3.3、人脸小框检测:人脸区域是否在这个人脸小框内,若在,说明用户的确将身份证头像放在了这个框里,那么此时这一帧身份证图像大小正好合适且完整,接下来才捕获该帧,就获得了完整的身份证截图。

4、原理文章:https://kunnan.blog.csdn.net/article/details/117414741

5、如果无法下载Demo,请关注mp:【iOS逆向】,进行获取

[video(video-4u9nEmy4-1626172763106)(type-csdn)(url-https://live.csdn.net/v/embed/169263)(image-https://vedu.csdnimg.cn/2f283d34dd104883ba47cea551403796/snapshots/d181f061db784808b7a6db4bd029cb72-00001.jpg)(title-iOS OCR:自定义相机进行身份证信息识别(正反面))]


I 、 OCR 之身份证识别 (正反)

1.1 原理

1、自定义相机并利用第三方库SDK libexidcardios 进行识别
2、添加自定义的扫描界面(中间有一个镂空窗口和来回移动的扫描线)
3、人脸小框检测:人脸区域是否在这个人脸小框内,若在,说明用户的确将身份证头像放在了这个框里,那么此时这一帧身份证图像大小正好合适且完整,接下来才捕获该帧,就获得了完整的身份证截图。

只要身份证号码处于摄像头预览图层中时,即不用完全对准身份证也可以读取到身份证号码,但此时截取到的身份证图像并不完整。

1.2 Usage

涉及的素材

idcard_first.png、idcard_first_head.png、idcard_back.png、nav_back.png、nav_torch_on.png、nav_torch_off.png

目录结构

Category、Tool、libexidcard、Model、View、Controller这六个文件夹

Info.plist文件中添加权限描述(Key Value)

Privacy - Camera Usage Description 是否允许访问相机
Privacy - Photo Library Usage Description 是否允许访问相册

II、demo 源码

从CSDN下载Demo源码:https://download.csdn.net/download/u011018979/19265912

1、功能:可自动快速读出中国二代身份证上的信息(姓名、性别、民族、住址、身份证号码)并截取到身份证图像

2、应用场景:身份证号码采集:信用卡网申、商户进件、实名认证

3、原理:

3.1、自定义相机并利用第三方库SDK libexidcardios 进行识别

3.2、添加自定义的扫描界面(中间有一个镂空窗口和来回移动的扫描线)

3.3、人脸小框检测:人脸区域是否在这个人脸小框内,若在,说明用户的确将身份证头像放在了这个框里,那么此时这一帧身份证图像大小正好合适且完整,接下来才捕获该帧,就获得了完整的身份证截图。

4、原理文章:https://kunnan.blog.csdn.net/article/details/117414741

2.1 获取实时图像进行信息识别

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
#pragma mark 从输出的数据流捕捉单一的图像帧
// AVCaptureVideoDataOutput获取实时图像,这个代理方法的回调频率很快,几乎与手机屏幕的刷新频率一样快
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    if ([self.outPutSetting isEqualToNumber:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]] || [self.outPutSetting isEqualToNumber:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]]) {
        CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        if ([captureOutput isEqual:self.videoDataOutput]) {
            // 身份证信息识别
            [self IDCardRecognit:imageBuffer];
            
            // 身份证信息识别完毕后,就将videoDataOutput的代理去掉,防止频繁调用AVCaptureVideoDataOutputSampleBufferDelegate方法而引起的“混乱”
            if (self.videoDataOutput.sampleBufferDelegate) {
                [self.videoDataOutput setSampleBufferDelegate:nil queue:self.queue];
            }
        }
    } else {
        NSLog(@"输出格式不支持");
    }
}

#pragma mark - 身份证信息识别
- (void)IDCardRecognit:(CVImageBufferRef)imageBuffer {
    CVBufferRetain(imageBuffer);
    
    // Lock the image buffer
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        size_t width= CVPixelBufferGetWidth(imageBuffer);// 1920
        size_t height = CVPixelBufferGetHeight(imageBuffer);// 1080
        
        CVPlanarPixelBufferInfo_YCbCrBiPlanar *planar = CVPixelBufferGetBaseAddress(imageBuffer);
        size_t offset = NSSwapBigIntToHost(planar->componentInfoY.offset);
        size_t rowBytes = NSSwapBigIntToHost(planar->componentInfoY.rowBytes);
        unsigned char* baseAddress = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);
        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;
            
            IDInfo *iDInfo = [[IDInfo alloc] init];
            
            ctype = pResult[i++];
            
//            iDInfo.type = ctype;            
            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);
                    if(ctype == 0x21) {
                        iDInfo.num = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x22) {
                        iDInfo.name = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x23) {
                        iDInfo.gender = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x24) {
                        iDInfo.nation = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x25) {
                        iDInfo.address = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x26) {
                        iDInfo.issue = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    } else if(ctype == 0x27) {
                        iDInfo.valid = [NSString stringWithCString:(char *)content encoding:gbkEncoding];
                    }
                }
            }
            
            if (iDInfo) {// 读取到身份证信息,实例化出IDInfo对象后,截取身份证的有效区域,获取到图像
                NSLog(@"\n正面\n姓名:%@\n性别:%@\n民族:%@\n住址:%@\n公民身份证号码:%@\n\n反面\n签发机关:%@\n有效期限:%@",iDInfo.name,iDInfo.gender,iDInfo.nation,iDInfo.address,iDInfo.num,iDInfo.issue,iDInfo.valid);
                
                CGRect effectRect = [RectManager getEffectImageRect:CGSizeMake(width, height)];
                CGRect rect = [RectManager getGuideFrame:effectRect];
                
                UIImage *image = [UIImage getImageStream:imageBuffer];
                UIImage *subImage = [UIImage getSubImage:rect inImage:image];
                
                // 推出IDInfoVC(展示身份证信息的控制器)
                IDInfoViewController *IDInfoVC = [[IDInfoViewController alloc] init];

                IDInfoVC.IDInfo = iDInfo;// 身份证信息
                IDInfoVC.IDImage = subImage;// 身份证图像
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.navigationController pushViewController:IDInfoVC animated:YES];
                });
            }
        }
        
        CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    }
    
    CVBufferRelease(imageBuffer);
}

2.2 添加自定义的扫描界面

- (void)drawRect:(CGRect)rect {
    rect = _IDCardScanningWindowLayer.frame;

    // 人像提示框
    UIBezierPath *facePath = [UIBezierPath bezierPathWithRect:_facePathRect];
    facePath.lineWidth = 1.5;
    [[UIColor whiteColor] set];
    [facePath stroke];
    
    // 水平扫描线
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    static CGFloat moveX = 0;
    static CGFloat distanceX = 0;
    
    CGContextBeginPath(context);
    CGContextSetLineWidth(context, 2);
    CGContextSetRGBStrokeColor(context,0.3,0.8,0.3,0.8);
    CGPoint p1, p2;// p1, p2 连成水平扫描线;
    
    moveX += distanceX;
    if (moveX >= CGRectGetWidth(rect) - 2) {
        distanceX = -2;
    } else if (moveX <= 2){
        distanceX = 2;
    }
    
    p1 = CGPointMake(CGRectGetMaxX(rect) - moveX, rect.origin.y);
    p2 = CGPointMake(CGRectGetMaxX(rect) - moveX, rect.origin.y + rect.size.height);
    
    CGContextMoveToPoint(context,p1.x, p1.y);
    CGContextAddLineToPoint(context, p2.x, p2.y);
    
    /*
     // 竖直扫描线
     static CGFloat moveY = 0;
     static CGFloat distanceY = 0;
     CGPoint p3, p4;// p3, p4连成竖直扫描线
     
     moveY += distanceY;
     if (moveY >= CGRectGetHeight(rect) - 2) {
     distanceY = -2;
     } else if (moveY <= 2) {
     distanceY = 2;
     }
     p3 = CGPointMake(rect.origin.x, rect.origin.y + moveY);
     p4 = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + moveY);
     
     CGContextMoveToPoint(context,p3.x, p3.y);
     CGContextAddLineToPoint(context, p4.x, p4.y);
     */
    
    CGContextStrokePath(context);
}

2.3 设置人脸扫描区域

  • 为什么做人脸扫描?
由于预览图层是全屏的,当用户有时没有将身份证对准拍摄框边缘时,也会成功读取身份证上的信息,即也会捕获到不完整的身份证图像。

因此,为了截取到比较完整的身份证图像,在自定义扫描界面的合适位置上加了一>个身份证头像框,让用户将该小框对准身份证上的头像,最终目的是使程序截取到完整的>身份证图像。

当该小框检测到人脸时,再对比人脸区域是否在这个小框内,若在,说明用户的确>将身份证头像放在了这个框里,那么此时这一帧身份证图像大小正好合适且完整,接下来>才捕获该帧,就获得了完整的身份证截图。

有了文字、拍摄区域的提示,99%的用户会主动将身份证和拍摄框边缘对齐,就能够获得完整的身份证图像,不做人脸区域的检测也是可以的

从输出的元数据中捕捉人脸

检测人脸是为了获得“人脸区域”,做“人脸区域”与“身份证人像框”的区域对比,当前者在后者范围内的时候,才能截取到完整的身份证图像
#pragma mark - AVCaptureMetadataOutputObjectsDelegate


-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count) {
        AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
        
        AVMetadataObject *transformedMetadataObject = [self.previewLayer transformedMetadataObjectForMetadataObject:metadataObject];
        CGRect faceRegion = transformedMetadataObject.bounds;
        
        if (metadataObject.type == AVMetadataObjectTypeFace) {
            NSLog(@"是否包含头像:%d, facePathRect: %@, faceRegion: %@",CGRectContainsRect(self.faceDetectionFrame, faceRegion),NSStringFromCGRect(self.faceDetectionFrame),NSStringFromCGRect(faceRegion));
            
            if (CGRectContainsRect(self.faceDetectionFrame, faceRegion)) {// 只有当人脸区域的确在小框内时,才再去做捕获此时的这一帧图像
                // 为videoDataOutput设置代理,程序就会自动调用下面的代理方法,捕获每一帧图像
                if (!self.videoDataOutput.sampleBufferDelegate) {
                    [self.videoDataOutput setSampleBufferDelegate:self queue:self.queue];
                }
            }
        }
    }
}

2.4 去掉人脸扫描区域检测

如果你不想加入人脸识别这一功能,你的app无需这么精细的话

1 、注释掉所有metadataOutput的代码及AVCaptureMetadataOutputObjectsDelegate代理方法 (-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection)

2、在懒加载videoDataOutput对象时,设置setSampleBufferDelegate

AVCaptureVideoDataOutput *)videoDataOutput {
    if (_videoDataOutput == nil) {
        _videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        
        _videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
        _videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey:self.outPutSetting};
        
        [_videoDataOutput setSampleBufferDelegate:self queue:self.queue];
        

    }
    
    return _videoDataOutput;
}

#pragma mark metadataOutput
-(AVCaptureMetadataOutput *)metadataOutput {
    if (_metadataOutput == nil) {
        _metadataOutput = [[AVCaptureMetadataOutput alloc]init];
        
//        [_metadataOutput setMetadataObjectsDelegate:self queue:self.queue];
    }
    
    return _metadataOutput;
}

III、经典案例:识别身份证号码

在这里插入图片描述

https://mp.weixin.qq.com/s/C-k_oZCYoxEUO0ujQZmQ2Q

IV、遇到的问题

4.1 -[UIViewController init] must be used from main thread only

解决方案:

                dispatch_async(dispatch_get_main_queue(), ^{
                    
                    // 推出IDInfoVC(展示身份证信息的控制器)
                    IDInfoViewController *IDInfoVC = [[IDInfoViewController alloc] init];

                    IDInfoVC.IDInfo = iDInfo;// 身份证信息
                    IDInfoVC.IDImage = subImage;// 身份证图像

                    [self.navigationController pushViewController:IDInfoVC animated:YES];
                });

see also:光学文字识别-储蓄卡

libexbankcardcore
exbankcard.h
BankCard.h

从CSDN下载Demo源码:https://download.csdn.net/download/u011018979/19268420

1、功能:扫描银行卡识别信息( 银行名称、 银行卡号)并截取银行卡图像

2、应用场景:快速填充银行卡号的场景,比如商户进件、实名认证

3、原理:

3.1、自定义相机并利用第三方库SDK libexbankcardios.alibbexbankcard.a进行识别(识别次数无限,免费 )

3.2、添加自定义的扫描界面(中间有一个镂空窗口和来回移动的扫描线)

4、原理文章:https://kunnan.blog.csdn.net/article/details/117421214

5、如果无法下载Demo,请关注mp:【iOS逆向】,进行获取

https://github.com/zhangkn/libexidcardios

目录
相关文章
|
4月前
|
JavaScript IDE 开发工具
找不到模块“./App.vue”或其相应的类型声明。ts(2307)
这篇文章介绍了在Vue 3 + TypeScript + Vite开发环境中解决找不到`.vue`文件模块或其类型声明错误的两种方法:使用VSCode的TypeScript Vue Plugin (Volar)插件或手动在`env.d.ts`文件中声明`*.vue`模块类型。
669 0
找不到模块“./App.vue”或其相应的类型声明。ts(2307)
|
5月前
|
机器学习/深度学习 人工智能 文字识别
文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
文本,文字扫描01,OCR文本识别技术展示,一个安卓App,一个简单的设计,文字识别可以应用于人工智能,机器学习,车牌识别,身份证识别,银行卡识别,PaddleOCR+SpringBoot+Andr
|
6月前
|
移动开发 小程序 开发工具
微信支付的类型分析(JSAPI+APP+H5+NATIVE+付款码+合单)
微信支付的类型分析(JSAPI+APP+H5+NATIVE+付款码+合单)
612 1
|
5月前
4. 解决uni-app开发过程中view、image等标签出现诸如“出现错误:类型“{ class: string; }”的参数不能赋给类型“.......”
4. 解决uni-app开发过程中view、image等标签出现诸如“出现错误:类型“{ class: string; }”的参数不能赋给类型“.......”
501 0
|
7月前
|
iOS开发
iOS使用.framework类型的静态库
iOS使用.framework类型的静态库
52 1
|
7月前
|
iOS开发 Perl
iOS使用.a类型的静态库
iOS使用.a类型的静态库
56 1
|
7月前
|
开发工具 iOS开发
iOS制作.a类型的静态库
iOS制作.a类型的静态库
47 1
|
7月前
|
编解码 安全 测试技术
APP测试类型
APP测试类型
|
Web App开发 iOS开发 开发者
ios证书类型及其作用说明
ios证书类型及其作用说明
111 0
|
程序员 开发工具 iOS开发
iOS 获取手机的型号,系统版本,软件名称,软件版本,手机类型(型号)
iOS 获取手机的型号,系统版本,软件名称,软件版本,手机类型(型号)
152 0
下一篇
DataWorks