天天品尝iOS7甜点 :: Day 16 :: Decoding QR Codes with AVFoundation

这篇文章是天天品尝iOS7甜点系列的一部分,你可以查看完整的系列目录:天天品尝iOS7甜点


Introduction - 介绍

在昨天,我们已经查看了CoreImage中包含的新的过滤器中的一些,并且发现在iOS7中,我们可以有能力自己生成一个二维码。所以,既然给出了如何生成二维码,就需要能够对这个二维码进行解码,当然不能让你失望了,我们在今天的文章中就来介绍如何使用AVFoundation框架中的一些新特性进行解码二维码。

本章的实例程序能够在github上面进行访问,访问地址:github.com/ShinobiControls/iOS7-day-by-day

AVFoundation pipeline - AVFoundation工作流

AVFoundation是一个大的框架,它可以促进创建,编辑,显示和捕获多媒体。这篇文章并不是主要介绍如何使用AVFoundation,而是我们要通过这个框架来提取手机屏幕上面的二维码,为了能够使用这个框架,我们首先需要导入这个框架:

1
@import AVFoundation;

当我们捕获媒体的时候,我使用AVCaptureSession类来充当我们工作流的核心。然后我们需要添加输入和输出来完成这次会话。我们将会在viewDidLoad方法中设置这些。首先,创建一个会话:

1
AVCaptureSession *session = [[AVCaptureSession alloc] init];

我们需要添加主要的摄像头作为会话的输入。输入就是一个AVCaptureDeviceInput对象,它是通过一个AVCaptureDevice对象创建的:

1
2
3
4
5
6
7
8
9
10
11
12
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;

AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];

if (input) {
// Add the input to the session
[session addInput:input];
}else {
NSLog(@"error: %@", error);
return;
}

这里我们获得了一个默认视频输入的设备引用,它将代表设备上面的后置摄像头。然后使用这个设备创建一个AVCaptureDeviceInput输入对象,然后把它添加到会话中。

为了获得获得视频中的内容,我们需要创建一个AVCaptureVideoPreviewLayer.它是一个CALayer的子类,当它添加到会话中的时候,它可以显示当前视图中的输出内容。考虑到这些,我们需要一个实例变量_previewLayer来作为AVCaptureVideoPreviewLayer的引用:

1
2
3
4
5
_previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:session];
_previewLayer.videoGravit = AVLayerVideoGravityResizeAspectFill;
_previewLayer.bounds = self.view.bounds;
_previewLayer.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
[self.view.layer addSublayer:_previewLayer];

videoGravity属性是用来指定视频是如何出现在层上面。由于视频的尺寸和屏幕不是相等的,我们可以把视频的边缘给砍掉,然后让它填充整个屏幕,所以使用AVLayerVideoGravityResizeAspectFill值。我们添加这个层作为视图层的子层。

现在才是真正开启会话的时候:

1
2
// Start the AVSession running
[session startRunning];

如果你运行应用程序,你将会摄像头的输出信息呈现在屏幕上面-神奇的感觉。

Capturing metadata - 捕获元数据

从iOS5开始就可以使用我们上述的内容,但是在这个章节,我们将会做很多事情,而这些东西只有在iOS7中才会存在的。

一个AVCaptureSession对象有附加的AVCaptureOutput对象。形成一个工作流的终点。在这里我们感兴趣的是AVCaptureOutput的子类AVCaptureMetadataOutpu.它可以查出视频中的任何元数据然后输出它。这个类型的输出并不会形成图像或者视频,而是从图像或者视频中提取的元数据本身。设置这些如下所示:

1
2
3
4
5
*out = [[AVCaptureMetadataOutput alloc] init];
// Have to add the output before setting metadata types
[session addOutput:output];
// What different things can we register to recognise?
NSLog(@"%@", [output availableMetadataObjectTypes]);

这里,我们创建一个元数据输出对象,并且把它作为一个输出添加到会话中。然后我们可以提供一个方法用来记录不同的元数据类型的列表:

1
2
3
4
5
6
7
8
9
10
11
12
2013-10-09 11:10:26.085 CodeScanner[6277:60b] (
"org.gs1.UPC-E",
"org.iso.Code39",
"org.iso.Code39Mod43",
"org.gs1.EAN-13",
"org.gs1.EAN-8",
"com.intermec.Code93",
"org.iso.Code128",
"org.iso.PDF417",
"org.iso.QRCode",
"org.iso.Aztec"
)

