【iOS音视频学习】AVFoundation人脸识别

537 阅读5分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

AVFoundation

  • AVFoundation是苹果OS X和iOS系统中用于处理基于时间的媒体数据的高级OC框架。通过开发所需的工具提供了大量功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序。AVFoundation充分利用了苹果A系列处理器的优势...好了,不扯了,总而言之就是很厉害,做iOS音视频开发,你就是必须得学。

这篇文章主要讲解使用AVFoundation做人脸识别,关于AVFoundation的基础捕捉实现,可以看我之前的文章【iOS音视频学习】AVFoundation捕捉

人脸识别

目前客户端人脸识别框架很多,苹果自家的AVFoundationCoreImageVision,知名开源框架OpenCV,腾讯阿里等三方框架,AVFoundation是这些里学习成本使用成本最低的,非常方便在业务初期实现简单的人脸识别功能。

  • 静态人脸识别: 识别单张图片
  • 动态人脸识别: 拍摄预览,视频

静态动态AVFoundation均支持

思路

  • 使用AVCaptureMetadataOutput获取人脸识别元数据
  • 通过CALayer展现人脸捕捉框,其实操作AVCaptureMetadataOutput获取数据很简单,把获取到的数据展示出来会比较麻烦,设计到各种坐标转换

一 给捕捉会话设置AVCaptureMetadataOutput

  • 注意,人脸检测使用了硬件加速器,所以任务需要在主线程执行
- (BOOL)configMetadataOutputWithType:(NSArray<AVMetadataObjectType> *)metadatObjectTypes {
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    // 人脸检测使用了硬件加速器,所以任务需要在主线程执行
    if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession beginConfiguration];
        [self.captureSession addOutput:self.metadataOutput];
        // 限制检查到元数据类型集合的做法是一种优化处理方法。可以减少我们实际感兴趣的对象数量
        self.metadataOutput.metadataObjectTypes = metadatObjectTypes;
        [self.metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        [self.captureSession commitConfiguration];
        return YES;
    } else {
        return NO;
    }
}

- (void)removeMetadataOutput {
    if (self.metadataOutput) [self.captureSession removeOutput:self.metadataOutput];

}
  • 在回调中可以拿到元数据
  • 注意这个回调是一直回调的
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
    if (self.delegate && [self.delegate respondsToSelector:@selector(mediaCaptureMetadataSuccessWithMetadataObjects:)]) {
        [self.delegate mediaCaptureMetadataSuccessWithMetadataObjects:metadataObjects];
    }
}

二 将元数据给到展示Layer

  • 因为回调是一直回调,例如人脸A一直捕捉到,那就会一直给到数据,人脸大小倾斜角等可能会变化,元数据中有个faceID是人脸唯一的,所以这里可以使用字典存储人脸,有相同的直接展示,不用一直创建浪费性能
  • 这里要做到人脸捕捉框跟随人脸的变动倾斜等,需要对YawAngle(偏移)RollAngle(转动)设置
  • RollAngle(转动)围绕Z轴旋转,例如脸部向肩膀转动
  • RollAngle(转动)围绕Y轴,例如点头

@property(nonatomic, strong) **CALayer** *overlayLayer;  /// *< 承载人脸识别layer*

@property(nonatomic, strong) NSMutableDictionary<NSNumber *, CALayer *> *faceLayers;  /// *< 人脸识别layer集合,key是faceID*

[self.layer addSublayer:self.overlayLayer];

