AVFoundation开发秘籍笔记:第7章 高级捕捉功能

7.1 视频缩放

在iOS7之前的版本中,苹果公司通过AVCaptureConnection的videoScaleAndCropFactor属性对摄像头缩放进行了有限制的支持。开发者可以调整连接缩放的值从默认的1.0增加到videoMaxScaleAndCropFactor属性定义的最大值。这一功能在一些特 定的场景非常实用,不过带有一些明显的限制。第一个就是videoScaleAndCropFactor是一个连接级的设置,意思就是要想在用户界面正确获取它的状态值,开发者必须对AVCaptureVideoPreviewLayer应用一个正确的缩放变换。这是一个额外的工作量,并且当应用高缩放因子进行变换时会导致预览层的图片质量降低。第二个也是最大的限制就是这个属性只能在AVCaptretilllmageOutput连接中设置,这就让视频捕捉类应用程序无法利用这一特性。幸运的是,iOS 7版本为缩放功能给出了一个更好的方法。这个方法可以让你直接对AVCaptureDevice对象使用缩放参数,这意味着所有会话的输出,包括预览层,都会自动响应这一设置的状态。让我们现在就看一下如何使用这个新功能吧。

AVCaptureDevice提供了名为videoZoomFactor的属性,用于控制捕捉设备的缩放等级。这个属性的最小值为1.0,即不能进行缩放的图片。最大值由捕捉设备的activeFormat值确定,它是新类型AVCaptureDeviceFormat的一个实例。这个类定义了活动捕捉格式功能的细节,其中就包括videoMaxZoomFactor。

设备执行缩放效果是通过居中裁剪由摄像头传感器捕捉到的图片实现。当设置了一个低缩放因子时,一般低于1.5, 图片等于或大于输出尺寸,这就在不用放大的情况下进行了一个适度的缩放,图片质量得以全部保存。在哪个点开始放大图像需要由AVCaptureDeviceFormat的videoZoomFactorUpscaleThreshold值确定。

让我们看一下示例代码来学习如何使用这个功能。在Chapter 7目录下可以找到一个名为ZoomKamera_Starter的示例项目,如图7-1所示。


注意:
本章中的示例项目用到一个基类THBaseCameraController。这个类本质上就是第6章中创建的摄像头控制器,只不过增加了一些扩展点。尤其是它定义了一些可以重写的方法来配置会话输入、输出和会话预设。这样就能把注意力集中在本章需要介绍的新功能上,而避免被前一章内容的干扰。

在启动项目中,可以看到一个THCameraController类的存根实现。这个类扩展了THBaseCameraController并添加了视频缩放功能。代码清单7-1是这个类的接口。

代码清单7-1 THCameraController 接口

#import <AVFoundation/AVFoundation.h>
#import "THBaseCameraController.h"

@protocol THCameraZoomingDelegate <NSObject>                            // 1
- (void)rampedZoomToValue:(CGFloat)value;
@end

@interface THCameraController : THBaseCameraController

@property (weak, nonatomic) id<THCameraZoomingDelegate> zoomingDelegate;

- (BOOL)cameraSupportsZoom;                                             //2

- (void)setZoomValue:(CGFloat)zoomValue;
- (void)rampZoomToValue:(CGFloat)zoomValue;
- (void)cancelZoom;

@end

(1) THCameraZoomingDelegate提供了一个rampedZoomToValue:方法,用于在用户界面保持缩放滑动条控件与当前缩放状态的同步。
(2)由于并不是所有的硬件都支持这个功能,所以需要添加一个cameraSupportsZoom方法,这样客户端可以进行查询,以确定是否展示缩放控件。

让我们转到类的实现中,开始学习如何实现具体的缩放功能,如代码清单7-2所示。

代码清单7-2 THCameraController 的实现

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

const CGFloat THZoomRate = 1.0f;

// KVO Contexts
static const NSString *THRampingVideoZoomContext;
static const NSString *THRampingVideoZoomFactorContext;

@implementation THCameraController

- (BOOL)cameraSupportsZoom {
    return self.activeCamera.activeFormat.videoMaxZoomFactor > 1.0f;        // 1
}

- (CGFloat)maxZoomFactor {
    return MIN(self.activeCamera.activeFormat.videoMaxZoomFactor, 4.0f);    // 2
}

