阅读这篇文章请先了解 AVFoundation 拍照/录制视频 文章中视频的捕获流程。
先看下人脸捕获的流程图:-
AVCaptureSession
的初始化配置流程跟 AVFoundation 拍照/录制视频 文章中的基本一致,只是少用了 音频设备输入、照片输出、电影文件输出 相关的类。 -
AVCaptureMetadataOutput
:捕获元数据输出类。该功能的核心类,用来识别人脸并且输出人脸数据(faceID、bounds
...)。
注意:
AVFoundation
最多同时支持识别10个人脸。
AVCaptureSession
的初始化配置的核心代码:
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
AVCaptureDevice *videoDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *videoInput =
[AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
if ([self.captureSession canAddInput:videoInput]) {
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
}
}
self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
if ([self.captureSession canAddOutput:self.metadataOutput]) {
[self.captureSession addOutput:self.metadataOutput];
//设置识别的元数据类型
self.metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeFace];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
[self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
}
self.videoQueue = dispatch_queue_create("CQCamera.Video.Queue", NULL);
AVCaptureSession
的初始化配置结束后,将captureSession
设置为AVCaptureVideoPreviewLayer
的session
。
[(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
然后在全局串行队列内异步执行startRunning
:
if (![self.captureSession isRunning]) {
dispatch_async(self.videoQueue, ^{
[self.captureSession startRunning];
});
}
执行startRunning
方法后,一旦检测到人脸就会执行AVCaptureMetadataOutputObjectsDelegate
代理方法:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
if ([self.faceDetectDelegate respondsToSelector:@selector(didDetectFaces:)]) {
[self.faceDetectDelegate didDetectFaces:metadataObjects];
}
}
- 注意我们这里将
faceDetectDelegate
代理设置为previewView
,将元数据对象集合(metadataObjects
)传递到预览图层。因为预览图层里面有我们需要的UI
数据。 -
metadataObject
元数据对象中有我们需要的人脸基本数据faceID
、bounds
、rollAngle
、yawAngle
...
走到这一步,我们已经将人脸识别的核心功能做完了,下面就是如何根据检测到的人脸数据,进行UI处理,也就是将人脸框住。随着人脸的移动,框也作出响应的调整。
下面我们看下代理方法didDetectFaces:
中的核心代码:
- (void)didDetectFaces:(NSArray *)faces {
//转换坐标系
NSArray *transformedFaces = [self transformedFromFace:faces];
//
NSMutableArray *lostFaceKeys = [self.faceLayers.allKeys mutableCopy];
[transformedFaces enumerateObjectsUsingBlock:^(AVMetadataFaceObject * _Nonnull faceObj, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber *faceID = @(faceObj.faceID);
//1.移除: lostFaceKeys中移除当前捕获到的人脸
[lostFaceKeys removeObject:faceID];
//2.当前faceID对应的layer
CALayer *layer = self.faceLayers[faceID];
if (!layer) {
//2.1 如果没有 创建Layer
layer = [self makeLayer];
//2.2 添加
[self.overlayLayer addSublayer:layer];
//2.3 保存到字典中
self.faceLayers[faceID] = layer;
}
//3.配置属性
layer.transform = CATransform3DIdentity;//初始值
layer.frame = faceObj.bounds;
if (faceObj.hasRollAngle) {
CATransform3D rollTransform3D = [self transformRollAngle:faceObj.rollAngle];
layer.transform = CATransform3DConcat(layer.transform, rollTransform3D);
}
if (faceObj.hasYawAngle) {
CATransform3D yawTransform3D = [self transformYawAngle:faceObj.yawAngle];
layer.transform = CATransform3DConcat(layer.transform, yawTransform3D);
}
}];
//将剩下的人脸ID集合从上一个图层和faceLayers字典中移除
[lostFaceKeys enumerateObjectsUsingBlock:^(NSNumber * _Nonnull faceID, NSUInteger idx, BOOL * _Nonnull stop) {
CALayer *layer = self.faceLayers[faceID];
[layer removeFromSuperlayer];
[self.faceLayers removeObjectForKey:faceID];
}];
}
- 1、首先,我们需要转换坐标系,因为 元数据对象(
AVMetadataObject
)中一开始返回来的是摄像头的坐标数据,我们需要转换成视图上可展示的数据(也就是转换成UIKit
坐标数据)。
看下转换代码:
- (NSArray *)transformedFromFace:(NSArray *)faces {
NSMutableArray *transformedFaces = [NSMutableArray arrayWithCapacity:faces.count];
[faces enumerateObjectsUsingBlock:^(AVMetadataObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//转换需要考虑图层、镜像、视频重力、方向等因素 在iOS6.0之前需要开发者自己计算,但iOS6.0后提供方法
AVMetadataFaceObject *newObj = (AVMetadataFaceObject *)[self.previewLayer transformedMetadataObjectForMetadataObject:obj];
[transformedFaces addObject:newObj];
}];
return transformedFaces;
}
- 2、
layer
:就是我们用来框住人脸的框框。 - 3、
rollAngle、yawAngle
在3D坐标系中分别代表的是沿Z
轴 和Y
轴旋转的角度,被称为 欧拉角。我们这里并未用到的沿X
轴 旋转的角度称为Pitch
。
需要将我们获取到的 元数据对象(AVMetadataFaceObject
) 中的rollAngle、yawAngle
转为弧度然后再转为CATransform3D
。
//将rollAngle(欧拉角中的rollAngle围绕Z轴旋转)转换为CATransform3D
- (CATransform3D)transformRollAngle:(CGFloat)rollAngle {
CGFloat degrees = CQDegreesToRadians(rollAngle);//角度转弧度
return CATransform3DMakeRotation(degrees, 0.0f, 0.0f, 1.0f);
}
//将yawAngle(欧拉角中的yawAngle围绕Y轴旋转)转换为CATransform3D
- (CATransform3D)transformYawAngle:(CGFloat)yawAngle {
CGFloat degrees = CQDegreesToRadians(yawAngle);//角度转弧度
//由于overlayer 需要应用sublayerTransform,所以图层会投射到z轴上,人脸从一侧转向另一侧会有3D 效果
CATransform3D yawTransform3D = CATransform3DMakeRotation(degrees, 0.0f, -1.0f, 0.0f);
//因为应用程序的界面固定为垂直方向,但需要为设备方向计算一个相应的旋转变换
//如果不这样,会造成人脸图层的偏转效果不正确
CATransform3D orientationTransform3D = [self orientationTransform];
return CATransform3DConcat(yawTransform3D, orientationTransform3D);
}
代码中转换yawAngle
时调用了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);
}
注意:该文章只贴出了核心代码。
最后看下效果: