AVFoundation-06人脸检测

概述

AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。

iOS 媒体环境.png

实现效果

人脸识别.jpeg

捕捉会话

AV Foundation 捕捉栈的核心类是AVCaptureSession。一个捕捉会话相当于一个虚拟的插线板,用于连接输入和输出的资源。捕捉会话管理从物理设备得到的数据流。

self.captureSession = [[AVCaptureSession alloc] init];
[self.captureSession setSessionPreset:AVCaptureSessionPreset640x480];
[self.previewView setSession:self.captureSession];

AVCaptureMetadataOutput

在使用捕捉设备进行处理前,首先要添加输入、输出。输出的类型是 AVCaptureOutput,它是一个抽象基类,用于将捕捉到的数据输出。AV Foundation 框架定义了AVCaptureOutput 的一些扩展,但是在人脸动态识别的时候我们用到是它的子类 AVCaptureMetadataOutput。AVCaptureMetadataOutput 用于输出元数据,如二维码、条形码、以及人脸等。我们在使用的时候需要指定 Metadata 的相关类型,我们可以通过 AVCaptureMetadataOutput 的 ```@property(nonatomic, readonly) NSArray<AVMetadataObjectType> *availableMetadataObjectTypes;


// Output
self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
[self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_global_queue(0, 0)];

if ([self.captureSession canAddOutput:self.metaDataOutput]) {
[self.captureSession addOutput:self.metaDataOutput];
}
for (NSString *metaType in self.metaDataOutput.availableMetadataObjectTypes) {
NSLog(@"%@", metaType);
}
self.metaDataOutput.metadataObjectTypes = @[AVMetadataObjectTypeFace];


####AVMetadataFaceObject
当检测到人脸的时候AVCaptureMetadataOutput 会输出子类型为 AVMetadataFaceObject 的数组。AVMetadataFaceObject 定义了多个描述检测到人脸的属性。其中最重要的是人脸的边界(bounds),它是CGRect类型的变量。它的坐标系是基于设备标量坐标系,它的范围是摄像头原始朝向左上角(0,0)到右下角(1,1)。除了边界,AVMetadataFaceObject还提供了检测到人脸的斜倾角和偏转角。斜倾角(rollAngle)表示人的头部向肩的方向侧倾角度, 偏转角(yawAngle)表示人沿Y轴旋转的角度。AVMetadataFaceObject 定义如下:

@interface AVMetadataFaceObject : AVMetadataObject <NSCopying>
{
@private
AVMetadataFaceObjectInternal *_internal;
}
@property(readonly) NSInteger faceID;
// 斜倾角
@property(readonly) BOOL hasRollAngle;
@property(readonly) CGFloat rollAngle;
// 偏转角
@property(readonly) BOOL hasYawAngle;
@property(readonly) CGFloat yawAngle;
@end


###捕捉预览
AVCaptureVideoPreviewLayer是CALayer的子类,可以对捕捉视频进行实时预览。它有个AVLayerVideoGravity属性可以控制画面的缩放和拉升效果。

AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
previewLayer.frame = [UIScreen mainScreen].bounds;
[self.view.layer addSublayer:previewLayer];

####坐标变换
由于 AVMetadataFaceObject 中的人脸的边界(bounds)的坐标系是基于设备标量坐标系,它的范围是摄像头原始朝向左上角(0,0)到右下角(1,1),因此需要将它转换到我们的视图坐标系中。在转换的时候系统会考虑orientation, mirroring, videoGravity 等许多因素。在转换的时候我们只需要使用捕捉预览 AVCaptureVideoPreviewLayer 提供的 ```- (nullable AVMetadataObject *)transformedMetadataObjectForMetadataObject:(AVMetadataObject *)metadataObject;``` 方法。

  • (NSArray *)transformFacesToLayerFromFaces:(NSArray *)faces
    {
    NSMutableArray *transformFaces = [NSMutableArray array];
    for (AVMetadataFaceObject *face in faces) {
    AVMetadataObject *transFace = [(AVCaptureVideoPreviewLayer *)self.layer transformedMetadataObjectForMetadataObject:face]
    [transformFaces addObject:transFace];
    }
    return transformFaces;
    }

####实现动态人脸检测
+ 创建捕捉会话,并设置输入、输出以及预览图层。将 AVCaptureMetadataOutput 的metadataObjectTypes 设为捕获人脸 AVMetadataObjectTypeFace。

  • (void)setupSession
    {
    self.captureSession = [[AVCaptureSession alloc] init];

    self.captureSession = [[AVCaptureSession alloc] init];
    [self.captureSession setSessionPreset:AVCaptureSessionPreset640x480];
    [self.previewView setSession:self.captureSession];

    // Create a device input with the device and add it to the session.
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error;
    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
    if (!input) {
    return;
    }
    [self.captureSession addInput:input];

    // Create a VideoDataOutput and add it to the session
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [self.videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(0, 0)];
    self.videoDataOutput.videoSettings = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey];
    [self.captureSession addOutput:self.videoDataOutput];

    // Output
    self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];

    [self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_global_queue(0, 0)];
    if ([self.captureSession canAddOutput:self.metaDataOutput]) {
    [self.captureSession addOutput:self.metaDataOutput];
    }

    for (NSString *metaType in self.metaDataOutput.availableMetadataObjectTypes) {
    NSLog(@"%@", metaType);
    }

    self.metaDataOutput.metadataObjectTypes = @[AVMetadataObjectTypeFace];

    [self.captureSession startRunning];
    }


+ 创建预览视图 QMPreviewView,将预览视图的 layerClass 设置为 AVCaptureVideoPreviewLayer,并初始化相关视图。由于我们用到了偏转角(yawAngle)在旋转的时候我们需要用到透视投影,这样在绕Y轴旋转的时候才更加逼真。

// 透视投影
static CATransform3D PerspectiveTransformMake(CGFloat eyePosition)
{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / eyePosition;
return transform;
}

  • (Class)layerClass
    {
    return [AVCaptureVideoPreviewLayer class];
    }
  • (instancetype)initWithFrame:(CGRect)frame
    {
    if (self = [super initWithFrame:frame]) {
    [self setupView];
    }
    return self;
    }

  • (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
    if (self = [super initWithCoder:aDecoder]) {
    [self setupView];
    }
    return self;
    }

  • (void)setupView
    {
    _faceLayerDict = [NSMutableDictionary dictionary];

    AVCaptureVideoPreviewLayer *previewLayer = (id)self.layer;
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    self.overlayLayer = [CALayer layer];
    self.overlayLayer.frame = self.bounds;
    self.overlayLayer.sublayerTransform = PerspectiveTransformMake(1000);
    [previewLayer addSublayer:self.overlayLayer];
    }


+ 初始化QMPreviewView,初始化后需要为它设置 AVCaptureSession 才能进行捕捉的实时预览。

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    [self setupView];
    [self setupSession];
    }

  • (void)setupView
    {
    QMPreviewView *previewView = [[QMPreviewView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:previewView];
    _previewView = previewView;
    }

+ 处理AVCaptureMetadataOutputObjectsDelegate回调方法解析并绘制人脸矩形。每个人脸 AVFoundation 都会给出唯一的faceID,当人脸离开屏幕时候,对应的人脸也会在回调中消失。我们根据人脸ID保存着绘制的矩形,当人脸消失的时候,我们需要将绘制的矩形去除。

  • (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
    {
    // for (AVMetadataFaceObject *face in metadataObjects) {
    // NSLog(@"face = %ld, bounds = %@", face.faceID, NSStringFromCGRect(face.bounds));
    // }

    [self.previewView onDetectFaces:metadataObjects];
    }

  • (NSArray *)transformFacesToLayerFromFaces:(NSArray *)faces
    {
    NSMutableArray *transformFaces = [NSMutableArray array];
    for (AVMetadataFaceObject *face in faces) {
    AVMetadataObject *transFace = [(AVCaptureVideoPreviewLayer *)self.layer transformedMetadataObjectForMetadataObject:face]
    [transformFaces addObject:transFace];

    }
    return transformFaces;
    }

  • (CALayer *)makeLayer
    {
    CALayer *layer = [CALayer layer];
    layer.borderWidth = 5.0f;
    layer.borderColor = [UIColor colorWithRed:0.0f green:255.0f blue:0.0f alpha:255.0f].CGColor;
    return layer;
    }

  • (CATransform3D)transformFromYawAngle:(CGFloat)angle
    {
    CATransform3D t = CATransform3DMakeRotation(DegreeToRadius(angle), 0.0f, -1.0f, 0.0f);
    return CATransform3DConcat(t, [self orientationTransform]);
    }

  • (CATransform3D)orientationTransform
    {
    CGFloat angle = 0.0f;
    switch ([UIDevice currentDevice].orientation) {
    case UIDeviceOrientationPortraitUpsideDown:
    angle = M_PI;
    break;
    case UIDeviceOrientationLandscapeRight:
    angle = -M_PI/2.0;
    break;
    case UIDeviceOrientationLandscapeLeft:
    angle = M_PI/2.0;
    break;
    default:
    angle = 0.0f;
    break;
    }
    return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
    }

pragma mark - Public

  • (void)setSession:(AVCaptureSession *)session
    {
    ((AVCaptureVideoPreviewLayer *)self.layer).session = session;
    }

  • (void)onDetectFaces:(NSArray *)faces
    {
    // 坐标变换
    NSArray *transFaces = [self transformFacesToLayerFromFaces:faces];
    NSMutableArray *missFaces = [[self.faceLayerDict allKeys] mutableCopy];

    for (AVMetadataFaceObject *face in transFaces) {
    NSNumber *faceID = @(face.faceID);
    // 如果当前人脸还在镜头里,则不用移除
    [missFaces removeObject:faceID];

      CALayer *layer = self.faceLayerDict[faceID];
      if (!layer) {  // 生成新的人脸矩形
          layer = [self makeLayer];
          self.faceLayerDict[faceID] = layer;
          [self.overlayLayer addSublayer:layer];
      }
      
      layer.transform = CATransform3DIdentity;
      layer.frame = face.bounds;
      
      // 根据偏转角,对矩形进行旋转
      if (face.hasRollAngle) {
          CATransform3D t = CATransform3DMakeRotation(DegreeToRadius(face.rollAngle), 0, 0, 1.0);
          layer.transform = CATransform3DConcat(layer.transform, t);
      }
      // 根据斜倾角,对矩形进行旋转变换
      if (face.hasYawAngle) {
          CATransform3D t = [self transformFromYawAngle:face.yawAngle];
          layer.transform = CATransform3DConcat(layer.transform, t);
      }
    

    }
    // 去除离开屏幕的人脸和矩形视图变换
    for (NSNumber *faceID in missFaces) {
    CALayer *layer = self.faceLayerDict[faceID];
    [layer removeFromSuperlayer];
    [self.faceLayerDict removeObjectForKey:faceID];
    }
    }



####参考
[AVFoundation开发秘籍:实践掌握iOS & OSX应用的视听处理技术](https://book.douban.com/subject/26577333/)

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

推荐阅读更多精彩内容