iOS音视频底层(二)之AVFoundation高级捕捉(人脸/二维码识别)

647 阅读7分钟

本文主要内容

一.iOS上人脸识别的策略分析
二.AVFoundation人脸识别实现
三.AVFoundation二维码识别

一.iOS上人脸识别的策略分析

  • CoreIamge
  • face++(2014阿里收购,收费)
  • OpenCV(图片处理,银行卡号、身份证号识别)
  • libefacedetection(C++)
  • AV Foundation(腾讯原生)
  • vision(苹果模型:iOS11.0)
  • 腾讯:优图项目组

人脸识别系统组成

截屏2022-08-16 09.16.26.png

截屏2022-08-16 09.17.44.png

二.AVFoundation人脸识别实现

人脸识别流程

  • 1.视频采集(上一篇详述,属于耗时工作)
  • 2.为session添加一个元数据的输出AVCaptureMetadataOutput
  • 3.设置元数据的范围(人脸数据、二维码数据、一维码等)
  • 4.开始捕捉:设置捕捉完成代理didOutputMetadataObjects
  • 5.获取到捕捉人脸相关信息:代理方法中可以获取
  • 6.对人脸数据的处理:将人脸框出来

THCameraController.m

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>

@interface THCameraController ()<AVCaptureMetadataOutputObjectsDelegate>

@property(nonatomic, strong)AVCaptureMetadataOutput *metadataOutput;

@end

@implementation THCameraController

// 创建session
- (BOOL)setupSessionOutputs:(NSError **)error {
    
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
       
    // 为session添加一个metadataOutput
    if ([self.captureSession canAddOutput: self.metadataOutput]) {
        [self.captureSession addOutput: self.metadataOutput];
        
        // 输出数据 --> 人脸数据
        // 优化:指定元数据类型,减少识别兴趣,人脸识别有兴趣
        NSArray *metadataObjectType = @[AVMetadataObjectTypeFace];
        self.metadataOutput.metadataObjectTypes = metadataObjectType;
        
        // 创建主队列: 人脸检测使用硬件加速器,任务需要在主线程执行
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        
        // 设置metadataOutput代理方法,检测视频中每一帧数据是否包含人脸数据,包含则调用回调方法
        [self.metadataOutput setMetadataObjectsDelegate: self queue: mainQueue];
        return YES;
    } else {
        // 打印错误信息
    }
    return NO;
}

// 代理方法:捕获到你设置元数据对象
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {

    // metadataObjects:包含捕获到人脸数据(人脸数据可能重复,人脸位置不变)
    // 使用循环,打印人脸数据
    for (AVMetadataFaceObject *face in metadataObjects) {
        // faceID, bounds
        NSLog(@"Face ID: %li", (long)face.faceID);
        NSLog(@"Face bounds %@", NSStringFromCGRect(face.bounds));
    }
    
    // 已经获取视频中的人脸个数、人脸位置,处理人脸
    // 在预览图层上进行处理:THPreviewView类
    // 通过代理将捕捉的人脸元数据传递给THPreviewView.m,将元数据转换为layer
    [self.faceDetectionDelegate didDetectFaces: metadataObjects];
}

@end

需要先进行一些必要的初始化 THPreviewView.m

- (void)setupView {
    
    // 初始化faceLayers 属性为字典
    self.faceLayers = [NSMutableDictionary dictionary];
    
    // 设置图层的填充方式videoGravity 使用AVLayerVideoGravityResizeAspectFill
    self.previewView.videoGravity = AVLayerVideoGravityResizeAspectFill;
    
    // 一般在previewLayer上添加一个透明的图层:初始化overlayLayer
    self.overLayer = [CALayer layer];
    self.overLayer.frame = self.bounds;
    
    //图层上的图形发生3D变换时,设置投影方式
    self.overLayer.sublayerTransform = CATransform3DMakePerspective(1000)
    [self.previewLayer addSublayer: self.overLayer];
}

static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) {
    
    // CATransform3D 图层的旋转、缩放、偏移、歪斜和应用的透视
    // CATransform3DIdentity时单元矩阵,该矩阵没有缩放、旋转等
    // CALayer 属于 CoreAnimation
    CATransform3D transform = CATransform3DIdentity;
    
    // 透视效果(近大远小),通过设置m34(-1.0/D)默认时0,D越小透视效果越明显
    // eyePosition 500-1000
    transform.m34 = -1.0 / eyePosition;
    
    return transform;
}

