AVMetadataFaceObject
iOS 内置的相机应用会有这样一个功能,视图中有新的人脸进入时,会自动建立享元的焦点。一个黄色的矩形会显示在新检测到的人脸位置,并以矩形的中点完成自动对焦。我们在使用 AV Foundation
时,通过一个特定的 AVCaptureOutput
子类 AVCaptureMetadataOutput
就可以在应用程序中实现相同的功能。
AVCaptureMetadataOutput
支持对最多 10 个人脸进行实时检测。与其它输出类似,不同于输出一个静态图片或 QuickTime 影片,它输出的是元数据。这个元数据来自于 AVMetadataObject
抽象类的形式,该类定义了用来处理多种元数据类型的接口。当使用人脸检测时,会输出一个具体的子类类型 AVMetadataFaceObject
。
AVMetadataFaceObject
实例定义了多个用于描述被检测到人脸的属性,最重要的一个属性就是人脸的边界bounds
。它是一个设备标量坐标格式的 CGRect
。除了边界 , AVMetadataFaceObject
实例还给出了用于定义检测人脸倾斜角 rollAngle
和偏转角的参数 yawAngle
。倾斜角表示人的头部向肩膀方向的侧倾角度,偏转角表示人脸绕 y 轴旋转的角度。
获取人脸数据的实现
在基于AV Foundation ⑬ 创建一个简单的相机程序 相机控制器 CameraController
基础上增加一个 FaceDetectionDelegate
协议,定义一个didDetectFaces:
处理人脸的方法,并使子类遵守它:
@protocol FaceDetectionDelegate
- (void)didDetectFaces:(NSArray *)faces;
@end
@interface CameraController : CameraController
//...
@property (weak, nonatomic) id faceDetectionDelegate;
//...
@end
接着,定义一个 setupSessionFaceOutputs:
用于配置捕捉会话添加 AVCaptureMetadataOutput
输出,并
- 创建
AVCaptureMetadataOutput
的实例,并将其添加至捕捉会话的实例captureSession
中; - 设置
metadataOutput
输出元数据类型的属性metadataObjectTypes
为人脸类型AVMetadataObjectTypeFace
; - 设置
metadataOutput
的元数据输出协议AVCaptureMetadataOutputObjectsDelegate
的委托对象,当有新的元数据被检测到时,会回调captureOutput:didOutputMetadataObjects:fromConnection:
方法 - 实现
captureOutput:didOutputMetadataObjects:fromConnection:
方法,输出它们的faceId
和bounds
@interface CameraController ()
@property(nonatomic,strong)AVCaptureMetadataOutput *metadataOutput;
@end
@implementation CameraController
- (BOOL)setupSessionFaceOutputs:(NSError **)error {
self.metadataOutput = [[AVCaptureMetadataOutput alloc]init];
//为捕捉会话添加设备
if ([self.captureSession canAddOutput:self.metadataOutput]){
[self.captureSession addOutput:self.metadataOutput];
//获得人脸属性
NSArray *metadatObjectTypes = @[AVMetadataObjectTypeFace];
//设置metadataObjectTypes 指定对象输出的元数据类型。
/*
限制检查到元数据类型集合的做法是一种优化处理方法。可以减少我们实际感兴趣的对象数量
支持多种元数据。这里只保留对人脸元数据感兴趣
*/
self.metadataOutput.metadataObjectTypes = metadatObjectTypes;
//创建主队列: 因为人脸检测用到了硬件加速,而且许多重要的任务都在主线程中执行,所以需要为这次参数指定主队列。
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//通过设置AVCaptureVideoDataOutput的代理,就能获取捕获到一帧一帧数据
[self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
return YES;
}else
{
//报错
if (error) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey:@"Failed to still image output"};
*error = [NSError errorWithDomain:THCameraErrorDomain code:THCameraErrorFailedToAddOutput userInfo:userInfo];
}
return NO;
}
}
//捕捉数据
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
fromConnection:(AVCaptureConnection *)connection {
//使用循环,打印人脸数据
for (AVMetadataFaceObject *face in metadataObjects) {
NSLog(@"Face detected with ID:%li",(long)face.faceID);
NSLog(@"Face bounds:%@",NSStringFromCGRect(face.bounds));
}
// 调用委托对象的 didDetectFaces: 方法
[self.faceDetectionDelegate didDetectFaces:metadataObjects];
}
@end
AVCaptureMetadataOutput
的metadataObjectTypes
还可以过滤条码识别,AVMetadataObjectTypeQRCode
、AVMetadataObjectTypeAztecCode
、AVMetadataObjectTypeDataMatrixCode
、AVMetadataObjectTypePDF417Code
在预览视图中处理人脸数据
在添加人脸检测功能时,需要使用 Core Animation 的代码实现一个矩形图层显示在新检测到的人脸位置;
- 先创建一个
overlayLayer
,其大小等于预览图层的边界并将其添加到AVCaptureVideoPreviewLayer
预览层上,并设置层的sublayerTransform
属性为CATransform3D
- 由于人脸数量不仅仅是一个,因此创建一个
faceLayers
的NSMutableDictionary
容器根据AVMetadataFaceObject
对象的faceId
属性作为Key
用于存储一个个的矩形图层,用于确定哪些人脸移出了视图并将其对应的图层移出用户界面; - 因为
AVCaptureMetadataOutput
对象捕捉的元数据AVMetadataObject
是设备坐标系,所以要将该数据转换到视图坐标系空间,通过实现transformedFacesFromFaces
方法,调用AVCaptureVideoPreviewLayer
的transformedMetadataObjectForMetadataObject:
完成转换,并返回; - 实现
didDetectFaces:
方法,将人脸元数据转换坐标后,获取当前faceLayers
中的所有key
也就是 存储faceId
的数组lostFaces
,遍历每个转换的人脸对象并根据其faceId
查找对应的图层,如果查找到则从lostFaces
中移出,如果未查找到根据其边界创建一个带有颜色边框的图层添加至overlayLayer
,缓存至字典中 - 设置图层的
transform
属性CATransform3DIdentity
,通过检查hasRollAngle
判断是否有斜倾角,则通过transformForRollAngle:
获取相应的CATransform3D
值,将它与标识变化关联在一起,并设置transform
属性 - 同样的根据偏转角通过
transformForYawAngle:
获取,获取相应的CATransform3D
值,将它与标识变化关联在一起,并设置transform
属性 ,完成图层的 3D 效果旋转。 - 遍历数组将剩下的人脸ID 集合从上一个图层和
faceLayers
字典中移除
- (NSMutableDictionary *)faceLayers{
if (!_faceLayers) {
_faceLayers = [NSMutableDictionary dictionary];
}
return _faceLayers;
}
- (CALayer *)overlayLayer{
if (!_overlayLayer) {
_overlayLayer.frame = self.bounds;
_overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);;
}
return _overlayLayer;
}
//将设备的坐标空间的人脸转换为视图空间的对象集合
- (NSArray *)transformedFacesFromFaces:(NSArray *)faces {
NSMutableArray *transformeFaces = [NSMutableArray array];
for (AVMetadataObject *face in faces) {
//将摄像头的人脸数据 转换为 视图上的可展示的数据
//简单说:UIKit的坐标 与 摄像头坐标系统(0,0)-(1,1)不一样。所以需要转换
//转换需要考虑图层、镜像、视频重力、方向等因素 在iOS6.0之前需要开发者自己计算,但iOS6.0后提供方法
AVMetadataObject *transformedFace = [self.previewLayer transformedMetadataObjectForMetadataObject:face];
//转换成功后,加入到数组中
[transformeFaces addObject:transformedFace];
}
return transformeFaces;
}
//将检测到的人脸进行可视化
- (void)didDetectFaces:(NSArray *)faces {
//创建一个本地数组 保存转换后的人脸数据
NSArray *transformedFaces = [self transformedFacesFromFaces:faces];
//获取faceLayers的key,用于确定哪些人移除了视图并将对应的图层移出界面。
/*
支持同时识别10个人脸
*/
NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
//遍历每个转换的人脸对象
for (AVMetadataFaceObject *face in transformedFaces) {
//获取关联的faceID。这个属性唯一标识一个检测到的人脸
NSNumber *faceID = @(face.faceID);
//将对象从lostFaces 移除
[lostFaces removeObject:faceID];
//拿到当前faceID对应的layer
CALayer *layer = self.faceLayers[faceID];
//如果给定的faceID 没有找到对应的图层
if (!layer) {
//调用makeFaceLayer 创建一个新的人脸图层
layer = [self makeFaceLayer];
//将新的人脸图层添加到 overlayLayer上
[self.overlayLayer addSublayer:layer];
//将layer加入到字典中
self.faceLayers[faceID] = layer;
}
//设置图层的transform属性 CATransform3DIdentity 图层默认变化 这样可以重新设置之前应用的变化
layer.transform = CATransform3DIdentity;
//图层的大小 = 人脸的大小
layer.frame = face.bounds;
//判断人脸对象是否具有有效的斜倾角。
if (face.hasRollAngle) {
//如果为YES,则获取相应的CATransform3D 值
CATransform3D t = [self transformForRollAngle:face.rollAngle];
//将它与标识变化关联在一起,并设置transform属性
layer.transform = CATransform3DConcat(layer.transform, t);
}
//判断人脸对象是否具有有效的偏转角
if (face.hasYawAngle) {
//如果为YES,则获取相应的CATransform3D 值
CATransform3D t = [self transformForYawAngle:face.yawAngle];
layer.transform = CATransform3DConcat(layer.transform, t);
}
}
//遍历数组将剩下的人脸ID集合从上一个图层和faceLayers字典中移除
for (NSNumber *faceID in lostFaces) {
CALayer *layer = self.faceLayers[faceID];
[layer removeFromSuperlayer];
[self.faceLayers removeObjectForKey:faceID];
}
}
- (CALayer *)makeFaceLayer {
//创建一个layer
CALayer *layer = [CALayer layer];
//边框宽度为5.0f
layer.borderWidth = 5.0f;
//边框颜色为红色
layer.borderColor = [UIColor redColor].CGColor;
//返回layer
return layer;
}
//将 RollAngle 的 rollAngleInDegrees 值转换为 CATransform3D
- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees {
//将人脸对象得到的RollAngle 单位“度” 转为Core Animation需要的弧度值
CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);
//将结果赋给CATransform3DMakeRotation x,y,z轴为0,0,1 得到绕Z轴倾斜角旋转转换
return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}
//将 YawAngle 的 yawAngleInDegrees 值转换为 CATransform3D
- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees {
//将角度转换为弧度值
CGFloat yawAngleInRaians = THDegreesToRadians(yawAngleInDegrees);
//将结果CATransform3DMakeRotation x,y,z轴为0,-1,0 得到绕Y轴选择。
//由于overlayer 需要应用sublayerTransform,所以图层会投射到z轴上,人脸从一侧转向另一侧会有3D 效果
CATransform3D yawTransform = CATransform3DMakeRotation(yawAngleInRaians, 0.0f, -1.0f, 0.0f);
//因为应用程序的界面固定为垂直方向,但需要为设备方向计算一个相应的旋转变换
//如果不这样,会造成人脸图层的偏转效果不正确
return CATransform3DConcat(yawTransform, [self orientationTransform]);
}
- (CATransform3D)orientationTransform {
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:
angle = 0.0f;
break;
}
return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused"
static CGFloat THDegreesToRadians(CGFloat degrees) {
return degrees * M_PI / 180;
}