AV Foundation ⑭ 捕捉中检测人脸

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 <NSObject>

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

@end
  
@interface CameraController : CameraController
//...
@property (weak, nonatomic) id <FaceDetectionDelegate> faceDetectionDelegate;
//...
@end

接着,定义一个 setupSessionFaceOutputs:用于配置捕捉会话添加 AVCaptureMetadataOutput 输出,并

  1. 创建 AVCaptureMetadataOutput 的实例,并将其添加至捕捉会话的实例 captureSession 中;
  2. 设置 metadataOutput 输出元数据类型的属性 metadataObjectTypes为人脸类型AVMetadataObjectTypeFace
  3. 设置 metadataOutput 的元数据输出协议 AVCaptureMetadataOutputObjectsDelegate 的委托对象,当有新的元数据被检测到时,会回调captureOutput:didOutputMetadataObjects:fromConnection: 方法
  4. 实现 captureOutput:didOutputMetadataObjects:fromConnection:方法,输出它们的 faceIdbounds

@interface CameraController ()<AVCaptureMetadataOutputObjectsDelegate>
@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

AVCaptureMetadataOutputmetadataObjectTypes 还可以过滤条码识别,AVMetadataObjectTypeQRCodeAVMetadataObjectTypeAztecCodeAVMetadataObjectTypeDataMatrixCodeAVMetadataObjectTypePDF417Code

在预览视图中处理人脸数据

    在添加人脸检测功能时,需要使用 Core Animation 的代码实现一个矩形图层显示在新检测到的人脸位置;

  1. 先创建一个 overlayLayer ,其大小等于预览图层的边界并将其添加到 AVCaptureVideoPreviewLayer 预览层上,并设置层的 sublayerTransform 属性为 CATransform3D
  2. 由于人脸数量不仅仅是一个,因此创建一个 faceLayersNSMutableDictionary 容器根据 AVMetadataFaceObject对象的 faceId 属性作为 Key 用于存储一个个的矩形图层,用于确定哪些人脸移出了视图并将其对应的图层移出用户界面;
  3. 因为 AVCaptureMetadataOutput 对象捕捉的元数据 AVMetadataObject 是设备坐标系,所以要将该数据转换到视图坐标系空间,通过实现transformedFacesFromFaces 方法,调用AVCaptureVideoPreviewLayertransformedMetadataObjectForMetadataObject: 完成转换,并返回;
  4. 实现 didDetectFaces: 方法,将人脸元数据转换坐标后,获取当前faceLayers中的所有 key 也就是 存储faceId的数组 lostFaces,遍历每个转换的人脸对象并根据其 faceId查找对应的图层,如果查找到则从 lostFaces中移出,如果未查找到根据其边界创建一个带有颜色边框的图层添加至 overlayLayer,缓存至字典中
  5. 设置图层的 transform 属性 CATransform3DIdentity,通过检查 hasRollAngle 判断是否有斜倾角,则通过transformForRollAngle: 获取相应的 CATransform3D值,将它与标识变化关联在一起,并设置 transform 属性
  6. 同样的根据偏转角通过transformForYawAngle:获取,获取相应的 CATransform3D值,将它与标识变化关联在一起,并设置 transform 属性 ,完成图层的 3D 效果旋转。
  7. 遍历数组将剩下的人脸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;
}



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

推荐阅读更多精彩内容