人脸检测

自定义相机中我们可以进行聚焦操作,我们也能检测人脸之后对目标人脸进行聚焦,这样看上去会更加智能。在 iOS 中,我们可以通过 Core Image 中的CIDetector和CIFaceFeature进行人脸检测功能。但是,在拍照软件中,人间检测对帧率有较高的要求,刚才提到的基于静态图片人脸检测的类似乎不太够用。幸运的是,除了他们可以检测人脸,在 AVFoundation 中的AVCaptureMetadataOutput也支持人脸检测,并且他直接在捕捉会话中工作,使用起来也十分方便。本文主要介绍AVCaptureMetadataOutput在人脸检测上的应用。

AVCaptureMetadataOutput 使用

AVCaptureMetadataOutput的使用和其他AVCaptureOutput很相似,主要分为下面:

创建AVCaptureMetadataOutput对象

将该输出类添加到捕捉会话中

开启AVCaptureMetadataOutput的人脸检测功能

[self.metaOutput setMetadataObjectTypes:@[AVMetadataObjectTypeFace]];

设置AVCaptureMetadataOutputObjectsDelegate代理

[self.metaOutput setMetadataObjectsDelegate:self queue:self.metaQueue];

PS: 必须在添加进捕捉会话后再设置metadataObjectTypes,否则程序将会因为该输出类暂时不支持人脸检测而崩溃。

AVCaptureMetadataOutputObjectsDelegate

在AVCaptureMetadataOutput中其实最重要的就是AVCaptureMetadataOutputObjectsDelegate代理,我们也是通过一下代理方法来获取识别结果的:

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof avmetadataobject> *)metadataObjects fromConnection:(AVCaptureConnection *)connection;

在这个方法中,我们最需要关注的就是metadataObjects数组,在这个数组中,我们能能拿到当前帧识别出来的所有对象。他的数组元素类型是AVMetadataObject,也是我们最终需要的AVMetadataFaceObject类的父类。在AVMetadataFaceObject中,我们可以获取以下信息:

time:识别到人脸的时间

duration:该人脸的维持时间

bounds:人脸在镜头中的位置(基于设备坐标)

需要使用AVCaptureVideoPreviewLayer中的坐标转换

faceID:人脸ID

rollAngle:倾斜角

表示人的头部向肩膀方向的侧斜角度

yawAngle:偏转角

人脸绕 y 轴旋转的角度

PS:浮点数在运算的时候会出现精度丢失的问题,这些细小的误差在视频处理中会无限放大,因此 AVFoundation 中时间的表示不用NSTimeInterval(double),而是使用CMTime。

CMTime其实就是使用分数来表示时间,即

。比如CMTimeMake(1,2)表示0.5秒

检测显示

因为有帧率和性能的要求,我们不能使用UIView显示。为了方便,本文使用CALayer进行显示:

人脸检测结果AVMetadataFaceObject中有faceID,我们需要将使用faceID区分不同人脸的人脸框显示CALayer,因此,需要下面的这个NSMutableDictionary进行存储:

@property (nonatomic, strong) NSMutableDictionary *faceLayers;

为了方便管理,我们统一将人脸框的CALayer添加到指定的CALayer(overlayLayer)上。在初始化的时候,需要注意以下细节:

overlayLayer的frame需要和预览视图一致

overlayLayer需要设置相应的sublayerTransform

为了显示yawAngle的效果,需要让子层绕 Y 轴旋转

self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);
CATransform3DMakePerspective
static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / eyePosition;
return transform;
}

人脸框显示前操作

上文说到,我们拿到的AVMetadataFaceObject中的bounds是基于设备坐标系的,但是显示的时候我们需要是普通视图坐标系中的。因此,需要先做下列的转换操作:

AVMetadataObject *transfromedFace = [self.videoPreviewLayer transformedMetadataObjectForMetadataObject:face]

人脸框显示操作

// 记录离开镜头的人脸框
NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
// 遍历识别到的所有人脸数据
for (AVMetadataFaceObject *face in transformedFaces) {
NSNumber *faceID = @(face.faceID);
[lostFaces removeObject:faceID];
// 获取人脸框
CALayer *layer = self.faceLayers[faceID];
if (!layer) {
layer = [self makeFaceLayer];
[self.overlayLayer addSublayer:layer];
self.faceLayers[faceID] = layer;
}
// 重置 transform
layer.transform = CATransform3DIdentity;
// 显示位置
layer.frame = face.bounds;
// 显示倾斜角
if (face.hasRollAngle) {
CATransform3D t = [face transformFromRollAngle];
layer.transform = CATransform3DConcat(layer.transform, t);
}
// 显示偏转角
if (face.hasYawAngle) {
CATransform3D t = [face transformFromYawAngle];
layer.transform = CATransform3DConcat(layer.transform, t);
}
}
// 移除已经离开镜头的人脸框
for (NSNumber *faceID in lostFaces) {
CALayer *layer = self.faceLayers[faceID];
[layer removeFromSuperlayer];
[self.faceLayers removeObjectForKey:faceID];
}

关于代码中transformFromRollAngle和transformFromYawAngle方法可以参考AVMetadataFaceObject+Transform.m