- (void)setZoomValue:(CGFloat)zoomValue {                                   // 3
    if (!self.activeCamera.isRampingVideoZoom) {

        NSError *error;
        if ([self.activeCamera lockForConfiguration:&error]) {              // 4

            // Provide linear feel to zoom slider
            CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);      // 5
            self.activeCamera.videoZoomFactor = zoomFactor;

            [self.activeCamera unlockForConfiguration];                     // 6

        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

...

@end

(1)首先是cameraSupportsZoom方法的实现。 通过当前选中的AVCaptureDevice(activeCamera)获取它的活动AVCaptureDeviceFormat。如果格式的videoMaxZoomFactor值大于1.0,则捕捉设备支持缩放功能。
(2)要确定最大允许缩放因子,找到活动格式videoMaxZoomFactor的MIN值和4.0f。4.0f这个值是随意的,不过需要定义一个合理的缩放范围。通常我们不希望将内容缩放至videoMaxZoomFactor大小,因为太大就不实用了,不过在计算最大值时要时刻留意该值。如果试图设置缩放因子超过允许的最大值,就会出现异常。
(3)当用户移动图7-1中的缩放条时,就会调用setZoomValue:方法。滑动条的范围从0.0f到1.0f。
(4)如果捕捉设备当前没有将视频缩放,需要锁定设备进行配置。这一配置与其他的类似,如果在修改配置前没有进行锁定则会抛出一个异常。
(5)应用程序提供的缩放范围是1x到4x。这一增长是指数形式的,所以要提供范围线性增长的感觉,需要通过计算最大缩放因子的zoomValue次幂(0到1)来计算zoomFactor值。计算后,设置捕捉设备的videoZoomFactor属性的值。
(6)当完成配置后,结束设备锁定。

运行应用程序并移动滑动条,可以看到缩放级别不断在捕捉照片和视频的预览层中反映出来。随意调整maxZoomFactor方法的返回上限来查看设置更高缩放级别的效果。

videoZoomFactor属性会立即调整缩放级别。这对一些连续类型的控制比较实用,比如滑动条;不过,我们还希望在一个时间段内将缩放数值从一个值逐渐调整为另一个值。如果希望创建一个飞梭摇杆类型的控制,这个方法就非常适合。让我们为这些按钮添加一些功能来实现按时间调整缩放级别,如代码清单7-3所示。

代码清单7-3 Zoom Ramping

- (void)rampZoomToValue:(CGFloat)zoomValue {                                // 1
    CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);
    NSError *error;
    if ([self.activeCamera lockForConfiguration:&error]) {
        [self.activeCamera rampToVideoZoomFactor:zoomFactor                 // 2
                                        withRate:THZoomRate];
        [self.activeCamera unlockForConfiguration];
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
}

- (void)cancelZoom {                                                        // 3
    NSError *error;
    if ([self.activeCamera lockForConfiguration:&error]) {
        [self.activeCamera cancelVideoZoomRamp];                           
        [self.activeCamera unlockForConfiguration];
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
}

(1)在用户界面中按下加号或减号按钮就会调用rampZoomToValue:方法。如果用户按下减号(-)按钮,传递到方法中的值为0.0f,如果用户按下加号(+)按钮,值则为1.0f。就像之前做的那样,基于zoomValue参数计算一个正确的zoomFactor值。
(2)调用rampToVideoZoomFactor:withRate:方法并传递计算的zoomFactor和常量THZoomRate,即等于1.0f。 这个方法的效果是每秒增加缩放因子一倍。相应的速率值如何设置取决于开发者自己,不过通常都是在1.0f到3.0f范围之内,可以提供一个比较舒服的缩放控制效果。
(3)当用户在按钮上发起一个touch up inside动作,会调用cancelZoom方法。这会取消当前缩放进程并设置zoomFactor作为当前的状态。与所有设备配置变化一样,都需要先锁定设备,再应用期望的变更,完成后解除锁定。

再次运行应用程序并尝试这一新功能。会发现加号和减号按钮已经加入了期望的功能并平滑地对视频进行缩放;不过注意一个问题, 即用户界面中的缩放条没有与当前缩放级别同步变化。让我们按照代码清单7_4解决这个问题。

代码清单7-4添加缩放状态监听器

- (BOOL)setupSessionInputs:(NSError **)error {
    BOOL success = [super setupSessionInputs:error];                        // 1
    if (success) {
        [self.activeCamera addObserver:self
                            forKeyPath:@"videoZoomFactor"
                               options:0
                               context:&THRampingVideoZoomFactorContext];
        [self.activeCamera addObserver:self
                            forKeyPath:@"rampingVideoZoom"
                               options:0
                               context:&THRampingVideoZoomContext];

    }
    return success;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    if (context == &THRampingVideoZoomContext) {
        [self updateZoomingDelegate];                                       // 2
    } else if (context == &THRampingVideoZoomFactorContext) {
        if (self.activeCamera.isRampingVideoZoom) {
            [self updateZoomingDelegate];                                   // 3
        }
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

- (void)updateZoomingDelegate {
    CGFloat curZoomFactor = self.activeCamera.videoZoomFactor;
    CGFloat maxZoomFactor = [self maxZoomFactor];
    CGFloat value = log(curZoomFactor) / log(maxZoomFactor);                // 4
    [self.zoomingDelegate rampedZoomToValue:value];                         // 5
}

(1)重写超类的setupSessionInputs:方法的实现,这样就可以在捕捉会话配置好之后访问activeCamera。添加self作为捕捉设备videoZoomFactor和rampingVideoZoom两个关键路径的监听器。
(2)如果上下文为&THRampingVideoZoomContext,则调用updateZoomingDelegate方法。当缩放开始时监听方法开始出现,当缩放结束时再次出现。
(3)如果上下文为&THRampingVideoZoomFactorContext并且有正在运行的视频缩放动作,则再次调用updateZoomingDelegate方法。
(4)将当前缩放级别转换为滑动条的0到1比例,对当前videoZoomFactor值取对数,除以maxZoomFactor值取对数。
(5)最后将结果赋给zoomingDelegate,这样用户界面就可以相应地进行更新了。现在再次运行该应用程序,可以看到当与飞梭摇杆控件进行交互时,滑动条的值也相应地更新了。

AV Foundation新 的缩放功能是平台的一大补充,对使用捕捉连接的videoScaleAndCropFactor提供了极大帮助。直接对AVCaptureDevice 设置videoZoomFactor说明现在可以使用这个缩放来控制一切了。

7.2 人脸检测

如果你用过iOS内置的相机应用,会注意到这样一个功能, 视图中有新的人脸进入时会自动建立相应的焦点。一个黄色的矩形框会显示在新检测到的人脸位置,并以矩形的中点完成自动对焦。这是一个有用的功能,因为它不需要手动操作就可以简捷地完成对焦拍摄。幸运的是,使用AV Foundation的实时人脸检测功能可在应用程序中实现相同的功能。苹果公司首次向iOS开发者提供人脸检测功能是在Core Image框架中给出的。Core Image框架定义了CIDetector和CIFaceFeature两个对象,它们使用起来非常简单但提供了非常强大的人脸检测功能。不过这些方法并没有针对实时性进行优化,导致在现代摄像头和视频应用程 序要求的帧率之下很难应用。在iOS6版本中,一个新的硬件加速特性被直接加入到AVFoundation中,这次支持对10个人脸进行实时检测。通过一个特定的AVCaptureOutput类型AVCaptureMetadataOutput可以实现上述功能。它的输出同之前见过的类似,不过不同于输出一个静态图片或QuickTime影片,它输出的是元数据。这个元数据来自于一个AVMetadata-Object抽象类的形式,该类定义了用来处理多种元数据类型的接口。当使用人脸检测时,会输出一个具体的子类类型AVMetadataFaceObject。

AVMetadataFaceObject实例定义了多个用于描述被检测到人脸的属性,最重要的一个属性就是人脸的边界(bounds)。它是一个设备标量坐标格式的CGRect。第6章中介绍过设备标量坐标系的范围是摄像头原始朝向从左上角的(0,0)到右下角的(1,1)。

除了边界(bounds),AVMetadataFaceObject示例还给出了用于定义检测人脸斜倾角和偏转角的参数。斜倾角(roll angle)表示人的头部向肩膀方向的侧倾角度,偏转角(yaw angle)表示人脸绕y轴旋转的角度。

让我们创建一个应用程序来看具体是如何使用这些功能的。可在Chapter 7目录中找到一个名为FaceKamera_Starter的示例项目。我们从创建应用程序的摄像头控制器类开始,如代码清单7-5所示。

代码清单7-5 THCameraController 接口

#import <AVFoundation/ AVFoundation. h>
#import "THBa seCameraController.h"

@protocol THFaceDetectionDelegate <NsObject>
- (void)didDetectFaces:(NSArray *) faces;
@end

@interface THCameraController : THBaseCameraController

@property (weak, nonatomic) id <THFaceDetectionDelegate> faceDetectionDelegate;

@end

与本章中的其他类一样, 该类扩展于THBaseCameraController,THBaseCameraController定义了核心的摄像头操作方法。我们在该接口中定义一个新的协议THFaceDetectionDelegate,并在THCameraController类中为它定义一个属性。 这一委托会 在摄像头捕捉新的元数据时得到通知。转到类的具体实现代码中,开始具体添加这些功能,如代码清单7-6所示。

代码清单7-6 THCameraController 的实现

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

@interface THCameraController ()
@property (strong, nonatomic) AVCaptureMetadataOutput *metadataOutput;      // 1
@end

@implementation THCameraController

- (BOOL)setupSessionOutputs:(NSError **)error {

    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];           // 2

    if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession addOutput:self.metadataOutput];

        NSArray *metadataObjectTypes = @[AVMetadataObjectTypeFace];         // 3
        self.metadataOutput.metadataObjectTypes = metadataObjectTypes;

        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        [self.metadataOutput setMetadataObjectsDelegate:self                // 4
                                                  queue:mainQueue];

        return YES;

    } else {                                                                // 5
        if (error) {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey:
                                           @"Failed to still image output."};
            *error = [NSError errorWithDomain:THCameraErrorDomain
                                         code:THCameraErrorFailedToAddOutput
                                     userInfo:userInfo];
        }
        return NO;
    }
}

@end

(1)首先在类扩展中创建一个属性用来保存这个类使用的AVCaptureMetadataOutput实例。
(2)完全重写setupSessionOutputs:方法,因为超类中关于这个方法的实现在我们的示例中是不需要的。取而代之的是创建一个新的AVCaptureMetadataOutput示例并将 它添加为捕捉会话的输出。
(3)当配置AVCaptureMetadataOutput对象时,重要的一点 是要通过设置metadataObjectTypes属性指定对象输出的元数据类型。限制检测到的元数据类型集合的做法是一种优化处理方法,可以减少我们实际感兴趣的对象数量。AV Foundation支持多种元数据类型,但是由于当前我们只对人脸元数据感兴趣,所以定义一个NSArray用于保存单独的带有AVMetadataObjectTypeFace常量值的元素。
(4)当有新的元数据被检测到时,AVCaptureMetadataOutput需要 有一个委托对象可以被调用。我们标记了self作为委托并在可能出现的回调中定义了-系列调度队列。也可以使用自定义系列的调度队列,不过由于人脸检测用到硬件加速,而且许多重要任务都在主线程中执行,所以需要为这个参数指定主队列。
(5)最后,如果输出没有被添加,需要使用适当的NSError填充NSError并返回NO.

如果现在编译项目,会看到编译器告警显示self不是一个有效的委托。这是因为它未遵循AVCaptureMetadataOutputObjectsDelegatet协议。在新的元数据被检测并通知控制器之前,需要应用这个协议并实现captureOutput:didOutputMetadataObjects:fromConnection:方法。让我们添加一些内容到THCameraController类中来解决这一问题, 如代码清单7-7所示。

代码清单7-7接受AVCaptureMetadataOutputObjectsDelegate协议

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

@interface THCameraController () <AVCaptureMetadataOutputObjectsDelegate>   // 1
...
@end

@implementation THCameraController

- (BOOL)setupSessionOutputs:(NSError **)error {
    ...
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection {

    for (AVMetadataFaceObject *face in metadataObjects) {                   // 2
        NSLog(@"Face detected with ID: %li", (long)face.faceID);
        NSLog(@"Face bounds: %@", NSStringFromCGRect(face.bounds));
    }

    [self.faceDetectionDelegate didDetectFaces:metadataObjects];            // 3
}

@end

(1)令类扩展接受AVCaptureMetadataOutputObjectsDelegate协议。
(2)在协议的方法实现中,添加一些临时的调试代码来检查检测是否如期出现。遍历每一个检测到的人脸,for循环输出它们的faceID和bounds。当确定配置完成后就可以移除这些调试代码了。
(3)最后调用委托对象的didDetectFaces:方法,传递给它由AVCaptureMetadataOutput示例输出的AVMetadataObject实例数组。

运行该应用程序。当有人脸进入到摄像头区域时,你会看到调试信息被发送到控制台。记住,边界数据是以设备坐标系给出的。稍后将讨论如何将这些数据转换为更有用的坐标空间。

创建人脸检测委托

本书中的大部分示例代码都遵循这样的原则,即尽量不对用户界面层做过多干涉。不过在添加人脸检测功能时我们需要使用一些UIKit和ICore Animation代码,目的是为了更好地了解元数据的用法。本应用程序中实现了THFaceDetectionDelegate协议的对象是THPlayerView类。让我们看一下代码清单7-8给出的接口。

代码清单7-8 THPreviewView 接口

#import <AVFoundation/AVFoundation.h>
#import "THFaceDetectionDelegate.h"

@interface THPreviewView : UIView <THFaceDetectionDelegate>

@property (strong, nonatomic) AVCaptureSession *session;

@end

这个类的接口很简单,一个有趣的地方是它实现了THFaceDetectionDelegate协议。这个类将成为元数据的目标并用于提供可视化呈现方式。

转到具体实现开始创建这个类,视图实现包含了大量代码,所以我们先从基础类结构开始分块创建它,如代码清单7-9所示。

代码清单7-9 THPreviewView 的实现

#import "THPreviewView.h"

@interface THPreviewView ()                                                 // 1
@property (strong, nonatomic) CALayer *overlayLayer;
@property (strong, nonatomic) NSMutableDictionary *faceLayers;
@property (nonatomic, readonly) AVCaptureVideoPreviewLayer *previewLayer;
@end

@implementation THPreviewView

+ (Class)layerClass {                                                       // 2
    return [AVCaptureVideoPreviewLayer class];
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupView];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        [self setupView];
    }
    return self;
}

- (void)setupView {
    //
}

- (AVCaptureSession*)session {
    return self.previewLayer.session;
}

- (void)setSession:(AVCaptureSession *)session {                            // 3
    self.previewLayer.session = session;
}

- (AVCaptureVideoPreviewLayer *)previewLayer {
    return (AVCaptureVideoPreviewLayer *)self.layer;
}

- (void)didDetectFaces:(NSArray *)faces {
    //
}

@end

(1)创建一个类扩展并定义类所使用的内部属性。
(2)同上一章一样, 重写layerClass方法并返回一个AVCaptureVideoPreviewLayer实例。 这一技巧会 在新实例创建时自动将AVCaptureVideoPreviewLayer设为该类的备用层。
(3)要建立AVCaptureSession和AVCapturePreviewLayer之间的关联,需要重写setSession:方法并设置AVCaptureSession实例为预览层的session属性。

创建好基本的类结构之后,继续完成setupView方法的实现,如代码清单7-10所示。

代码清单7-10 setupView方法的实现

@implementation THPreviewView

...

- (void)setupView {
    self.faceLayers = [NSMutableDictionary dictionary];                     // 1
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    self.overlayLayer = [CALayer layer];                                    // 2
    self.overlayLayer.frame = self.bounds;
    self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);
    [self.previewLayer addSublayer:self.overlayLayer];
}

static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) {    // 3
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / eyePosition;
    return transform;
}

@end

(1)初始化faceL ayers属性,赋给它一个NSMutableDictionary来保存 对应检测人脸的层实例。此外,设置videoGravity为AVLayerVideoGravityResizeAspectFil使其填满整个预览层的边界范围。可以在第6章中查找有关视频gravity值的介绍。
(2)初始化overlayLayer属性,赋给它一个新的CALayer实例并设置其边界等于视图的边界,这样它就可以填满整个预览层。设置层的sublayerTransform属性 为CATransform3D,可以对所有子层应用视角转换。
(3)创建一个返回CATransform3D类型的函数。它是CoreAnimation中所使用的transformation matrix类型,用于进行缩放和旋转等转换。设置m34元素让开发者可以应用视角 转换,即让子层绕Y轴旋转。

如果你之前没有用过Core Animation框架,上面的代码可能看起来比较难懂,不过不必担心,稍后就会看到这一配置的效果了。要学习Core Animation框架,推荐阅读NickLockwood撰写的iOSCore Animation (2014, Boston: Addison-Wesley)一书。

AVCaptureMetadataOutput对象捕捉的元数据位于设备空间,要使用这一元数据, 首先需要将该数据转换到视图坐标系空间。幸运的是这一变换很容易,因为AVCaptureVideoPreviewLayer提供了一个方法可以完成这个艰难的任务。如代码清单7-11所示。

代码清单7-11元数据转换

@implementation THPreviewView

...

- (void)didDetectFaces:(NSArray *)faces {
    //
}

- (NSArray *)transformedFacesFromFaces:(NSArray *)faces {                   // 2
    NSMutableArray *transformedFaces = [NSMutableArray array];
    for (AVMetadataObject *face in faces) {
        AVMetadataObject *transformedFace =                                 // 3
            [self.previewLayer transformedMetadataObjectForMetadataObject:face];
        [transformedFaces addObject:transformedFace];
    }
    return transformedFaces;
}

...

@end

(1)创建一个本地NSArray来保存转换后的人脸数据。
(2)创建一个新方法,将设备坐标空间的人脸对象转换为视图空间对象集合。
(3)遍历传入方法的人脸集合,在for循环中调用预览层的transformedMetadataObjectForMetadataObject:方法,并将它添加到transformedFaces数组。

现在我们有一个AVMetadataFaceObject实例组成的集合,其中具有创建用户界面所需的坐标点。让我们继续学习预览视图的实现,如代码清单7-12所示。

代码清单7-12将检测到的人脸进行可视化

@implementation THPreviewView

...

- (void)didDetectFaces:(NSArray *)faces {

    NSArray *transformedFaces = [self transformedFacesFromFaces:faces];

    NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];      // 1

    for (AVMetadataFaceObject *face in transformedFaces) {

        NSNumber *faceID = @(face.faceID);                                  // 2
        [lostFaces removeObject:faceID];

        CALayer *layer = [self.faceLayers objectForKey:faceID];             // 3
        if (!layer) {
            // no layer for faceID, create new face layer
            layer = [self makeFaceLayer];                                   // 4
            [self.overlayLayer addSublayer:layer];
            self.faceLayers[faceID] = layer;
        }

        layer.transform = CATransform3DIdentity;                            // 6
        layer.frame = face.bounds;
    }

    for (NSNumber *faceID in lostFaces) {                                   // 7
        CALayer *layer = [self.faceLayers objectForKey:faceID];
        [layer removeFromSuperlayer];
        [self.faceLayers removeObjectForKey:faceID];
    }

}

- (CALayer *)makeFaceLayer {                                                // 5
    CALayer *layer = [CALayer layer];
    layer.borderWidth = 5.0f;
    layer.borderColor =
        [UIColor colorWithRed:0.188 green:0.517 blue:0.877 alpha:1.000].CGColor;
    return layer;
}

...

@end

(1)创建一个保存在faceLayers字典中键值的可变副本。这个数组用于确定哪些人脸移出了视图并将其对应的图层移出用户界面。
(2)遍历每个转换的人脸对象并捕捉其关联的faceID,这个属性唯一标识一个 检测到的人脸。将对象从lostFaces数组中移除,在这个方法最后面它所对应的图层不会从用户界面移出。
(3)在faceLayers字典中查找CALayer实例当前的faceID值。这一字典作为CALayer对象的一“个临时缓存。
(4)如果对于给定的faceID没有找到对应的层,调用makeFaceLayer方法创建一个新的人脸图层并将其添加到overlayLayer。最后,将它添加到字典中就可以被之前didDetectFaces:方法的调用重新使用了。
(5) makeFaceLayer方法创建 了一个新的CALayer,带有5个像素的浅蓝色加粗边框,并将这个图层返回到调用函数。
(6)设置图层的transform属性为CATransform3DIdentity。标识变换是图层的默认变换,本质的意思就是不可变状态。这会重新设置我们之前刚刚应用的所有变换。还需要基于人脸对象的bounds属性设置图层的框架。
(7)最后,遍历lostFaces数组中剩下的人脸ID集合并将它们从上一图层和faceLayers字典中移出。.

现在我们准备运行应用程序实际看一下这个功能的效果。应用程序默认使用前置摄像头,所以我们从摄像头中会看到在你的脸周围出现一个蓝色的方框,并实时跟踪脸部的移动。这很酷吧,我们还没有使用AVMetadataFaceObject提供的其他相关数据。我们已经讨论过人脸对象包含了斜倾角(roll angle)和偏转角(yaw angle)。让我们看一下如何在用户界面中对其进行描述,如代码清单7-13所示。

代码清单7-13可视化斜倾角和偏转 角

@implementation THPreviewView

...

- (void)didDetectFaces:(NSArray *)faces {

    NSArray *transformedFaces = [self transformedFacesFromFaces:faces];

    NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];      // 1

    for (AVMetadataFaceObject *face in transformedFaces) {

        NSNumber *faceID = @(face.faceID);                                  // 2
        [lostFaces removeObject:faceID];

        CALayer *layer = [self.faceLayers objectForKey:faceID];             // 3
        if (!layer) {
            // no layer for faceID, create new face layer
            layer = [self makeFaceLayer];                                   // 4
            [self.overlayLayer addSublayer:layer];
            self.faceLayers[faceID] = layer;
        }

        layer.transform = CATransform3DIdentity;                            // 1
        layer.frame = face.bounds;

        if (face.hasRollAngle) {
            CATransform3D t = [self transformForRollAngle:face.rollAngle];  // 2
            layer.transform = CATransform3DConcat(layer.transform, t);
        }

        if (face.hasYawAngle) {
            CATransform3D t = [self transformForYawAngle:face.yawAngle];    // 4
            layer.transform = CATransform3DConcat(layer.transform, t);
        }
    }

    for (NSNumber *faceID in lostFaces) {                                   // 6
        CALayer *layer = [self.faceLayers objectForKey:faceID];
        [layer removeFromSuperlayer];
        [self.faceLayers removeObjectForKey:faceID];
    }

}

// Rotate around Z-axis
- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees {        // 3
    CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}

// Rotate around Y-axis
- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees {          // 5
    CGFloat yawAngleInRadians = THDegreesToRadians(yawAngleInDegrees);

    CATransform3D yawTransform =
        CATransform3DMakeRotation(yawAngleInRadians, 0.0f, -1.0f, 0.0f);

    return CATransform3DConcat(yawTransform, [self orientationTransform]);
}

- (CATransform3D)orientationTransform {                                     // 6
    CGFloat angle = 0.0;
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationPortraitUpsideDown:
            angle = M_PI;
            break;
        case UIDeviceOrientationLandscapeRight:
            angle = -M_PI / 2.0f;
            break;
        case UIDeviceOrientationLandscapeLeft:
            angle = M_PI / 2.0f;
            break;
        default: // as UIDeviceOrientationPortrait
            angle = 0.0;
            break;
    }
    return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
}

static CGFloat THDegreesToRadians(CGFloat degrees) {
    return degrees * M_PI / 180;
}

...

@end