需要重要注意的就是我们在尝试这个的时候已经把我们的元数据输出设置到会话中。由于可用的类型依赖与输入设备。我们可以使用下面的代码注册查找的二维码类型:

1
2
// We're only interested in QR Codes
[output setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];

这是一个数组类型,所以你可以只能多个你想要的元数据类型。

当元数据对象从视频流中找到一些东西,它就可以生成元数据,然后通知它的代理,所以,我们需要设置代理:

1
2
// This VC is the delegate, Please call us on the main queue
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

由于AVFoundation被设计成可以允许线程访问,所以我们需要指定代理在那个线程中使用。

我们需要适配AVCaptureMetadataOutputObjectsDelegate协议:

1
2
3
4
5
@interface SCViewController () <AVCaptureMetadataOutputObjectsDelegate> {
AVCaptureVideoPreviewLayer *_previewLayer;
UILabel *_decodeMessage;
}
@end

我们需要实现协议的方法是captureOutput:didOutputMetadataObjects:fromConnection::

1
2
3
4
5
6
7
8
9
10
#prgma mark - AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
for (AVMetadataObject *metadata in metadataObjects) {
if ([metadata.type isEqualToString:AVMetadataObjectTypeQRCode]) {
AVMetadataMachineReadableCodeObject *transformed = (AVMetadataMachineReadableCodeObject *)metadata;
// Update the view with the decoded text
_decodedMessage.text = [transformed stringValue];
}
}
}

其中的metadataObjects数组包含了AVMetadataObject对象(就是我们设置检索的类型的数据).由于我们只注册查找二维码类型,我们我们得到的数组里面的内容的类型都是AVMetadataObjectTypeQRCode.AVMetadataMachineReadableCodeObject类型具有一个stringValue属性,它包含了所有元数据队形解码的值信息。在这里,我们把获得到的字符串信息显示到_decodedMessage标签上面,标签我们可以在viewDidLoad方法中进行设置:

1
2
3
4
5
6
7
// Add a label to display the resultant message
_decodedMessage = [[UILabel alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.bounds) - 75, CGRectGetWidth(self.view.bounds), 75)];
_decodedMessage.numberOfLines = 0;
_decodedMessage.backgroundColor = [UIColor colorWithWhite:0.8 alpha:0.9];
_decodedMessage.textColor = [UIColor darkGrayColor];
_decodedMessage.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:_decodedMessage];

运行应用程序,然后用摄像头对准二维码,我们就可以在标签上面解码出二维码上面的内容,然后显示出来,具体的效果如下图所示:

Drawing the code outline - 给二维码添加上线框

除了提供解码元数据对象,它还包含了位置的边界,我们的应用程序将会直观的标识出具体的元数据对象的具体的位置。

为了达到这个目的,首先我们需要创建一个UIView的子类,它可以提供一系列的点,然后将会连接起来。这将会使我们明确的来构建它:

1
2
3
@interface SCShapeView : UIView
@property (strong, nonatomic) NSArray *corners;
@end

其中的corners属性数组包含了CGPoint对象,每一个都代表我们希望绘制图像路径的拐角处。

我们将会使用一个CAShapeLayer来进行绘制这些点,并且这是非常有效率的方法来绘制图形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface SCShapeView() {
CAShapeLayer *_outline;
}
@end

@implementation SCShapeView

- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
_outline = [CAShapeLayer new];
_outline.strokeColor = [[[UIColor blueColor] colorWithAlphaComponent:0.8] CGColor];
_outline.lineWidth = 2.0;
_outline.fillColor = [[UIColor clearColor] CGColor];
[self.layer addSublayer:_outline];
}
return self;
}

@end

在这里我们创建一个图像层,设置一些外观的属性,然后把它添加到当前的层中。我们现在就需要设置图形的路径,也就是我们现在需要设置corners属性了:

1
2
3
4
5
6
- (void)setCorners:(NSArray *)corners {
if (corners != _corners) {
_corners = corners;
_outline.path = [[self createPathFromPoints:corners] CGPath];
}
}

上述含义是如果corners属性发生了变化,图形就将会使用新的位置进行绘制.我们使用一个工具方法来通过封装了CGPoint对象的数组对象来创建一个UIBezierPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UIBeazierPath *)cratePathFromPoints:(NSArray *)points {
UIBezierPath *path = [UIBezierPath new];
// Start at the first corner
[path moveToPoint:[[points firstObject] CGPointValue]];

// Now draw lines around the corners
for (NSUinter i = 1, i < points.count; i++) {
[path addLineToPoint:[points[i] CGPointValue]];
}

// And join it back to the first corner
[path addLineToPoint:[[points firstObject] CGPointValue]];

return path;
}

