关于二维码(或者条形码,以下归类简称二维码)扫描和生成的,我相信网络上相关的文章层数不穷,但是,大部分都是直接粘贴上代码,不去解释,这样导致每次遇到诸如此类的功能行的问题,简单方便的CV工程师程序,久而久之,对于程序开发更局限于表面,开发这条道路也会越来越局限了.
好了,言归正传,接下来我就分享一下,自己在二维码开发的过程中遇到的问题和一些经验吧.
注:这里的扫描仅限于相机扫描,所以建议各位开发者,需要在真机上进行测试
一. 二维码的扫描
0.准备工作
- 1).宏定义
定义当前页面的宽和高,通过delegate.window获取frame
#define SCREEN_WIDTH [UIApplication sharedApplication].delegate.window.frame.size.width
#define SCREEN_HEIGHT [UIApplication sharedApplication].delegate.window.frame.size.height - 2).协议
-
AVCaptureMetadataOutputObjectsDelegate
-
这是有关摄像设备输出的相关的代理,这里我们需要用到扫描后的结果,后面会做出详细的解释
-
UIAlertViewDelegate
主要是显示出来扫描的结果,可以看做相对的辅助
1.依赖库
因为二维码的扫描是基于真机上的相机,我们需要引入<AVFoundation/AVFoundation.h>
#import <AVFoundation/AVFoundation.h>
关于这个库的介绍,相信很多做过视频和音频播放的童鞋们并不陌生,这个也是基于cocoa下比较常用的库
2.定义对应变量属性
关于属性的创建,我们需要了解到每个属性的作用和相关操作
1).创建相机AVCaptureDevice
AVCaptureDevice的每个实例对应一个设备,如摄像头或麦克风。集体的信息可以参考苹果相关API.
@property (strong,nonatomic)AVCaptureDevice * device;
2).创建输入设备AVCaptureDeviceInput
AVCaptureDeviceInput是AVCaptureInput子类提供一个接口,用于捕获从一个AVCaptureDevice媒体。AVCaptureDeviceInput是AVCaptureSession实例的输入源,提供媒体数据从设备连接到系统。
@property (strong,nonatomic)AVCaptureDeviceInput * input;
3).创建输出设备AVCaptureMetadataOutput
AVCaptureMetadataOutput对象拦截元数据对象发出的相关捕获连接,并将它们转发给委托对象进行处理。您可以使用这个类的实例来处理特定类型的元数据中包含的输入数据。你使用这个类你做其他的输出对象的方式,通常是通过添加一个AVCaptureSession对象作为输出。简单而言就是,AVCaptureMetadataOutput将获取到的元数据交给AVCaptureSession进行处理的途径.
@property (strong,nonatomic)AVCaptureMetadataOutput * output;
4).创建AVFoundation中央枢纽捕获类AVCaptureSession。
下面的是关于AVCaptureSession的原生API
To perform a real-time capture, a client may instantiate AVCaptureSession and add appropriate AVCaptureInputs, such as AVCaptureDeviceInput, and outputs, such as AVCaptureMovieFileOutput. [AVCaptureSession startRunning] starts the flow of data from the inputs to the outputs, and [AVCaptureSession stopRunning] stops the flow. A client may set the sessionPreset property to customize the quality level or bitrate of the output.
在苹果的API中大致是这样重点解释的:
- 执行实时捕获,一个客户可以实例化AVCaptureSession并添加适当AVCaptureInputs,AVCaptureDeviceInput和相关的输出,如AVCaptureMovieFileOutput。
- [AVCaptureSession startRunning]开始的数据流从输入到输出
- [AVCaptureSession stopRunning]停止流动。
- 客户端可以设置sessionPreset属性定制质量水平或输出的比特率
@property (strong,nonatomic)AVCaptureSession * session;
5).创建AVCaptureSession预览视觉输出AVCaptureVideoPreviewLayer,在API介绍中,我们不难发现,他是继承自CoreAnimation的CALayer的子类,,这里我们可以看做是将图片输出的一个平台(搭载), 因此适合插入在一层的层次结构作为一个图形界面的一部分。
- 在苹果原生API介绍中,我们可以了解到,我们可以通过创建+ layerWithSession:或-initWithSession:对AVCaptureVideoPreviewLayer进行实例与捕获会话预览。
- 使用"videoGravity”属性,可以影响内容是如何看待相对于层界限。
- 在某些硬件配置,层可以使用"orientation"(操纵的方向) 和 "mirrored"(镜像)等进行操作.
@property (strong,nonatomic)AVCaptureVideoPreviewLayer * preview;
3.初始化变量
确定了相关属性,接下来,我们对相关变量进行初始化,就好比原料我们有了,接下来我们对这些材料进行粗略的加工.
至于初始化的位置,一般情况下我们将一个页面作为二维码操作的,这边算作是一个模块处理,所以,建议在ViewDidLoad方法(生命周期)里面进行创建.如果需要特殊处理,具体情况具体分析吧,因为需求不一样,所以,下面的栗子采用在ViewDidLoad中进行.
1).初始化基础"引擎"Device
// Device,这里需要注意的是AVCaptureDevice不能直接创建的实例
self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
2).初始化输入流 Input,并添加Device
self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];
3).初始化输出流Output
self.output = [[AVCaptureMetadataOutput alloc] init];
下面,敲黑板了,
这里需要注意的是:在输出流的设置中,如果不对AVCaptureMetadataOutput的属性rectOfInterest进行设置,扫描的区域默认是展示的AVCaptureVideoPreviewLayer全部区域.这里我们采用区域扫描,也就是所谓的条框扫描,提高用户体验度.
// 创建view,通过layer层进行设置边框宽度和颜色,用来辅助展示扫描的区域
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
redView.layer.borderWidth = 2;
redView.layer.borderColor = [UIColor cyanColor].CGColor;
[self.view addSubview:redView];
//设置输出流的相关属性
// 确定输出流的代理和所在的线程,这里代理遵循的就是上面我们在准备工作中提到的第一个代理,至于线程的选择,建议选在主线程,这样方便当前页面对数据的捕获.
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
3+). 设置扫描区域的大小(这个也是我在开发中,遇到的最*最^n坑爹的问题,标注一下)
为什么是"3+"呢,主要是本来想将这部分放在后面单独讲,但是考虑到连贯性,就单独做一个补充小节来讲吧,而且属于设置session层的部分.
self.output.rectOfInterest = CGRectMake((100)/(SCREEN_HEIGHT),(SCREEN_WIDTH - 100 - 200)/SCREEN_WIDTH,100/SCREEN_HEIGHT,200/SCREEN_WIDTH);
其实呢,我是想在当前view创建一块CGRectMake(100, 100, 200, 100)的扫描区域,如下图的扫描区域框:
但是呢,这里需要说明的一点就是,我们如果按照常规的CGRect创建方式去设置,是肯定不对的,他会出现扫描区域不是预设的,为什么呢?
原因很蛋疼,因为我们平常的设置CGRect是以左上角为原点,横向增加为+x,纵向增加为+y,横向为宽度width,纵向为高度height,没毛病吧,但是,坑爹的就是output的rectOfInterest是以左上角为原点,x与y数值对调,width和height数值对调,并且,x,y,width和height的数值为0 ~ 1.如下对比图:
说明一下:这里的计算对比,针对的是,知道扫描框相对于父视图的位置,我们根据扫描框的CGRect可以计算出需要设置的output的rectOfInterest的CGRect.至于为什么需要对调原理,最近有时间研究研究(苹果这个设计让很多人不解),目前仅供参考.大家要是有相关的方案或者明确为何这样,希望在下面留言,我们一起深究一下.
这里的概念区别于我们所认知的CGRect的设置,建议童鞋们还是手动算一下,之后进行边缘化测试,就是测试二维码从边缘完全进入扫描区域并且存在扫描任务的位置.
4).初始化捕获数据类AVCaptureSession
// 初始化session
self.session = [[AVCaptureSession alloc]init];
// 设置session类型,AVCaptureSessionPresetHigh 是 sessionPreset 的默认值。
[_session setSessionPreset:AVCaptureSessionPresetHigh];
补充:这里简单对sessionPreset的属性值进行以下说明:
苹果API中提供了如下的四种方式:
// AVCaptureSession 预设适用于高分辨率照片质量的输出
AVF_EXPORT NSString *const AVCaptureSessionPresetPhoto NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 预设适用于高分辨率照片质量的输出
AVF_EXPORT NSString *const AVCaptureSessionPresetHigh NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 预设适用于中等质量的输出。 实现的输出适合于在无线网络共享的视频和音频比特率。
AVF_EXPORT NSString *const AVCaptureSessionPresetMedium NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 预设适用于低质量的输出。为了实现的输出视频和音频比特率适合共享 3G。
AVF_EXPORT NSString *const AVCaptureSessionPresetLow NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
PS:在API的介绍中,除了以上的迹象,我们还会看到好几种类型,不过不是针对 ipad iphone 的。针对 MAC_OS,不便介绍,感兴趣的可以查看相关API
5).将输入流和输出流添加到session中
这里可以看做是集成,就好比是,我们现在正在建造一辆汽车,我们的原件已经做好了,现在要放到汽车的骨架上.
// 添加输入流
if ([_session canAddInput:self.input]) {
[_session addInput:self.input];
}
// 添加输出流
if ([_session canAddOutput:self.output]) {
[_session addOutput:self.output];
}
// 下面的是比较重要的,也是最容易出现崩溃的原因,就是我们的输出流的类型
// 1.这里可以设置多种输出类型,这里必须要保证session层包括输出流
// 2.必须要当前项目访问相机权限必须通过,所以最好在程序进入当前页面的时候进行一次权限访问的判断(在文章的最后,我会贴出相关的代买)
self.output.metadataObjectTypes =@[AVMetadataObjectTypeQRCode];
6).设置输出展示平台AVCaptureVideoPreviewLayer
// 初始化
self.preview =[AVCaptureVideoPreviewLayer layerWithSession:_session];
// 设置Video Gravity,顾名思义就是视频播放时的拉伸方式,默认是AVLayerVideoGravityResizeAspect
// AVLayerVideoGravityResizeAspect 保持视频的宽高比并使播放内容自动适应播放窗口的大小。
// AVLayerVideoGravityResizeAspectFill 和前者类似,但它是以播放内容填充而不是适应播放窗口的大小。最后一个值会拉伸播放内容以适应播放窗口.
// 因为考虑到全屏显示以及设备自适应,这里我们采用fill填充
self.preview.videoGravity =AVLayerVideoGravityResizeAspectFill;
// 设置展示平台的frame
self.preview.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// 因为 AVCaptureVideoPreviewLayer是继承CALayer,所以添加到当前view的layer层
[self.view.layer insertSublayer:self.preview atIndex:0];
7).一切准备就去,开始运行
[self.session startRunning];
4.扫描结果处理
这里就需要用到我们之前设置的两个代理AVCaptureMetadataOutputObjectsDelegate和UIAlertViewDelegate
在AVCaptureMetadataOutputObjectsDelegate的代理方法中,有didOutputMetadataObjects这个方法,表示输出的结果,我们扫描二维码的结果将要在这里进行处理
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
// 判断扫描结果的数据是否存在
if ([metadataObjects count] >0){
// 如果存在数据,停止扫描
[self.session stopRunning];
// AVMetadataMachineReadableCodeObject是AVMetadataObject的具体子类定义的特性检测一维或二维条形码。
// AVMetadataMachineReadableCodeObject代表一个单一的照片中发现机器可读的代码。这是一个不可变对象描述条码的特性和载荷。
// 在支持的平台上,AVCaptureMetadataOutput输出检测机器可读的代码对象的数组
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex:0];
// 获取扫描到的信息
NSString *stringValue = metadataObject.stringValue;
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"扫描结果"
message:stringValue
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"确定", nil];
[self.view addSubview:alert];
[alert show];
}
}
在UIAlertViewDelegate代理方法中,我们确认信息后,可以对信息有相应的操作,这里我只是简单的进行了继续进行数据捕捉(扫描)
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
[self.session startRunning];
}
5.运行展示
下面我们看看运行的结果,这里测试过程包括区域扫描的边缘化测试:
二.二维码的生成
讲完了二维码的扫描,接下来我们接着讲讲二维码的生成.
二维码的生成的核心在于图形的绘制,我们通过滤镜CIFilter和图形绘制的上下文方式生成二维码.
0.准备工作
1.依赖库
二维码的生成区别于二维码的扫描,因为他的核心是基于图形的绘制完成的,所以需要导入CoreImage框架
#import <CoreImage/CoreImage.h>
2.创建滤镜CIFilte
1.创建滤镜CIFilter实例对象,并通过类方法,将filter的名称指定为CIQRCodeGenerator
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
2.由于filter的强大,我们目前仅是实现简单的二维码的生成,所以,我们将filter的各项属性均设置成默认
[filter setDefaults];
3.给过滤器CIFilter添加数据
这里需要说明的是,二维码的主要内容可以是如下几种类型(传统的条形码只能放数字):
- 纯文本
- URL
- 名片(这个有待考证,表示我并没有试验过)
// 基于多种类型,我们简单的生成字符串的二维码
// 创建字符串
NSString *dataString = @"锋绘动漫";
// 将字符串转换成date类型,并通过KVO的形式保存至滤镜CIFilter(目前指定为二维码)的inputMessage中
NSData *data = [dataString dataUsingEncoding:NSUTF8StringEncoding];
[filter setValue:data forKeyPath:@"inputMessage"];
4.获取输出的二维码
CIImage *outputImage = [filter outputImage];
5.获取高清的二维码,并展示
因为CIFilter生成的二维码相对而言模糊,达不到设备快速识别的需求,同时用户体验差.
所以通过图像绘制的上下文来获得高清的二维码图片.
PS:由于获取高清图片不是该章节的重点,相关的代码部分来自网络,放到文章的最后,仅供看考
// 获取二维码
self.imageView.image = [self createErWeiMaImageFormCIImage:outputImage withSize:200];
6.运行结果
扫描的内容请参考第一节"二维码生成"的运行结果
我是调皮的分割线
小结
这就是我理解的二维码的生成和二维码的扫描,其中主要的还是针对两个框架的研究,让我学到了很多东西.
在学习的过程中,比较建议大家多去查看苹果原生的API,这个对自我理解是比较重要的,网络上的总结出来的,只能作为自己的参考,切不可取而代之,最大的禁忌就是CV工程师的道路,再简单的代码也要自己敲出来.
有什么问题欢迎大家留言多多留言,多多交流
下面附上demo地址(本人的github上):
二维码扫描(可区域)和生成的Demo地址
PS:相关代码
1.权限访问
NSString *mediaType =AVMediaTypeVideo;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
if(authStatus ==AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
UIAlertView *alert =[[UIAlertView alloc] initWithTitle:@“项目名称”
message:@"请在iPhone的“设置”-“隐私”-“相机”功能中,找到“项目名称”打开相机访问权限"
delegate:nil
cancelButtonTitle:@"确定"
otherButtonTitles: nil];
[alert show];
return;
}
2.获取高清图片
- (UIImage *)getErWeiMaImageFormCIImage:(CIImage *)image withSize:(CGFloat) size {
CGRect extent = CGRectIntegral(image.extent);
CGFloat scale = MIN(size/CGRectGetWidth(extent), size/CGRectGetHeight(extent));
// 1.创建bitmap;
size_t width = CGRectGetWidth(extent) * scale;
size_t height = CGRectGetHeight(extent) * scale;
CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray();
CGContextRef bitmapRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone);
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef bitmapImage = [context createCGImage:image fromRect:extent];
CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone);
CGContextScaleCTM(bitmapRef, scale, scale);
CGContextDrawImage(bitmapRef, extent, bitmapImage);
// 2.保存bitmap到图片
CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapRef);
CGContextRelease(bitmapRef);
CGImageRelease(bitmapImage);
return [UIImage imageWithCGImage:scaledImage];
}