(1)对每个人脸图层,先将它的transform属 性设置为CATransform3DIdentity。这样就可以重新设置之前应用过的变换。
(2)通过检查hasRollAngle属性判断人脸对象是否具有有效的斜倾角。如果hasRollAngle返回NO,在获取对象的rollAngle属性将会产生异常。如果人脸对象具有rollAngle,则获取相应的CATransform3D,将它与标识变换关联在一起, 并设置图层的transform属性。
(3)从人脸对象得到的rollAngle单位是度,不过我们需要使用Core Animation的转换函数将它转换为所需的弧度值。将转换结果赋给CATransform3DMakeRotation函数,x、y、z轴的参数分别为0、0、1。这样会得到一个绕Z轴的斜倾角旋转转换。
(4)与计算rollAngle时 我们所做的一样,首先需要询问人脸对象是否具有偏转角。如果有,则获取这个值并计算相应的转换,将它与图层现存的转换串联在一起,并将它设置为图层的transform属性。
(5)构建一个关于偏转角的转换与创建斜倾角的方法类似。首先将角度转换为弧度并创建一个旋转转换,不过这次我们是绕Y轴旋转。由于overlayLayer需 要应用sublayerTransform,所以图层会投影到Z轴,人脸从一侧移动到另一侧时就会出现一个3D效果。
(6)应用程序的用户界面固定为垂直方向,不过我们需要为设备方向计算一个相应的旋转变换。如果不这样做会导致人脸图层的偏转效果不正确。这一转换会同其他变换关联。

再次运行应用程序,现在将头左右摆动,会看到矩形框发生旋转并跟踪着目标。同样,当我们将头转到肩膀后面,会看到矩形区域跟踪这一偏转并映射到3D空间。

现在我们已经深入理解了如何使用AV Foundation进行人脸检测,我们才刚刚创建了一个比较粗糙的有关人脸识别的用户界面。我希望像帽子、眼睛和胡须都出现在脑袋上。结合CoreAnimation图层和视频图片是完全可以捕捉到静态图片的,不过这需要用到非常重要的Quartz框架,它不在本书的讨论范围之内。我建议有兴趣的朋友可以从Apple DeveloperConnection网站找到Apple's SquareCam示例,或从WWDC 2013中找到StacheCam示例来学习如何实现这一结合。

7.3 机器可读代码识别

AV Foundation的一个重要新功能是可以识别机器可读的代码,这其实就是条码扫描的官方叫法。AV Foundation框架定义了对多种条码符号进行实时识别的方法,前置及后置摄像头都可以,并且只要能运行iOS 7和iOS 8的设备都支持这个功能。在学习这-技术的详细内容前,我们先来看一下框架所支持的几种不同的条码类型。

7.3.1 一维一条码

一维条码是我们所知的最流行的条码类型。大量用于商店、生产制造业和零售业,在大多数商品控制系统中都扮演着举足轻重的角色。AV Foundation支持的一维条码如表7-1所示。


AV Foundation还支持3种二维码。QR码主要用于移动营销,不过我们会发现在移动领域中QR码还有许多创造性的用法。Aztec码广泛用于航天领域内登机牌。PDF417通常用于商品运输应用程序。表7-2给出了这3种条码的示例图。


7.3.2 创建条码扫描器

让我们创建一个示例应用程序来使用这些功能。在Chapter 7目录中可以找到一个名为CodeKamera_Starter的示例项目。我们从创建摄像头控制器对象开始,如代码清单7-14所示。

代码清单7-14 THCameraController 接口

#import <AVFoundat ion/ AVFoundation.h>
#import "THBaseCameraController .h"

@protocol THCodeDetectionDelegate <NSObject>
- (void)didDetectCodes:(NSArray *) codes;
@end

@interface THCameraController:THBaseCameraController

@property (weak, nonatomic) id <THCodeDetectionDelegate> codeDetectionDelegate;

@end

THCameraController的接口与我们为FaceKamera应用程序创建的接口几乎一样;唯一的 不同是定义了一个新的委托协议THCodeDetectionDelegate。这个委托只定义了-一个didDetectCodes:方法,当有新的条码被识别时会调用该方法。让我们转到它的实现中开始编写这个功能,如代码清单7-15所示。

代码清单7-15设置摄像头

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

@interface THCameraController () <AVCaptureMetadataOutputObjectsDelegate>   // 1
@property (strong, nonatomic) AVCaptureMetadataOutput *metadataOutput;
@end

@implementation THCameraController

- (NSString *)sessionPreset {                                               // 2
    return AVCaptureSessionPreset640x480;
}

- (BOOL)setupSessionInputs:(NSError *__autoreleasing *)error {
    BOOL success = [super setupSessionInputs:error];
    if (success) {
        if (self.activeCamera.autoFocusRangeRestrictionSupported) {         // 3

            if ([self.activeCamera lockForConfiguration:error]) {

                self.activeCamera.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;

                [self.activeCamera unlockForConfiguration];
            }
        }
    }
    return success;
}

...

@end

(1)首先创建一个类扩展来保存到AVCaptureMetadataOutput的引用。与我们在Face-Kamera应用程序中做的一样, 这个类需要接受AVCaptureMetadataOutputObjectsDelegate协议,这样才能在得到新元数据时收到通知。
(2)重写sessionPreset方法返回一个其他会话预设类型来使用。可以任意选择最适合应用程序的捕捉预设类型,不过苹果公司建议开发者使用最低合理解决方案以提高性能。
(3)捕捉设备的自动对焦功能通常在任何距离都可以进行扫描,这一功能也是我们在典型的相机或视频捕捉应用程序中所需要的。不过,iOS 7版本增加了一个新属性,允许我们使用一个范围约束对这个功能进行定制。我们扫描的大部分条码距离都不远,所以可以通过缩小扫描区域来提升识别成功率。记得检测是否可以支持该功能,如果可以,则设置autoFocusRangeRestriction属性为AVCaptureAutoFocusRangeRestrictionNear.

在完成捕捉设备输入配置后,让我们看一下捕捉会话输出的配置,如代码清单7-16所示。

代码清单7-16配置会话输出

@implementation THCameraController

...

- (BOOL)setupSessionOutputs:(NSError **)error {
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];

    if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession addOutput:self.metadataOutput];

        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        [self.metadataOutput setMetadataObjectsDelegate:self
                                                  queue:mainQueue];

        NSArray *types = @[AVMetadataObjectTypeQRCode,                      // 1
                           AVMetadataObjectTypeAztecCode,
                           AVMetadataObjectTypeUPCECode];

        self.metadataOutput.metadataObjectTypes = types;

    } else {
        NSDictionary *userInfo = @{NSLocalizedDescriptionKey:
                                       @"Failed to still image output."};
        *error = [NSError errorWithDomain:THCameraErrorDomain
                                     code:THCameraErrorFailedToAddOutput
                                 userInfo:userInfo];
        return NO;
    }

    return YES;
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {

    [self.codeDetectionDelegate didDetectCodes:metadataObjects];            // 2
}

@end

(1) setupSessionOutputs:方 法的实现与我们在FaceKamera应用程序中所用到的实现方法几乎一样。 唯一的不同是metadataObjectTypes 会被AVCaptureMetadataOutput对象使用。本例中我们指定感兴趣的扫描对象是QR码和Aztec码。
(2)在委托回调中调用THCodeDetectionDelegate,并传递由我们识别到的元数据对象组成的数组。

处理条码时出现的AVMetadataCaptureOutput对象是AVMetadataMachineReadableCodeObject实例。这个对象定义了一个stringValue属性用于提供条码实际数据值,还定义了两个属性用来表示条码的几何特征。bounds属 性提供了识别码的按坐标轴对齐的矩形边界,corners属性提供角点字典表示的NSArray。后一个属性更实用, 因为使用它可以让我们构建一个 与条码的角点坐标紧密对齐的Bezier路径。

7.3.3 创建条码识别的委托

本应用程序中的THCodeDetectionDelegate是THPreviewView.让我们看它是如何实现的,如代码清单7-17所示。

代码清单7-17 THPreviewView 接口

#import <AVFoundation/AVFoundation.h>
#import "THCodeDetectionDelegate.h"

@interface THPreviewView : UIView <THCodeDetectionDelegate>

@property (strong, nonatomic) AVCaptureSession *session;

@end

这个接口看起来很熟悉,唯一需要注意的是它接受了THCodeDetectionDelegate协议,使它可以成为元数据的目标。让我们看一下代码清单7-18给出的实现过程。与我们在FaceKamera应用程序中讨论的预览视图一样,我们分节来学习,先从基础类结构开始。

代码清单7-18 THPreviewView 的实现

#import "THPreviewView.h"

@interface THPreviewView ()                                         // 1
@property (strong, nonatomic) NSMutableDictionary *codeLayers;
@end

@implementation THPreviewView

+ (Class)layerClass {                                               // 2
    return [AVCaptureVideoPreviewLayer class];
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupView];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        [self setupView];
    }
    return self;
}

- (void)setupView {                                                 // 3
    _codeLayers = [NSMutableDictionary dictionary];
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
}

- (AVCaptureSession*)session {
    return self.previewLayer.session;
}

- (void)setSession:(AVCaptureSession *)session {                    // 4
    self.previewLayer.session = session;
}

- (AVCaptureVideoPreviewLayer *)previewLayer {
    return (AVCaptureVideoPreviewLayer *)self.layer;
}

- (void)didDetectCodes:(NSArray *)codes {
}

@end

(1)创建一个类扩展并定义类所使用的内部属性。
(2)重写layerClass方法并返回一个AVCaptureVideoPreviewLayer实例,就像上一节中做的一样。重写该方法可以让视图的备用层成为AVCaptureVideoPreviewLayer的实例。
(3)用NSMutableDictionary实例来初始化codelayers属性。它用来保存一组表示识别编码的几何信息的图层。还需要设置图层的videoGravity属性为AVLayerVideoGravityResizeAspect,保证宽高比在边界范围之内。
(4)要建立AVCaptureSession和AVCapturePreviewLayer之间的关联,可重写setSession:方法并设置AVCaptureSession实例作为预览层的session属性。

基础的类结构创建完成后,我们继续实现didDetectCodes:方法,如代码清单7-19所示。第一步需要做的是将设备坐标系中的元数据转换为视图坐标系的数据。

代码清单7-19元数据转换

@implementation THPreviewView

...

- (void)didDetectCodes:(NSArray *)codes {
    NSArray *transformedCodes = [self transformedCodesFromCodes:codes];     // 1
}

- (NSArray *)transformedCodesFromCodes:(NSArray *)codes {                   // 2
    NSMutableArray *transformedCodes = [NSMutableArray array];
    for (AVMetadataObject *code in codes) {
        AVMetadataObject *transformedCode =
        [self.previewLayer transformedMetadataObjectForMetadataObject:code];
        [transformedCodes addObject:transformedCode];
    }
    return transformedCodes;
}

@end

(1)基于传递给委托方法的AVMetadataMachineReadableCodeObject实例,创建一个本地NSArray来保存转换完成的元数据对象。
(2)创建一个新的方法,将设备坐标空间元数据对象转换为视图坐标空间对象。这个方法会遍历传进方法中的元数据对象集合,对每一对象调用预览图层的transformedMetadata-ObjectForMetadataObject:方法,并将它添加到transformedCodes数组。

现在我们有一组AVMetadataMachineReadableCodeObject实例,它们都具有组成用户界面的有效坐标。让我们继续实现这个方法,如代码清单7-20所示。

代码清单7-20创建图层

@implementation THPreviewView

...

- (void)didDetectCodes:(NSArray *)codes {

    NSArray *transformedCodes = [self transformedCodesFromCodes:codes];

    NSMutableArray *lostCodes = [self.codeLayers.allKeys mutableCopy];// 1

    for (AVMetadataMachineReadableCodeObject *code in transformedCodes) {

        NSString *stringValue = code.stringValue;// 2
        if (stringValue) {
            [lostCodes removeObject:stringValue];
        } else {
            continue;
        }

        NSArray *layers = self.codeLayers[stringValue];// 3

        if (!layers) {
            // no layers for stringValue, create new code layers
            layers = @[[self makeBoundsLayer], [self makeCornersLayer]];

            self.codeLayers[stringValue] = layers;
            [self.previewLayer addSublayer:layers[0]];
            [self.previewLayer addSublayer:layers[1]];
        }

        CAShapeLayer *boundsLayer  = layers[0];// 4
        boundsLayer.path  = [self bezierPathForBounds:code.bounds].CGPath;

        NSLog(@"String: %@", stringValue);// 5
    }

    for (NSString *stringValue in lostCodes) {// 6
        for (CALayer *layer in self.codeLayers[stringValue]) {
            [layer removeFromSuperlayer];
        }
        [self.codeLayers removeObjectForKey:stringValue];
    }
}

- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
    return [UIBezierPath bezierPathWithRect:bounds];
}

- (CAShapeLayer *)makeBoundsLayer {
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor =
        [UIColor colorWithRed:0.95f green:0.75f blue:0.06f alpha:1.0f].CGColor;
    shapeLayer.fillColor = nil;
    shapeLayer.lineWidth = 4.0f;
    return shapeLayer;
}

- (CAShapeLayer *)makeCornersLayer {
    CAShapeLayer *cornersLayer = [CAShapeLayer layer];
    cornersLayer.lineWidth = 2.0f;
    cornersLayer.strokeColor =
        [UIColor colorWithRed:0.172 green:0.671 blue:0.428 alpha:1.000].CGColor;
    cornersLayer.fillColor =
        [UIColor colorWithRed:0.190 green:0.753 blue:0.489 alpha:0.500].CGColor;
    
    return cornersLayer;
}

@end

(1)从codeLayers字 典中创建键的可变副本。这个数组用来判断哪个图层应该在方法结尾处移除。
(2)查找代码的stringValue。如果返回一个有效的字符串对象,则将它从lostCodes数组中移除。如果stringValue为nil,则执行continue语句继续 下一个迭代,因为应该跳过不具有合法值的任何代码。
(3)为当前的stringValue查找已有的图层数组,如果没有对应的条目,则创建两个新的CAShapeLayer对象。CAShapeLayer是具 体化的CALayer子类,用于绘制Bezier路径。第一个对象绘制bounds矩形,另一个对象用于绘制之后构建的cormers路径。在字典中为这些图层添加一个条目,并将每个图层加到previewLayer中。
(4)对于边界图层,创建一个和对象的bounds关联的UIBezierPath。Core Animation只支持Quartz类型,所以我们获取UIBezierPath的CGPath属性, 会得到一个CGPathRef用来赋给图层的path属性。
(5)为达到应用程序的目的,我们将stringValue记录 到控制台。在实际应用程序中,开发者会以其他有意义的方式将这个值展示给用户。
(6)最后,遍历剩下的lostCodes条目,将每一个条 目的图层从previewLayer中移除,并将数组条目从字典中移除。

现在可以运行应用程序了,我们可以使用表7-2中的条码来测试这个功能。对于每个识别码,都会在屏幕上看到一个矩形方框。如果摄像头视图与条码垂直,矩形的边界就会对齐。不过当以一定 的角度进行扫描时就不能很好对齐。这时就会用到cormers属性了,大部分情况下,cormers属性都可以提供较好的资源来绘制代码的几何图形,因为它提供了包含代码角点 的字典。使用这些点很容易构建出与条码角落紧密对齐的Bezier路径。让我们看如何应用cormers属性,如代码清单7-21所示。

代码清单7-21使用 comers属性

@implementation THPreviewView

...

- (void)didDetectCodes:(NSArray *)codes {

    NSArray *transformedCodes = [self transformedCodesFromCodes:codes];

    NSMutableArray *lostCodes = [self.codeLayers.allKeys mutableCopy];

    for (AVMetadataMachineReadableCodeObject *code in transformedCodes) {

        NSString *stringValue = code.stringValue;
        if (stringValue) {
            [lostCodes removeObject:stringValue];
        } else {
            continue;
        }

        NSArray *layers = self.codeLayers[stringValue];

        if (!layers) {
            // no layers for stringValue, create new code layers
            layers = @[[self makeBoundsLayer], [self makeCornersLayer]];

            self.codeLayers[stringValue] = layers;
            [self.previewLayer addSublayer:layers[0]];
            [self.previewLayer addSublayer:layers[1]];
        }

        CAShapeLayer *boundsLayer  = layers[0];
        boundsLayer.path  = [self bezierPathForBounds:code.bounds].CGPath;

        CAShapeLayer *cornersLayer = layers[1];                             // 1
        cornersLayer.path = [self bezierPathForCorners:code.corners].CGPath;

        NSLog(@"String: %@", stringValue);
    }

    for (NSString *stringValue in lostCodes) {
        for (CALayer *layer in self.codeLayers[stringValue]) {
            [layer removeFromSuperlayer];
        }
        [self.codeLayers removeObjectForKey:stringValue];
    }
}

- (UIBezierPath *)bezierPathForCorners:(NSArray *)corners {
    UIBezierPath *path = [UIBezierPath bezierPath];
    for (int i = 0; i < corners.count; i++) {
        CGPoint point = [self pointForCorner:corners[i]];                   // 2
        if (i == 0) {                                                       // 4
            [path moveToPoint:point];
        } else {
            [path addLineToPoint:point];
        }
    }
    [path closePath];                                                       // 5
    return path;
}

- (CGPoint)pointForCorner:(NSDictionary *)corner {                          // 3
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)corner, &point);
    return point;
}

@end

(1)对于cormersLayer,基于元数据对象的cormers属性构建一个CGPath。
(2)在bezierPathForCormers:方法中,首先创建一个空的UIBezierPath。 遍历corners数组中的条目,为每个条目构建一个CGPoint。
(3)包含角点对象的字典具有x值的条目和y值的条目。开发者可将这些值取出来手动创建CGPoint值。不过Quartz框架为这个过程提供了一个便捷函数CGPointMakeWithDictionary-Representation,只需要给要填充的CGPoint结构传递字典和指针即可。
(4)基于这些点使用UIBezierPath方法构建Bezier路径。对第一个 点调用moveToPoint:方法开始绘制路径,对后面的点调用addLineToPoint:连到其余的点。
(5)最后,关闭路径并返回UIBezierPath实例给调用函数。

再次运行该应用程序,现在我们移动设备,可以看到图层紧密与条码的边角对齐了。

识别机器可读码的功能是AV Foundation框架非常受欢迎的功能之一,并且我们认为这个功能将会大量应用于应用程序的开发中。我们建议大家对其他几种类型的条码都进行实验,以更好地理解该功能。还可以参阅苹果公司的Tech Note TN2325,那里对使用这个功能给出了一个实用的建议和观点。

7.4 使用高帧率捕捉