这实际上是创建了一个完成的图形,运用了UIBezierPath的API。

现在我们创建这个图形视图,我们需要在试图控制器中使用它,然后把它现在在我们查找的二维码上面。让我们创建一个实例变量,然后在viewDidLoad方法中进行初始化:

1
2
3
4
_boundingBox = [[SCShapeView alloc] initWithFrame:self.view.bounds];
_boundingBox.backgroundColor = [UIColor clearColor];
_boundingBox.hidden = YES;
[self.view addSubview:_boundingBox];

现在我们需要更新这个视图中元数据输出的代理方法:

1
2
3
4
5
6
7
8
9
10
11
// Transform the meta-data coordinates to screen coords
AVMetadataMachineReadableCodeObject *transformed = (AVMetadataMachineReadableCodeObject *)[_previewLayer transformedMetadataObjectForMetadataObject:metadata];
// Update the frame on the _boundingBox view, and show it
_boundingBox.frame = transformed.bounds;
_boundingBox.hidden = NO;
// Now convert the corners array into CGPoints in the coordinate system
// of the bounding box itself
NSArray *translatedCorners = [self translatePoints:transformed.corners fromView:self.view toView:_boundingBox];

// Set the corners array
_boundingBox.corners = translatedCorners;

AVFoundation在屏幕上面进行绘制的时候,使用一个不同于UIKit的坐标系,所以第一部分我们需要使用AVCaptureVideoPreviewLayer中的一个代码片段transformedMetadataObjectForMetadataObject:方法来把自身的坐标系进行转换。使它编程我们自己预览层的坐标系统.

然后我们设置我们图形层的frame,它是通过查找的二维码的bounds得到的,然后对图形层进行显示.

现在我们就需要设置corners属性了,让图形层能够正确的显示,但是在此之前,我们需要再次改变系统坐标系。我们可以运用下面的工具类来达到这个目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSArray *)translatePoints:(NSArray *) fromView:(UIView *)fromView toView:(UIView *)toView {
NSMutableArray *translatedPoints = [NSMutableArray new];

// The points are provided in a dictionary with keys X and Y
for (NSDictionary *point in points) {
// Let's turn them into CGPoints
CGPoint pointValue = CGPointMake([point[@"X"] floatValue], [point[@"Y"] floatValue]);
// Now translate from one view to the other
CGPoint translatedPoint = [fromView convertPoint:pointValue toView:toView];
// Box them up and add to the array
[translatedPoint addObject:[NSValue valueWithCGPoint:translatedPoint]];
}
return [translatedPoints copy];
}

通过上面的方法我们转换成为正确的CGPoint数组设置到corners属性中。

如果你运行应用程序,你就会看到一个高亮的线框显示在我们的二维码上面:

最后一个小点是把绘制出来的线框过一段时间消失掉。这就阻止了当前没有发现二维码信息的时候,线框还存在的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)startOverlayHideTimer
{
// Cancel it if we're already running
if(_boxHideTimer) {
[_boxHideTimer invalidate];
}

// Restart it to hide the overlay when it fires
_boxHideTimer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(removeBoundingBox:)
userInfo:nil
repeats:NO];
}

- (void)removeBoundingBox:(id)sender
{
// Hide the box and remove the decoded text
_boundingBox.hidden = YES;
_decodedMessage.text = @"";
}

我们可以在代理方法方法中进行调用计时器:

1
2
// Sart the timer which will hiden the overlay
[self startOverlayHideTimer];

Conclusion - 总结

AVFoundation框架是强大且复杂的,但是在iOS7中,它变得更好了。以前在移动设备上面查找条形码是一个十分艰巨的任务,但是通过我们今天介绍的这些新的元数据输出类型,现在就变得十分的简单而高效。不管你是否需要使用到条形码,这都是一个你需要用到的简单方法。

本文翻译自:iOS7 Day-by-Day :: Day 16 :: Decoding QR Codes with AVFoundation

文章目录
  1. 1. Introduction - 介绍
  2. 2. AVFoundation pipeline - AVFoundation工作流
  3. 3. Capturing metadata - 捕获元数据
  4. 4. Drawing the code outline - 给二维码添加上线框
  5. 5. Conclusion - 总结
,