本文主要内容
一.iOS上人脸识别的策略分析
二.AVFoundation人脸识别实现
三.AVFoundation二维码识别
一.iOS上人脸识别的策略分析
- CoreIamge
- face++(2014阿里收购,收费)
- OpenCV(图片处理,银行卡号、身份证号识别)
- libefacedetection(C++)
- AV Foundation(腾讯原生)
- vision(苹果模型:iOS11.0)
- 腾讯:优图项目组
人脸识别系统组成
二.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转动
复制代码
三.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
复制代码