在iOS 7版本引入的几个功能中,对于最新的iOS设备,最有趣的一个功能就是高帧率视频的捕捉。以高帧率(FPS)捕捉视频内容带来了很多好处。首先高FPS视频通常具有更逼真的效果和更好的清晰度,因为由于增加了瞬时采样率而使对于移动物体的捕捉更加精确。对于细节方面的加强和动作流畅度的提升非常明显,尤其是当录制快速移动的内容时更为明显,比如运动场景。另一个应用更广泛的优势在于可以支持高质量的慢动作视频效果。最新发布的iOS设备支持以60FPS的速率捕捉视频(iPhone 5s可以支持120FPS),这意味着可以使用播放速度一半的帧率还可以达到以30FPS速率进行播放,播放体验更加流畅。没有什么比加入慢 动作更好的了,现在这个功能已经可以被我们使用了。

高帧率捕捉是开发者在一些场景中希望使用的技术。幸运的是,苹果公司并没有单独推出这一个功能,而是通过框架为这个功能给出了强大的支撑。

●捕捉: 框架支持60FPS帧率捕捉720p视频,或在iPhone 5s中将帧率提高到120FPS,并带有视频稳定技术。此外还可以支持启用droppable P-frames的h.264特性,保证高帧率内容可以在旧设备上流畅播放。

●播放: AVPlayer 已经支持以多种播放帧率播放资源内容,不过AVPlayerltem给出了一个显著的音频处理过程的优化,即在降低播放率的时候如何控制音频内容的播放。AVPlayerltem现在带有一个audioTimePitchAlgorithm属性,可以为降速或加速播放率设置算法。查看AVPlayerltem的API文档了解可用的选项。

●编辑: 框架的编辑功能为在可变组合中执行缩放编辑提供全面支持。我们将在第8章开始讨论有关媒体资源组合及编辑的内容。

●导出: AV Foundation提供了保存原始帧率的功能,所以高FPS内容可以被导出或进行帧率转换,这样就可以保证所有内容都以标准的30FPS进行输出。

7.4.1 高帧率捕捉综述

在上一章中,我们学习了如何通过指定会话预设值来设置捕捉会话的服务质量,开发者可能希望找到一些新的预设来支持高帧率捕捉。但是,支持这个功能并没有这么简单。无法实际扩展这一预设机制,因为要满足所有的帧率和尺寸组合会导致预设参数暴增。所以这一功能通过在iOS 7版本加入的并行配置机制来实现。这个新方法并没有替代预设模型,而是在需要对内容进行更细致配置时给出的另一种配置会话的方法。

本章前面介绍了使用AVCaptureDeviceFormat来判断激活捕捉设备格式所支持的最大缩放因子。在视频缩放示例中,用到了设备的activeFormat,会根据选中的会话预设自动设置它。除了active格式外,还可以通过获取formats属性得到设备所有支持的格式。AVCaptureDeviceFormat实例具有videoSupportedFrameRateRanges属性,它包含了一个AVFrameRateRange对象数组,其中带有格式所支持的最小帧率、最大帧率和时长信息。使用高帧率捕捉的一个基本秘诀就是找到设备的最高质量格式,找到它相关的帧时长,之后手动设置捕捉设备的格式和帧时长。当我们借用一些示例来看会 更清楚一些,打开Chapter 7目录中的SlowKamera Starter项目,开始实现这个功能。

7.4.2 支持高帧率捕捉

要支持这一功能现在 看来有些麻烦,不过我们可以通过将这一功能封 装到AVCaptureDevice。上的分类来简化这一过程, 如代码清单7-22所示。我们先来创建这个分类。

代码清单7-22高帧率捕捉分类

#import <AVFoundation/AVFoundation.h>

@interface AVCaptureDevice (THAdditions)

- (BOOL)supportsHighFrameRateCapture;
- (BOOL)enableMaxFrameRateCapture:(NSError **)error;

@end

我们在AVCaptureDevice创建分类,添加两个方法:一个用来判断当前设备是否支持高帧率捕捉,另一个用来实现这个功能。下面看一下具体的实现。

在给出分类方法的实现前,首先需要在AVCaptureDevice+THAdditions.m文件中创建一个私有类THQualityOfService,它可以简化分类方法的实现,如代码清单7-23所示。

代码清单7-23私有类THQualityOfService

#import "AVCaptureDevice+THAdditions.h"
#import "THError.h"

@interface THQualityOfService : NSObject

@property(strong, nonatomic, readonly) AVCaptureDeviceFormat *format;
@property(strong, nonatomic, readonly) AVFrameRateRange *frameRateRange;
@property(nonatomic, readonly) BOOL isHighFrameRate;

+ (instancetype)qosWithFormat:(AVCaptureDeviceFormat *)format
               frameRateRange:(AVFrameRateRange *)frameRateRange;

- (BOOL)isHighFrameRate;

@end

@implementation THQualityOfService

+ (instancetype)qosWithFormat:(AVCaptureDeviceFormat *)format
               frameRateRange:(AVFrameRateRange *)frameRateRange {

    return [[self alloc] initWithFormat:format frameRateRange:frameRateRange];
}

- (instancetype)initWithFormat:(AVCaptureDeviceFormat *)format
                frameRateRange:(AVFrameRateRange *)frameRateRange {
    self = [super init];
    if (self) {
        _format = format;
        _frameRateRange = frameRateRange;
    }
    return self;
}

- (BOOL)isHighFrameRate {
    return self.frameRateRange.maxFrameRate > 30.0f;
}

@end

要启用高帧率捕捉,需要查看捕捉设备的可用格式,并找到所支持的最高AVCaptureDeviceFormat及其最高AVFrameRateRange。THQualityOfService用来保存这些值并简化公共分类方法的实现。让我们看一下第一个分类方法的实现,如代码清单7-24所示。

代码清单7-24确定高 FPS支持

@implementation AVCaptureDevice (THAdditions)

- (BOOL)supportsHighFrameRateCapture {
    if (![self hasMediaType:AVMediaTypeVideo]) {                            // 1
        return NO;
    }
    return [self findHighestQualityOfService].isHighFrameRate;              // 2
}

- (THQualityOfService *)findHighestQualityOfService {

    AVCaptureDeviceFormat *maxFormat = nil;
    AVFrameRateRange *maxFrameRateRange = nil;

    for (AVCaptureDeviceFormat *format in self.formats) {

        FourCharCode codecType =                                            // 3
            CMVideoFormatDescriptionGetCodecType(format.formatDescription);

        if (codecType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) { // 4

            NSArray *frameRateRanges = format.videoSupportedFrameRateRanges;

            for (AVFrameRateRange *range in frameRateRanges) {              
                if (range.maxFrameRate > maxFrameRateRange.maxFrameRate) {
                    maxFormat = format;
                    maxFrameRateRange = range;
                }
            }
        }
    }

    return [THQualityOfService qosWithFormat:maxFormat                      // 5
                              frameRateRange:maxFrameRateRange];

}

...

@end

(1)通过查看AVCaptureDevice实例是否具有AVMediaTypeVideo媒体类型值来判断它是不是视频设备。如果不是,则从该方法中返回NO。
(2)调用findHighestQualityOfService方 法来确定这个摄像头设备所支持的最大format和帧率。通过THQualityOfService对象 查看其是否支持高帧率捕捉并返回结果值。
(3)遍历所有捕捉设备的支持formats并对每一个元素从formatDescription获取相应的codecType。CMFormatDescriptionRef是一个Core Media的隐含类型,提供了格式对象的许多相关信息。这里只需要codeType值等于420YpCbCr8BiPlanarVideoRange的格式,筛选出视频格式。
(4)遍历由videoSupportedFrameRateRanges属性返回的format的AVFrameRateRange对象集。对于每一个,都判断其maxFrameRate是否大于当前的最大值。最终的目的是找到这个摄像头设备所提供的最高的format和帧率。
(5)最后,返回一个新的内部THQualityOfService的实例,捕捉所支持的最高format和帧率范围。

现在我们知道如何确定活动摄像头所支持的最高级别了,下面看一下如何实际使用这个功能,如代码清单7-25所示。

代码清单7-25高帧率捕捉

- (BOOL)enableMaxFrameRateCapture:(NSError **)error {

    THQualityOfService *qos = [self findHighestQualityOfService];

    if (!qos.isHighFrameRate) {                                             // 1
        if (error) {
            NSString *message = @"Device does not support high FPS capture";
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey : message};

            NSUInteger code = THCameraErrorHighFrameRateCaptureNotSupported;

            *error = [NSError errorWithDomain:THCameraErrorDomain
                                         code:code
                                     userInfo:userInfo];
        }
        return NO;
    }


    if ([self lockForConfiguration:error]) {                                // 2

        CMTime minFrameDuration = qos.frameRateRange.minFrameDuration;

        self.activeFormat = qos.format;                                     // 3
        self.activeVideoMinFrameDuration = minFrameDuration;                // 4
        self.activeVideoMaxFrameDuration = minFrameDuration;

        [self unlockForConfiguration];
        return YES;
    }
    return NO;
}

(1)首先找到设备支持的最高质量的服务。如果不支持高帧率捕捉,则生成一个错误指针并返回NO。
(2)在修改捕捉设备前,需要为配置操作锁住设备。
(3)将设备的activeFormat设置为检索到的AVCaptureDeviceFormat。
(4)将最小帧时长和最大帧时长设置为AVFrameRateRange定义的值。AV Foundation通常处理帧时长数据,使用CMTime实例而不是帧率。minFrameDuration值 为maxFrameRate的倒数值。比如帧率为60FPS,则duration为 1/60秒。