- (void)setFaceMetadataObjects:(**NSArray**<**AVMetadataFaceObject** *> *)faceMetadataObjects {
    _faceMetadataObjects = faceMetadataObjects;
    NSArray *transformedFaces = [self transformedMetadataObjects:faceMetadataObjects];
    NSMutableArray *needRemoveFaces = [self.faceLayers.allKeys mutableCopy];  // 需要删除的
    for (AVMetadataFaceObject** *faceObj in transformedFaces) {
        NSNumber *faceID = @(faceObj.faceID);
        // 存在的不需要删除,从删除数组中移除
        [needRemoveFaces removeObject:faceID];
        // 拿到当前faceID对应的layer
        CALayer *faceLayer = self.faceLayers[faceID];
        // 如果给定的faceID 没有找到对应的图层,创建一个新的,找的则直接使用
        if (!faceLayer) {
            faceLayer = [**CALayer** layer];
            faceLayer.borderWidth = 5.0f;
            faceLayer.borderColor = [UIColor redColor].CGColor;
            [self.overlayLayer addSublayer:faceLayer];
            self.faceLayers[faceID] = faceLayer;
        }
        // 图层的大小 = 人脸的大小
        faceLayer.frame = faceObj.bounds;
        // 设置图层的transform属性 CATransform3DIdentity 先创建一个3D单元矩阵
        faceLayer.transform = CATransform3DIdentity;
        // 判断人脸对象是否具有有效的倾斜旋转。围绕Z轴,例人脸向肩膀转动
        if (faceObj.hasRollAngle) {
            // 获取相应的CATransform3D值,将它与标识变化关联在一起,并设置transform属性
            CATransform3D t = [self transformForRollAngle:faceObj.rollAngle];
            faceLayer.transform = CATransform3DConcat(faceLayer.transform, t);
        }
        // 判断人脸对象是否具有有效的偏转角
        if (faceObj.hasYawAngle) {
            // 获取相应的CATransform3D值
            CATransform3D  t = [self transformForYawAngle:faceObj.yawAngle];
            faceLayer.transform = CATransform3DConcat(faceLayer.transform, t);
        }
    }
    for (NSNumber *faceID in needRemoveFaces) {
        CALayer *layer = self.faceLayers[faceID];
        [layer removeFromSuperlayer];
        [self.faceLayers removeObjectForKey:faceID];
    }
}

  • 投影方式 正投影,无法体现立体效果   透视投影,体现立体效果
  • eyePosition 观察者到投射面的距离,一般建议500-1000
  • 这里需要注意RollAngle(转动)的转换处理要考虑设备方向,否则识别框会有点不太自然。
#pragma mark - 人脸识别layer相关函数

/**
 透视效果
  @param eyePosition 观察者到投射面的距离
 */
- (CATransform3D)transform3DMakePerspective:(CGFloat)eyePosition {
    // CATransform3D 图层的旋转,缩放,偏移,歪斜和应用的透*
    // CATransform3DIdentity是单位矩阵,该矩阵没有缩放,旋转,歪斜,透视。该矩阵应用到图层上,就是设置默认值。*
    CATransform3D  transform = CATransform3DIdentity;
    // 透视效果(就是近大远小),是通过设置m34 m34 = -1.0/D 默认是0.D越小透视效果越明显
    // eyePosition 观察者到投射面的距离*
    transform.m34 = -1.0/eyePosition;
    return transform;
}

/// 摄像头元数据转换
- (NSArray<AVMetadataObject *> *)transformedMetadataObjects:(NSArray<AVMetadataObject *> *)medMetadataObjects {
    NSMutableArray *transformedMetadatas = [NSMutableArray array];
    for (AVMetadataObject *metadataObject in medMetadataObjects) {
        // 将摄像头的元数据 转换为 视图上的可展示的元数据
        // 简单说:UIKit的坐标 与 摄像头坐标系统(0,0)-(1,1)不一样。所以需要转换
        // 转换需要考虑图层、镜像、视频重力、方向等因素 在iOS6.0之前需要开发者自己计算,但iOS6.0后提供方法
        AVMetadataObject *transformedMetadata = [(AVCaptureVideoPreviewLayer *)self.layer transformedMetadataObjectForMetadataObject:metadataObject];
        [transformedMetadatas addObject:transformedMetadata];
    }
    return transformedMetadatas;
}

/// 将RollAngle 的 度数转弧度 再生成 CATransform3D
- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees {
    // 将人脸对象得到的RollAngle 单位“度” 转为Core Animation需要的弧度值
    CGFloat rollAngleInRadians = [self degreesToRadians:rollAngleInDegrees];
    // RollAngle围绕Z旋转 x,y,z轴为0,0,1 得到绕Z轴倾斜角旋转转换
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}

/// 将YawAngle 的 度数转弧度 再生成 CATransform3D
- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees {
    // 绕Y轴旋转

    // 由于overlayer 需要应用sublayerTransform,所以图层会投射到z轴上,人脸从一侧转向另一侧会有3D 效果
     CGFloat yawAngleInRaians = [self degreesToRadians: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]);
}

/// 度数转弧度
- (CGFloat)degreesToRadians:(CGFloat)degrees {
    return degrees * M_PI / 180;
}

/// 根据设备方向调整角度
- (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);

}