// 将检测到的人脸进行可视化
- (void)didDetectFace:(NSArray *)faces {
    
    // 1.创建一个本地数组保存转换后的人脸数据:人脸数据位置信息(摄像头坐标系) --> 屏幕坐标系
    NSArray *transformedFaces = [self transformedFacesFromFaces:faces];
    
    /*
      2.获取faceLayers的key,用于确定哪些人移除了视图并将对应的图层移除界面
      支持同时识别10个人脸
    */
    // 如果人脸从摄像头消失,要删除它的图层,通过faceID
    // 假设所有的人脸都需要删除,然后再从删除列表中一一移除
    NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
    
    // 3.遍历每个转换的人脸对象
    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;
        
        // 判断人脸对象是否具有有效的倾斜角
        layer.transform = CATransform3DIdentity;
        
        // 理解为人的头部向肩膀方向倾斜
        if (face.hasRollAngle) {
        
            // 如果为YES,则获取相应的CATransform3D值
            CATransform3D t = [self transformForRollAngle: face.rollAngle];
            
            // 将它与标识变化关联在一起,并设置transform属性
            // CATransform3DConcat 矩阵相乘
            layer.transform = CATransform3DConcat(layer.transform, t);
        }
       
        // 判断人脸对象是否具有有效的偏转角
        if (face.hasYawAngle) {
        
            // 获取相应的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];
    
    }
    
    // 人脸识别以后的
}

// 坐标转换
- (NSArray *)transformedFacesFromFaces:(NSArray *)faces {
    
    // 将摄像头的人脸数据转换为视图上的可展示的数据
    // 简单说:就是UIKit的坐标与摄像头坐标系统(0,0)-(1,1)不一样,需要转换
    // 转换需要考虑图层、镜像、视频重力、方向等因素,在iOS6.0之后才提供了方法
    NSMutableArray *transformFace = [NSMutableArray array];
    for (AVMetadataObject *face in faces) {
        AVMetadataObject *newFace = [self.previewLayer transformedMetadataObjectForMetadataObject: face];
        
        [transformFace addObject: newFace];
    }
    return transformFace;
}

//
- (CALayer *)makeFaceLayer {
    
    // 创建一个layer
    CALayer *layer = [CALayer layer];
    // 边框宽度为5.0f
    layer.borderWidth = 5.0f
    // 边框颜色为红色
    layer.borderColor = [UIColor redColor].CGColor;
    // 设置背景图片
    layer.contents = (id)[UIImage imageNamed:@"xxx.png"].CGImage;
    
    // 返回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)yarAngleInDegrees {
    
    // 将角度转换为弧度
    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);
}
欧拉角是什么?
欧拉角是由3个角组成,这3个角分别是Yaw、Pitch、Roll。Yaw表示绕Y轴旋转的角度,Pitch表示绕X轴旋转的角度,Roll表示绕Z轴旋转的角度
· Yaw偏移
· Pitch 投掷、倾斜、坠落
· Roll转动

image.png

三.AVFoundation二维码识别

分类

  • QR码:移动营销
  • Aztec码:登机牌
  • PDF417:商品运输

二维码识别部分代码实现

@protocol THCodeDetectionDelegate <NSObject>

- (void)didDetectCodes:(NSArray *)codes;

@end

THPreviewView.m

#import "THPreviewView.h"

@interface THPreviewView()<THCodeDetectionDelegate>
// 二维码图层
@property(strong,nonatomic)NSMutableDictionary *codeLayers;

@end

@implementation THPreviewView

- (void)setupView {
    
    // 保存一组表示识别编码的几何信息图层
    _codeLayers = [NSMutableDictionary dictionary];
    
    // 设置图层的videoGravity属性,保证宽高比在边界范围之内
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
}

- (AVCaptureSession *)session {
    return [[self previewLayer] session];
}

// 重写setSession 方法,将AVCaptureSession作为预览层的session属性
- (void)setSession:(AVCaptureSession *)session {
    self.previewLayer.session = session;
}