完成分类之后,让我们在应用程序的THCameraController类中使用它,如代码清单7-26所示。

代码清单7-26使用AVCaptureDevice+THAdditions

#import "THCameraController.h"
#import "AVCaptureDevice+THAdditions.h"

@implementation THCameraController

- (BOOL)cameraSupportsHighFrameRateCapture {
    return [self.activeCamera supportsHighFrameRateCapture];
}

- (BOOL)enableHighFrameRateCapture {
    NSError *error;
    BOOL enabled = [self.activeCamera enableMaxFrameRateCapture:&error];
    if (!enabled) {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
    return enabled;
}

@end

如果你有一个最新的设备,就可以实际看下这个功能。启动应用程序并录制几秒视频。当点击Stop按钮时,视频显示在一个带有帧率控件的播放器中。尝试选择不同的帧率选项来体验一下高帧率视频的效果。

高帧率捕捉已经成为广泛需求的功能了,从iOS 7开始开发者就可以自己对其进行处理。除了捕捉外,AV Foundation对于在各个方面使用高帧率内容都进行了完全的支持,它成为了我们创建捕捉应用程序新类时的一个非常有创造力的工具。

7.5 视频处理

在第6章,使用AVCaptureMovieFileOutput类捕 捉QuickTime影片,这个类定义了从内置摄像头捕捉视频数据的简单方法,不过它无法同视频数据进行交互,而交互又是许多场景所需要的功能。思考一下苹果产品Photo Booth的需求,它对视频流应用了实时的效果,或者思考一下Sphero公司的增强现实类应用程序Sharky the Beaver,它将交互元素与摄像头捕捉到的环境内容融合在一起。 这两个示例都需要对捕捉到的视频数据进行比AVCaptureMovieFileOutput更加底层的控制。当需要使用底层控制时,就会用到框架提供的最底层的视频捕捉输出AVCaptureVideoDataOutput。

AVCaptureVideoDataOutput是一个AVCaptureOutput子类,可以直接访问摄像头传感器捕捉到的视频帧。这是一个强大的功能,因为这样我们就完全控制了视频数据的格式、时间和元数据,可以按照需求操作视频内容。大部分情况下,处理过程都是使用OpenGL ES或CoreImage,不过有时Quartz也可以满足一些简单的处理要求。在我们即将创建的示例应用程序中,我们会整合OpenGL ES,因为它在视频数据处理中应用更为广泛,不过下一章将 介绍如何整合Core Image并学习其强大的筛选器集合。

注意:
AV Foundation为处理音频数据提供了一个底层捕捉输出AVCaptureAudioDataOutput。本章仅介绍AVCaptureVideoDataOutput 的用法,不过在下一章讨论AVAssetReader和AVAssetWriter时,会用到音频处理的这个兄弟类。

使用AVCaptureVideoDataOutput与本章前面使用AVCaptureMetadataOuput的方法类似,最明显的区别在于它们各自的委托回调。与输出AVMetadataObject实例不同,AVCaptureVideoDataOutput输出的对象需要通过AVCaptureVideoDataOutputSampleBufferDelegate协议包含视频数据。

AVCaptureVideoDataOutputSampleBufferDelegate定义了下面两个方法:

●captureOutput:didOutputSampleBuffer:fromConnection: 每当有一个新的视频帧写入时该方法就会被调用。数据会基于视频数据输出的videoSettings 属性进行解码或重新编码。

●captureOutput:didDropSampleBuffer:fromConnection: 每当一个迟到的视频帧被丢弃时就会调用该方法。通常是因为在didOutputSampleBuffer:调用中消耗了太多处理时间就会调用该方法。开发者应该尽量提高处理效率,否则将收不到缓存数据。

这两个方法中最重要的参数与ample buffer相关。sample buffer以CMSampleBuffer对象的形式提供,在第6章讨论AVCaptretilillmageOutput时曾简单介绍过。深入理解这个类型非常重要,所以让我们看一下这个对象的含义和作用。

7.6 学习 CMSampleBuffer

CMSampleBuffer是一个由Core Media框架提供的Core Foundation风格的对象,用于在媒体管道中传输数字样本。CMSampleBuffer的角色是将基础的样本数据进行封装并提供格式和时间信息,还会加上所有在转换和处理数据时用到的元数据。让我们先看一下CMSampleBuffer提供的样本数据吧。

7.6.1 样本数据

在使用AVCaptureVideoDataOutput时,sample buffer会包含一个CVPixelBuffer,它是一个带有单个视频帧原始像素数据的CoreVideo对象。下面的示例展示了如何直接操作CVPixelBuffer的内容为捕捉到的图片buffer应用一个灰度效果。

const int BYTES_PER_PIXEL = 4;
CMSampleBufferRef sampleBuffer = // obtained sample buffer

CVPixelBufferRef pixe1Buffer = CMSampleBufferGetImageBuffer(sampleBuffer) ;        // 1

CVPixelBufferLockBaseAddress(pixelBuffer, 0);                                      // 2

size_t bufferwidth = CVPixelBufferGetWidth(pixelBuffer) ;                          // 3
size_t bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
unsigned char *pixel = (unsigned char *) CVPixelBufferGetBaseAddress (pixelBuffer);// 4
unsigned char grayPixel;

for (int row = 0; row < bufferHeight; row++) {                                      //5
    for(int column - 0; column < bufferwidth; column++) {
        grayPixel - (pixel[0] + pixe1[1] + pixel[2]) / 3;
        pixel[0] = pixel[1] = pixel[2] = grayPixel;
        pixel += BYTES_PER_PIXEL;
    }
}

CVPixelBufferUnlockBaseAddress (pixelBuffer, 0);                                  // 6

// Process grayscale video frame

示例中的新内容很多,让我们具体来分析一下吧。

(1)首先使用CMSampleBufferGetImageBuffer函数从CMSampleBufferRef中获取基本的CVPixelBuffer。CVPixelBuffer在 主内存中保存像素数据,提供了操作内容的机会。

(2)在与CVPixelBuffer数据交互前,必须调用CVPixelBufferL ockBaseAddress获取-一个相应内存块的锁。

(3)使用CVPixelBufferGetWidth函 数和CVPixelBufferGetHeight函数确定像素buffer的宽和高,这样就可以迭代它的行和列。

(4) CVPixelBufferGetBaseAddress 函数可以得到像素buffer的基址指针。现在就可以在buffer中进行索引并迭代其数据。

(5)迭代buffer中像素的行和列,执行简单的RGB像素灰度平均。

(6)最后,调用CVPixelBufferUnlockBaseAddress函数释 放第2步中的锁。

现在,我们可以把buffer转换为CGImageRef或UIlmage,或执行任何所需的附加图像处理。我们建议还可以额外看一下CVPixelBuffer.h中的CVPixelBuffer函数。

7.6.2 格式描述

除了原始媒体样本本身之外,CMSampleBuffer还提供了以CMFormatDescription对象的形式来访问样本的格式信息。CMFormatDescription.h定义了大量函数用于访问媒体样本的更多细节。在头文件中带有CMFormatDecription前缀的函数一般可以用于所有的媒体类型,还有前缀为CMVideoFormatDescription和CMAudioFormatDescription的函数分别适用于获取视频和音频的细节。

让我们看一下使用CMFormatDescription来区别音频和视频数据的一个示例。

CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription);
if (mediaType == kCMMediaType_Video) {
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // Process the frame of video
} else if (mediaType == kCMMediaType_Audio) {
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    // Process audio samples
}

7.6.3 时间信息

CMSampleBuffer还定义了关于媒体样本的时间信息。可以分别使用CMSampleBufferGetPresentationTimeStamp函数和CMSampleBufferGetDecodeTimeStamp函数提取时间信息来得到原始的表示时间戳和解码时间戳。下一章讨 论AVAssetReader和AVAssetWriter时会更多地用到这些函数。

7.6.4 附加的元数据

Core Media还在CMAttachment.h中定义了一个CMAttachment形式的元数据协议。API提供了读取和写入底层元数据的基础架构,比如可交换图片文件格式(Exif)标签。比如,下面的示例中我们可以从一个给定的CMSampleBuffer中获取Exif元数据。

CFDictionaryRef exifAttachments = (CFDictionaryRef)CMGetAttachment(sampleBuffer,kCGImagePropertyExi fDictionary,NULL);

如果将这个字典数据打印到控制台,可以看到类似下例的输出信息。

{
    ApertureValue = "2.526068811667587";
    BrightnessValue = "-0.4554591284958377";
    ExposureMode = 0;
    ExposureProgram = 2;
    ExposureTime = "0.0416666666666666";
    FNumber = "2.4";
    Flash = 32;
    FocalLenIn35mmFilm = 35;
    FocalLength = "2.18";
    ISOSpeedRatings = (
                       800
                       );
    LensMake = Apple;
    LensModel = "iPhone 5 front camera 2.18mm f/2.4";
    LensSpecification = (
                         "2.18",
                         "2.4",
                         "2.4"
                         );
    MeteringMode = 5;
    PixelXDimension = 640;
    PixelYDimension = 480;
    SceneType = 1;
    SensingMethod = 2;
    ShutterSpeedValue = "4. 584985584026477";
    WhiteBalance = 0;
}

CMSampleBuffer和它相关的类型在处理高级AV Foundation用例时扮演着重要的作用。稍后会实际用到它们,不过关于这个类型对象的一些额外用法将在下一章中介绍。

7.6.5 AVCaptureVideoDataOutput

让我们通过示例项目来学习如何使用AVCaptureVideoDataOutput.在Chapter 7目录中可以找到一个名为CubeKamera_Starter的示例项目。这个项目用到了一些将AVFoundation 和OpenGL ES相结合的技术。CubeKamera应用程序从前置摄像头捕捉视频并将这些视频帧映射为一个旋转方块的OpenGL贴图。

从项目的THCameraController类接口开始学习吧,如代码清单7-27所示。

代码清单7-27 THCameraController 接口

#import <AVFoundation/AVFoundation.h>
#import "THBaseCameraController.h"

@protocol THTextureDelegate <NSObject>
- (void)textureCreatedWithTarget:(GLenum)target name:(GLuint)name;
@end

@interface THCameraController : THBaseCameraController

- (instancetype)initWithContext:(EAGLContext *)context;
@property (weak, nonatomic) id <THTextureDelegate> textureDelegate;

@end

这个类的接口与本章之前的几个类类似,只不过多了几个有关OpenGL ES类型的定义。THCameraController的创建带有一个EAGLContext示例。这个对象提供了用于管理状态的渲染上下文和使用OpenGL ES进行绘制所需的资源。头文件还定义了一个THTextureDelegate协议,每当新的贴图(不可变OpenGL ES图片)被创建时就会调用这个协议。让我们看一下它的实现方法,如代码清单7-28所示。

代码清单7-28捕捉输出的配置

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

@interface THCameraController () <AVCaptureVideoDataOutputSampleBufferDelegate>

@property (weak, nonatomic) EAGLContext *context;
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;

@end

@implementation THCameraController

- (instancetype)initWithContext:(EAGLContext *)context {
    self = [super init];
    if (self) {
        _context = context;
    }
    return self;
}

- (NSString *)sessionPreset {                                                   // 1
    return AVCaptureSessionPreset640x480;
}

- (BOOL)setupSessionOutputs:(NSError **)error {

    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
    self.videoDataOutput.videoSettings =                                        // 2
    @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};

    [self.videoDataOutput setSampleBufferDelegate:self                          // 3
                                            queue:dispatch_get_main_queue()];

    if ([self.captureSession canAddOutput:self.videoDataOutput]) {              // 4
        [self.captureSession addOutput:self.videoDataOutput];
        return YES;
    }

    return NO;
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
}

@end

(1)这个应用程序不需要高分辨率的视频,所以我们重写sessionPreset方法来返回AVCaptureSessionPreset640x480.
(2)创建一个新的AVCaptureVideoDataOutput实例并自定义其videoSettings字典。摄像头初始格式为双平面420v。 如果回顾第1章关于色度子抽样的讨论,这个格式分为亮度和色度,并且在垂直和水平方法对颜色进行子抽样。虽然直接只用这个原始格式肯定可以,不过在使用OpenGL ES时经常会选用BGRA。要注意这一格式的转换会稍微牺牲一点性能。
(3)设置输出委托为self,因为类实现了AVCaptureVideoDataOutputSampleBufferDelegate协议,还在应该调用的委托方法上指定了调度队列。为queue参数指定的调度队列必须是一个序列队列。本例中,我们使用主队列,因为主队列最适合应用程序,不过在许多其他案例中,都是指定专门的视频处理队列。
(4)最后,执行标准测试来确认输出是否可以被添加到会话,如果可以,则添加到会话并返回YES。如果测试失败,则返回NO。

目前为止,这和我们之前的示例看起来很像。让我们继续并开始关注有关OpenGL ES整合点的一些知识。有关OpenGL ES的知识虽然不在本书谈论的范围内,不过作为AV Foundation开发人员,会发现在很多高性能的视频应用程序案例中OpenGL ES都是唯一提供控制和功能的可行解决方案。所以我们将AV Foundation基础框架所提供的工具视作与OpenGL ES之间的桥梁。想要找到完整的OpenGL ES 2.0介绍,建议阅读Erik Buck撰写的Learning OpenGLES for iOS-书(2012, Boston: Addison-Wesley)。

Core Video提供了一个对象类型CVOpenGLESTextureCache,作为Core Video像素buffer和OpenGL ES贴图之间的桥梁。缓存的目的是减少数据从CPU转移到GPU(也可能是反向)的开销。让我们从创建贴图缓存开始吧,如代码清单7-29所示。

代码清单7-29 创建OpenGLESTextureCache

@interface THCameraController () <AVCaptureVideoDataOutputSampleBufferDelegate>

@property (weak, nonatomic) EAGLContext *context;
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;

@property (nonatomic) CVOpenGLESTextureCacheRef textureCache;                           // 1
@property (nonatomic) CVOpenGLESTextureRef cameraTexture;

@end

@implementation THCameraController

- (instancetype)initWithContext:(EAGLContext *)context {
    self = [super init];
    if (self) {
        _context = context;
        CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,                // 2
                                                    NULL,
                                                    _context,
                                                    NULL,
                                                    &_textureCache);                    // 3
        if (err != kCVReturnSuccess) {
            NSLog(@"Error creating texture cache. %d", err);
        }
    }
    return self;
}

(1)添加两个新属性。第一个是为CVOpenGLESTextureCacheRef自己添加,第二个是为在AVCaptureVideoDataOutput委托回调中创建的CVOpenGLESTextureRef对象添加。
(2)使用CVOpenGLESTextureCacheCreate函数创建一个新的缓存实例。这个函数的关键参数是后备EAGLContext和textureCache指针。
(3)检查这个函数的返回值是个好主意。本例中,如果返回值不等于kCVReturnSuccess,则在控制台打印错误信息。在实际的应用程序中我们建议这一块的处理应该更加完善。

创建好贴图缓存后,我们就可以继续学习实际创建一些贴图了。使用CVPixelBuffer创建贴图的函数称为CVOpenGLESTextureCacheCreate TextureFromlmage。这个函数将在委托回调中使用,为每个视频帧创建一个新的贴图。如代码清单7-30所示。

代码清单7-30创建 OpenGL ES贴图

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {

    CVReturn err;
    CVImageBufferRef pixelBuffer =                                          // 1
        CMSampleBufferGetImageBuffer(sampleBuffer);

    CMFormatDescriptionRef formatDescription =                              // 2
        CMSampleBufferGetFormatDescription(sampleBuffer);
    CMVideoDimensions dimensions =
        CMVideoFormatDescriptionGetDimensions(formatDescription);

    err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, // 3
                                                       _textureCache,
                                                       pixelBuffer,
                                                       NULL,
                                                       GL_TEXTURE_2D,
                                                       GL_RGBA,
                                                       dimensions.height,
                                                       dimensions.height,
                                                       GL_BGRA,
                                                       GL_UNSIGNED_BYTE,
                                                       0,
                                                       &_cameraTexture);

    if (!err) {
        GLenum target = CVOpenGLESTextureGetTarget(_cameraTexture);         // 4
        GLuint name = CVOpenGLESTextureGetName(_cameraTexture);
        [self.textureDelegate textureCreatedWithTarget:target name:name];   // 5
    } else {
        NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
    }

    [self cleanupTextures];
}

- (void)cleanupTextures {                                                   // 6
    if (_cameraTexture) {
        CFRelease(_cameraTexture);
        _cameraTexture = NULL;
    }
    CVOpenGLESTextureCacheFlush(_textureCache, 0);
}

(1)首先从捕捉到的CMSampleBuffer获取基础CVImageBuffer(CVImageBuffer是CVPixelBufferRef的类型定义)。
(2)获取CMSampleBuffer的CMFormatDescription。用CMVideoFormatDescriptionGetDimensions函数通过格式描述信息来获取视频帧的维度。它会返回一个带有宽和高信息的CMVideoDimensions结构。
(3)使用CVOpenGLESTextureCacheCreateTextureFromlmage函 数从CVPixlBuffer创建一个OpenGL ES贴图。会注意到我们将dimensions.height作为宽和高两个参数的值。这并不是印刷错误,不过的确有点欺骗性,因为我们希望在水平方向上剪辑视频,所以这是一个完美矩形。还有另一种方法可以实现这样的裁剪,不过我们所用的这个方法既简单又合适。
(4)从CVOpenGLESTextureRef获取target和name的值。它们用来将贴图对象与旋转的小方块的表面进行合适的绑定。
(5)调用委托的textureCreatedWithTarget:name方法, 这里委托函数会执行实际的GL贴图绑定。
(6)最后,调用私有cleanupTextures来释放贴图并刷新贴图缓存。

应用程序已经全部开发完毕并准备试运行。运行应用程序会看到出现一个不断旋转的小方块,它的表面图案就是捕捉到的视频帧。这虽然是一个简单应用程序,不过它很好地将AVFoundation和OpenGL ES整合在一起,开发者会发现很多时候这两个技术都在同时使用。

AVCaptureVideoDataOutput定义了在视频帧被捕捉时访问该帧的接口。它提供了对呈现的数据和处理中的数据全方面的控制,让开发者可以创建出非常强大的视频应用。下一章在介绍如何记录自定义视频处理结果时还会提到这个话题。

7.7 小结

本章我们学习了许多功能和特性来润色和改进我们的应用程序。虽然我们都是孤立地在使用这些功能,不过在实际中会发现这些功能效果不错,可以很好地用在单一应用程序中。第6章和第7章介绍的这些功能和特性都是我们创建下一代精彩捕捉类应用程序的工具。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342