// 元数据转换
- (void)didDetectCodes:(NSArray *)codes {

    // 保存转换完成的元数据对象
    NSArray *transformedCodes = [self transformedCodesFromCodes: codes];
    
    // 从codeLayers字典中获得key,用来判断哪个图层应该在方法尾部移除
    NSMutableArray *lostCodes = [self.codeLayers.allKeys mutableCopy];
    
    // 遍历数组
    for (AVMetadataMachineReadableCodeObject *code in transformedCodes) {
        
        // 获得code.stringValue
        NSString *stringValue = code.stringValue;
        
        if (stringValue) {
            [lostCodes removeObject: stringValue];
        } else {
            continue;
        }
        
        // 根据当前的stringValue查找图层
        NSArray *layers = self.codeLayers[stringValue];
        
        // 如果没有对应的类目
        if (!layers) {
            
            // 新建图层: 方、圆
            layers = @[[self makeBoundsLayer],[self makeCornersLayer]];
            
            // 将图层以stringValue为key存入字典中
            self.codeLayers[stringValue] = layers;
            
            // 在预览图层上添加图层0、图层1
            [self.previewLayer addSublayer: layers[0]];
            [self.previewLayer addSublayer: layers[1]];
        }
        
        // 创建一个和对象的bounds关联的UIBezierPath
        // 画方框
        CAShapeLayer *boundsLayer = layers[0];
        boundsLayer.path = [self bezierPathForBounds: code.bounds].CGPath;
        
        // 对于cornersLayer构建一个CGPath
        CAShapeLayer *cornersLayer = layers[1];
        cornersLayer.path = [self bezierPathForCorners: code.corners].CGPath;
    }
    
    // 遍历lostCodes
    for (NSString *stringValue in lostCodes) {
        
        // 将里面的条目图层从previewLayer中移除
        for (CALayer *layer in self.codeLayers[stringValue]) {
            [layer removeFromSuperlayer];
        }
        
        // 数组条目中也要移除
        [self.codeLayers removeObjectForKey: stringValue];
    }
}

- (NSArray *)transformedCodesFromCodes:(NSArray *)codes {
    
    NSMutableArray *transformedCodes = [NSMutableArray array];
    
    // 遍历数组
    for (AVMetadataObject *code in codes) {
        
        // 将 设备坐标空间元数据对象 转化为 视图坐标空间对象
        AVMetadataObject *transformedCode = [self.previewLayer transformedMetadataObjectForMetadataObject: code];
        
        // 将转换好的数据添加到数组中
        [transformedCodes addObject: transformedCode];
    }
    
    // 返回已经处理好的数据
    return transformedCodes;
}

- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
    
    // 绘制一个方框
    return [UIBezierPath bezierPathWithOvalInRect: Bounds];
}

- (UIBezierPath *)bezierPathForCorners:(NSArray *)corners {

    // 创建一个空的UIBezierPath
    UIBezierPath *path = [UIBezierPath bezierPath];
    
    // 遍历数组中的条目,为每个条目构建一个CGPoint
    for (int i = 0; i < corners.count; i++) {
        CGPoint point = [self pointForCorner: corners[i]];
        
        if (i == 0) {
            [path moveToPoint: point];
        } else {
            [path addLineToPoint: point];
        }
    }
    
    [path closePath];
    return path;
}

// CAShapeLayer 是CALayer子类,用于绘制UIBezierPath,绘制bounds矩形
- (CAShapeLayer *)makeBoundsLayer {
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.lineWidth = 4.0f;
    shapeLayer.strokeColor = [UIColor colorWithRed: 0.95f green: 0.75f blue: 0.06f alpha: 1.0f].CGColor;
    shapeLayer.fillColor = nil;
    
    return shapeLayer;
}

// CAShapeLayer 是CALayer子类,用于绘制UIBezierPath,绘制corners路径
- (CAShapeLayer *)makeCornersLayer {
    
    CAShapeLayer *cornerLayer = [CAShapeLayer layer];
    cornerLayer.lineWidth = 2.0f;
    cornerLayer.strokeColor = [UIColor colorWithRed: 0.172f green: 0.671f blue: 0.48f alpha: 1.000f].CGColor;
    cornerLayer.fillColor = [UIColor colorWithRed: 0.190f green: 0.753f blue: 0.489f alpha: 0.5f].CGColor;
    
    return cornerLayer;
}


@end

有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