IOS音视频:相机识别

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、AR
    • 1、概念
    • 2、设备追踪
    • 3、ARFrame
    • 4、场景解析
    • 5、SceneKit
  • 二、二维码识别
    • 1、设备的配置流程
    • 2、CameraController 中的二维码识别方法
    • 3、PreviewView中的二维码识别方法
  • 三、人脸识别
    • 1、人脸识别简介
    • 2、使用 CoreImage 实现静态人脸识别
    • 3、使用 Vision 实现静态人脸识别
    • 4、使用 OpenCV 实现静态人脸识别
    • 5、使用 AVFoundation 实现动态人脸识别
  • Demo
  • 参考文献

一、AR

1、概念

AR 全称 Augmented Reality(增强现实)是一种在视觉上呈现虚拟物体与现实场景结合的技术。Apple 公司在 2017 年正式推出了ARKit,iOS 开发者可以在这个平台上使用简单便捷的 API 来开发 AR 应用程序。为了获得 ARKit的完整功能,需要A9 及以上芯片。

AR 的全称是增强现实,通过显示屏与各类传感器,能让虚拟世界中创作出的物品与现实世界产生交互。

技术方面,现有 AR 解决方案已经可以检测并记录虚拟物品在现实环境中的准确位置、分辨虚拟物品与现实中物品的遮挡关系、为虚拟物体增加在现实中的光源与阴影、让虚拟物品与现实中的物品产生交互等。就像其它新技术一样,AR 的具体使用场景仍处于探索阶段,下面是我在关注的几个使用角度。

游戏 - 沉浸与代入感:说到虚拟现实的应用,许多人可能都会想到游戏。起先我并没太注意这些所谓 AR 游戏与直接在手机里玩有什么区别,直到前一阵子,精灵宝可梦支持了 AR 模式,允许你把宝可梦放在世界中

在真实世界中与宝可梦互动的体验让我意识到,在手机中与模型交互,给人直观的感觉始终是在和屏幕互动,无论模型和场景搭建的有多精致,用户感知到的始终只是屏幕。将模型放在现实世界中,无论你怎么移动手机,它还在那里。虽然仍旧摸不到,但能看到它出现在日常生活中,熟悉的现实里,带来的沉浸与代入感完全不同。

当我打开 AR 应用时,恍然意识到三维模型本就应该空间概念。建筑学用二维的三视图来避免图纸与模型潜在的不确定因素,呈现在空间中的三维模型则可以彻底避免这个问题,人可以自然地走动来确定所有细节。

若你将 AR 的技术开发进度想象成建造一辆车,ARKit 的定位便是配件,之后的所有功能都建立在这个基础之上。ARKit 中提供一些与 AR 相关的核心能力,如:提供景深信息来判断物体在空间中的位置、用空间锚点来记录虚拟物品在真实世界中的经纬度及海拔坐标、用摄像头对面部信息进行追踪等。下图中,图标下方的阴影及切换角度后物品位置不变的特性,便是 ARKit 提供的基础能力。

原深感镜头组

许多人了解这个镜头组件是因为 iPhone X 发布会上的面容 ID,替换了传统指纹识别来用于安全的验证身份。实际上原深感镜头组用途不仅于身份验证,它会投射 30,000 多个不可见的点来来获取准确的深度数据,这些数据对 AR 很有帮助。前置摄像头所做的摄像时背景替换,脸上试戴墨镜这类 AR 应用,许多都是通过 ARKit 调用原深感摄像头组件的硬件来实现的。

针对 AR 模型的技术准备 USDZ

市面上 3D 建模软件众多,各家选用的导出格式也差异非常大。为解决素材格式不统一的问题,Apple 与皮克斯合作推出了 USDZ 模型封装格式。此后 AR 场景所采用的素材均需为 USDZ ,避免了项目中各类文件混杂的情况。USDZ 自身也是个实力派,支持将模型的动画,纹理材质等众多与模型相关的内容,全部封装在一个文件中。

空间感知芯片 U 系列

U 系列是在 iPhone 设备中新增的超宽带 UWB 芯片,它能提供在较大空间内极度精准定位的能力。举个例子,若你的 AirPods Pro 找不到了,配备 U 系列芯片的手机可以准确指向该耳机的准确方向与距离。现有 AR 技术主要基于摄像头,当用户脱离摄像头区域后,便会丢失目标,若用户携带 Air Tag 定位器,则可能用 U 芯片进行定位,来实现许多之前无法做到的场景。虽 Apple 暂时还没开放它在 AR 场景的应用,但 U 系列芯片给开发者提供了许多想象力。

iOS 平台的 AR 应用通常由 ARKit 和渲染引擎两部分构成:

架构

ARKitARSession 负责管理每一帧的信息。ARSession 做了两件事,拍摄图像并获取传感器数据,对数据进行分析处理后逐帧输出。如下图:

ARSession

2、设备追踪

a、启动 ARSession

设备追踪确保了虚拟物体的位置不受设备移动的影响。在启动 ARSession时需要传入一个 ARSessionConfiguration的子类对象,以区别三种追踪模式:ARFaceTrackingConfigurationARWorldTrackingConfigurationAROrientationTrackingConfiguration。其中 ARFaceTrackingConfiguration 可以识别人脸的位置、方向以及获取拓扑结构。此外,还可以探测到预设的 52 种丰富的面部动作,如眨眼、微笑、皱眉等等。ARFaceTrackingConfiguration 需要调用支持 TrueDepth 的前置摄像头进行追踪。以ARWorldTrackingConfiguration为例进行追踪,获取特征点。

// 创建一个 ARSessionConfiguration
let configuration = ARWorldTrackingSessionConfiguration()

// Create a session
let session = ARSession()

// Run
session.run(configuration)
b、ARSession 底层如何进行世界追踪
  1. ARSession 底层使用了 AVCaputreSession 来获取摄像机拍摄的视频(一帧一帧的图像序列)。
  2. ARSession 底层使用了CMMotionManager 来获取设备的运动信息(比如旋转角度、移动距离等)
  3. ARSession 根据获取的图像序列以及设备的运动信息进行分析,最后输出 ARFrameARFrame 中就包含有渲染虚拟世界所需的所有信息。
ARSession 底层如何进行世界追踪
ARSession 底层如何进行世界追踪
c、追踪信息点

AR-World 的坐标系如下,当我们运行 ARSession 时设备所在的位置就是 AR-World 的坐标系原点。

AR-World的坐标系

在这个 AR-World 坐标系中,ARKit 会追踪以下几个信息:

  • 追踪设备的位置以及旋转,这里的两个信息均是相对于设备起始时的信息。
  • 追踪物理距离(以“米”为单位),例如 ARKit 检测到一个平面,我们希望知道这个平面有多大。
  • 追踪我们手动添加的希望追踪的点,例如我们手动添加的一个虚拟物体。
d、追踪如何工作

ARKit使用视觉惯性测距技术,对摄像头采集到的图像序列进行计算机视觉分析,并且与设备的运动传感器信息相结合。ARKit 会识别出每一帧图像中的特征点,并且根据特征点在连续的图像帧之间的位置变化,然后与运动传感器提供的信息进行比较,最终得到高精度的设备位置和偏转信息。

追踪如何工作

上图中划出曲线的运动的点代表设备,可以看到以设备为中心有一个坐标系也在移动和旋转,这代表着设备在不断的移动和旋转。这个信息是通过设备的运动传感器获取的。

动图中右侧的黄色点是 3D 特征点。3D特征点就是处理捕捉到的图像得到的,能代表物体特征的点。例如地板的纹理、物体的边边角角都可以成为特征点。上图中我们看到当设备移动时,ARKit 在不断的追踪捕捉到的画面中的特征点。

ARKit 将上面两个信息进行结合,最终得到了高精度的设备位置和偏转信息。

e、ARWorldTrackingConfiguration

ARWorldTrackingConfiguration 提供 6DoFSix Degree of Freedom)的设备追踪。包括三个姿态角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滚角),以及沿笛卡尔坐标系中 XYZ 三轴的偏移量。

ARWorldTrackingConfiguration

不仅如此,ARKit 还使用了 VIOVisual-Inertial Odometry)来提高设备运动追踪的精度。在使用惯性测量单元(IMU)检测运动轨迹的同时,对运动过程中摄像头拍摄到的图片进行图像处理。将图像中的一些特征点的变化轨迹与传感器的结果进行比对后,输出最终的高精度结果。

从追踪的维度和准确度来看,ARWorldTrackingConfiguration非常强悍。但它也有两个致命的缺点:受环境光线质量影响和受剧烈运动影响。由于在追踪过程中要通过采集图像来提取特征点,所以图像的质量会影响追踪的结果。在光线较差的环境下(比如夜晚或者强光),拍摄的图像无法提供正确的参考,追踪的质量也会随之下降。追踪过程中会逐帧比对图像与传感器结果,如果设备在短时间内剧烈的移动,会很大程度上干扰追踪结果。

e、追踪状态

世界追踪有三种状态,我们可以通过 camera.trackingState 获取当前的追踪状态。

追踪状态

从上图我们看到有三种追踪状态:

Not Available// 世界追踪正在初始化,还未开始工作
Normal// 正常工作状态
Limited// 限制状态,当追踪质量受到影响时,追踪状态可能会变为 Limited 状态

TrackingState 关联的一个信息是ARCamera.TrackingState.Reason,这是一个枚举类型:

case excessiveMotion// 设备移动过快,无法正常追踪
case initializing// 正在初始化
case insufficientFeatures// 特征过少,无法正常追踪
case none// 正常工作

我们可以通过 ARSessionObserver 协议去获取追踪状态的变化。


3、ARFrame

a、ARFrame

ARFrame 中包含有世界追踪过程获取的所有信息,ARFrame 中与世界追踪有关的信息主要是:anchorscamera

//camera: 含有摄像机的位置、旋转以及拍照参数等信息
var camera: [ARCamera]

//ahchors: 代表了追踪的点或面
var anchors: [ARAnchor]
b、ARAnchor

ARAnchor 是空间中相对真实世界的位置和角度。ARAnchor 可以添加到场景中,或是从场景中移除。基本上来说,它们用于表示虚拟内容在物理环境中的锚定,所以如果要添加自定义 anchor,添加到 session 里就可以了,它会在 session 生命周期中一直存在。但如果你在运行诸如平面检测功能,ARAnchor 则会被自动添加到 session 中。

要响应被添加的 anchor,可以从 current ARFrame 中获得完整列表,此列表包含 session 正在追踪的所有 anchor。或者也可以响应 delegate方法,例如 addupdate 以及 removesession 中的 anchor 被添加、更新或移除时会通知。

c、ARCamera
ARCamera

每个 ARFrame 都会包含一个 ARCameraARCamera 对象表示虚拟摄像头。虚拟摄像头就代表了设备的角度和位置。

  • ARCamera提供了一个transformtransform 是一个4x4矩阵。提供了物理设备相对于初始位置的变换。
  • ARCamera 提供了追踪状态(tracking state),通知你如何使用 transform
  • ARCamera 提供了相机内部功能(camera intrinsics)。包括焦距和主焦点,用于寻找投影矩阵。投影矩阵是 ARCamera 上的一个 convenience 方法,可用于渲染虚拟你的几何体。

4、场景解析

场景解析主要功能是对现实世界的场景进行分析,解析出比如现实世界的平面等信息,可以让我们把一些虚拟物体放在某些实物处。ARKit 提供的场景解析主要有平面检测、场景交互以及光照估计三种,下面逐个分析。

场景解析
a、平面检测(Plane detection)

ARKit 的平面检测用于检测出现实世界的水平面。

平面检测

❶ 上图中可以看出,ARkit 检测出了两个平面,图中的两个三维坐标系是检测出的平面的本地坐标系,此外,检测出的平面是有一个大小范围的。

❷ 平面检测是一个动态的过程,当摄像机不断移动时,检测到的平面也会不断的变化。下图中可以看到当移动摄像机时,已经检测到的平面的坐标原点以及平面范围都在不断的变化。

平面检测

❸ 开启平面检测很简单,只需要在 run ARSession 之前,将 ARSessionConfigurationplaneDetection 属性设为 true 即可。

// Create a world tracking session configuration.
let configuration = ARWorldTrackingSessionConfiguration()
configuration.planeDetection = .horizontal

// Create a session.
let session = ARSession()

// Run.
session.run(configuration)

平面的表示方式:ARKit检测到一个平面时,ARKit会为该平面自动添加一个 ARPlaneAnchor,这个 ARPlaneAnchor 就表示了一个平面。

❺ 当 ARKit 系统检测到新平面时,ARKit 会自动添加一个 ARPlaneAnchorARSession 中。我们可以通过 ARSessionDelegate 获取当前 ARSessionARAnchor 改变的通知,主要有以下三种情况:

新加入了 ARAnchor:对于平面检测来说,当新检测到某平面时,我们会收到该通知,通知中的 ARAnchor 数组会包含新添加的平面,其类型是 ARPlaneAnchor

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
    for anchor in anchors {
        if let anchor = anchor as? ARPlaneAnchor {
            print(anchor.center)
            print(anchor.extent)
        }
    }
}

ARAnchor更新:从上面我们知道当设备移动时,检测到的平面是不断更新的,当平面更新时,会回调这个接口。

func session(_ session: ARSession, didUpdate anchors: [ARAnchor])

删除 ARAnchor:当手动删除某个 Anchor 时就会回调此方法。此外,对于检测到的平面来说,如果两个平面进行了合并,则会删除其中一个,此时也会回调此方法。

func session(_ session: ARSession, didRemove anchors: [ARAnchor])
b、场景交互(Hit-testing)

Hit-testing 是为了获取当前捕捉到的图像中某点击位置有关的信息(包括平面、特征点、ARAnchor 等)。原理图如下:

场景交互

当点击屏幕时,ARKit会发射一个射线,假设屏幕平面是三维坐标系中的 xy 平面,那么该射线会沿着 z 轴方向射向屏幕里面,这就是一次 Hit-testing 过程。此次过程会将射线遇到的所有有用信息返回,返回结果以离屏幕距离进行排序,离屏幕最近的排在最前面。

ARFrame 提供了Hit-testing的接口:

func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]

上述接口中有一个 types 参数,该参数表示此次 Hit-testing 过程需要获取的信息类型,ResultType 有以下四种。

featurePoint:表示此次 Hit-testing 过程希望返回当前图像中Hit-testing 射线经过的 3D 特征点。

featurePoint

estimatedHorizontalPlane:表示此次 Hit-testing 过程希望返回当前图像中 Hit-testing 射线经过的预估平面。预估平面表示 ARKit 当前检测到一个可能是平面的信息,但当前尚未确定是平面,所以 ARKit 还没有为此预估平面添加 ARPlaneAnchor

estimatedHorizontalPlane

existingPlaneUsingExtent:表示此次Hit-testing过程希望返回当前图像中Hit-testing射线经过的有大小范围的平面。

existingPlaneUsingExtent

existingPlane:表示此次Hit-testing过程希望返回当前图像中Hit-testing射线经过的无限大小的平面。

existingPlane

上图中,平面大小是绿色平面所展示的大小,但exsitingPlane选项表示即使Hit-testing射线落在了绿色平面外面,也会将此平面返回。换句话说,将所有平面无限延展,只要 Hit-testing 射线经过了无限延展后的平面,就会返回该平面。

Demo演示:Hit-testingpoint(0.5, 0.5)代表屏幕的中心,屏幕左上角为(0, 0),右下角为(1, 1)。 对于 featurePointestimatedHorizontalPlane 的结果,ARKit没有为其添加ARAnchor,我们可以使用Hit-testing获取信息后自己为ARSession 添加 ARAnchor,下面代码就显示了此过程。

// Adding an ARAnchor based on hit-test
let point = CGPoint(x: 0.5, y: 0.5)  // Image center

// Perform hit-test on frame.
let results = frame. hitTest(point, types: [.featurePoint, .estimatedHorizontalPlane])

// Use the first result.
if let closestResult = results.first {
    // Create an anchor for it.
    anchor = ARAnchor(transform: closestResult.worldTransform)
    // Add it to the session.
    session.add(anchor: anchor)
}
c、光照估计(Light estimation)

下图中,一个虚拟物体茶杯被放在了现实世界的桌子上。

光照估计

当周围环境光线较好时,摄像机捕捉到的图像光照强度也较好,此时,我们放在桌子上的茶杯看起来就比较贴近于现实效果,如上图最左边的图。但是当周围光线较暗时,摄像机捕捉到的图像也较暗,如上图中间的图,此时茶杯的亮度就显得跟现实世界格格不入。

针对这种情况,ARKit 提供了光照估计,开启光照估计后,我们可以拿到当前图像的光照强度,从而能够以更自然的光照强度去渲染虚拟物体,如上图最右边的图。

光照估计基于当前捕捉到的图像的曝光等信息,给出一个估计的光照强度值(单位为lumen,光强单位)。默认的光照强度为 1000lumen,当现实世界较亮时,我们可以拿到一个高于 1000lumen 的值,相反,当现实世界光照较暗时,我们会拿到一个低于 1000lumen的值。

ARKit 的光照估计默认是开启的,当然也可以通过下述方式手动配置:

configuration.isLightEstimationEnabled = true

获取光照估计的光照强度也很简单,只需要拿到当前的 ARFrame,通过以下代码即可获取估计的光照强度:

let intensity = frame.lightEstimate?.ambientIntensity

5、SceneKit

a、简介

渲染是呈现AR world的最后一个过程。此过程将创建的虚拟世界、捕捉的真实世界、ARKit 追踪的信息以及 ARKit场景解析的的信息结合在一起,渲染出一个 AR world。渲染过程需要实现以下几点才能渲染出正确的 AR world

  • 将摄像机捕捉到的真实世界的视频作为背景。
  • 将世界追踪到的相机状态信息实时更新到 AR world 中的相机。
  • 处理光照估计的光照强度。
  • 实时渲染虚拟世界物体在屏幕中的位置。
SceneKit

如果我们自己处理这个过程,可以看到还是比较复杂的,ARKit为简化开发者的渲染过程,为开发者提供了简单易用的使用SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的视图ARSCNView以及ARSKView。当然开发者也可以使用其他引擎进行渲染,只需要将以上几个信息进行处理融合即可。

b、SceneKit 的坐标系

我们知道 UIKit使用一个包含有 xy信息的 CGPoint 来表示一个点的位置,但是在 3D 系统中,需要一个z参数来描述物体在空间中的深度,SceneKit 的坐标系可以参考下图:

SceneKit 的坐标系

这个三维坐标系中,表示一个点的位置需要使用(x,y,z)坐标表示。红色方块位于x轴,绿色方块位于 y轴,蓝色方块位于z轴,灰色方块位于原点。在SceneKit中我们可以这样创建一个三维坐标:

let position = SCNVector3(x: 0, y: 5, z: 10)
c、SceneKit 中的场景和节点

我们可以将 SceneKit 中的场景(SCNScene)想象为一个虚拟的 3D 空间,然后可以将一个个的节点(SCNNode)添加到场景中。SCNScene 中有唯一一个根节点(坐标是(x:0, y:0, z:0)),除了根节点外,所有添加到 SCNScene 中的节点都需要一个父节点。

下图中位于坐标系中心的就是根节点,此外还有添加的两个节点 NodeANodeB,其中 NodeA的父节点是根节点,NodeB 的父节点是 NodeA

SceneKit 中的场景和节点

SCNScene 中的节点加入时可以指定一个三维坐标(默认为(x:0, y:0, z:0)),这个坐标是相对于其父节点的位置。这里说明两个概念:

  • 本地坐标系:以场景中的某节点(非根节点)为原点建立的三维坐标系
  • 世界坐标系:以根节点为原点创建的三维坐标系称为世界坐标系。

上图中我们可以看到 NodeA 的坐标是相对于世界坐标系(由于NodeA的父节点是根节点)的位置,而NodeB 的坐标代表了 NodeBNodeA 的本地坐标系位置(NodeB 的父节点是NodeA)。

d、SceneKit 中的摄像机

有了 SCNSceneSCNNode 后,我们还需要一个摄像机(SCNCamera)来决定我们可以看到场景中的哪一块区域(就好比现实世界中有了各种物体,但还需要人的眼睛才能看到物体)。摄像机在 SCNScene 的工作模式如下图:

SceneKit 中的摄像机
  • SceneKitSCNCamera 拍摄的方向始终为 z 轴负方向。
  • 视野(Field of View)是摄像机的可视区域的极限角度。角度越小,视野越窄,反之,角度越大,视野越宽。
  • 视锥体(Viewing Frustum)决定着摄像头可视区域的深度(z 轴表示深度)。任何不在这个区域内的物体将被剪裁掉(离摄像头太近或者太远),不会显示在最终的画面中。

SceneKit中我们可以使用如下方式创建一个摄像机:

let scene = SCNScene()
let cameraNode = SCNNode()
let camera = SCNCamera()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(cameraNode)
e、SCNView

最后,我们需要一个 View 来将 SCNScene 中的内容渲染到显示屏幕上,这个工作由 SCNView 完成。这一步其实很简单,只需要创建一个SCNView 实例,然后将 SCNViewscene属性设置为刚刚创建的 SCNScene,然后将 SCNView 添加到UIKitviewwindow 上即可。示例代码如下:

let scnView = SCNView()
scnView.scene = scene
vc.view.addSubview(scnView)
scnView.frame = vc.view.bounds

二、二维码识别

运行效果
2020-12-21 16:11:05.525933+0800 CodeKamera[18444:3128121] <AVMetadataMachineReadableCodeObject: 二维码信息:0x281251580, type="org.iso.QRCode", bounds={ 0.1,0.0 0.2x0.3 }>corners { 0.1,0.3 0.4,0.3 0.4,0.0 0.2,0.0 }, time 441939661045708, stringValue "谢佳培"

1、设备的配置流程

默认使用后置摄像头进行扫描,使用AVMediaTypeVideo表示视频

self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

设备输入初始化

self.input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:nil];

设备输出初始化,并设置代理和回调。当设备扫描到数据时通过该代理输出队列,一般输出队列都设置为主队列,也是设置了回调方法执行所在的队列环境。

self.output = [[AVCaptureMetadataOutput alloc] init];
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

会话初始化,并设置采样质量为高

self.session = [[AVCaptureSession alloc] init];
[self.session setSessionPreset:AVCaptureSessionPresetHigh];

通过会话连接设备的输入输出

if ([self.session canAddInput:_input])
{
    [self.session addInput:_input];
}

if ([self.session canAddOutput:_output])
{
    [self.session addOutput:_output];
}

指定设备的识别类型,这里只指定二维码识别这一种类型 AVMetadataObjectTypeQRCode。指定识别类型这一步一定要在输出添加到会话之后,否则设备的可识别类型会为空,程序会出现崩溃。

[self.output setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];

设置扫描信息的识别区域,以下代码设置为正中央的一块正方形区域,识别区域越小识别效率越高,所以不设置整个屏幕。

[self.output setRectOfInterest:CGRectMake(x, y, width, height)];

预览层初始化,self.session负责驱动input进行信息的采集,layer负责把图像渲染显示。预览层的区域设置为整个屏幕,这样可以方便我们进行移动二维码到扫描区域。

self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
self.previewLayer.frame = self.view.bounds;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer addSublayer:self.previewLayer];

重写代理的回调方法,获取后置摄像头扫描到二维码的信息,实现我们在成功识别二维码之后要实现的功能。

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    // 停止扫描
    [self.session stopRunning];
    
    if (metadataObjects.count >= 1)
    {
        // 数组中包含的都是AVMetadataMachineReadableCodeObject类型的对象,该对象中包含解码后的数据
        AVMetadataMachineReadableCodeObject *QRObject = [metadataObjects lastObject];
        // 拿到扫描内容在这里进行个性化处理
        NSString *result = QRObject.stringValue;
        // 解析数据进行处理并实现相应的逻辑...
        NSLog(@"扫描到的二维码的信息:%@",result);
    }
}

2、CameraController 中的二维码识别方法

a、设置会话的输入设备

设置相机自动对焦,这样可以在任何距离都可以进行扫描。 因为扫描条码,距离都比较近,所以AVCaptureAutoFocusRangeRestrictionNear通过缩小距离,来提高识别成功率。

- (BOOL)setupSessionInputs:(NSError *__autoreleasing *)error
{
    //设置相机自动对焦,这样可以在任何距离都可以进行扫描
    BOOL success = [super setupSessionInputs:error];
    if(success)
    {
        //判断是否能自动聚焦
        if (self.activeCamera.autoFocusRangeRestrictionSupported)
        {
            //锁定设备
            if ([self.activeCamera lockForConfiguration:error])
            {
                self.activeCamera.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;
                
                //释放排他锁
                [self.activeCamera  unlockForConfiguration];
            }
        }
    }
    
    return YES;
}
b、设置会话输出设备
- (BOOL)setupSessionOutputs:(NSError **)error
{

    //获取输出设备
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    
    //判断是否能添加输出设备
    if ([self.captureSession canAddOutput:self.metadataOutput])
    {
        //添加输出设备
        [self.captureSession addOutput:self.metadataOutput];
        
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        
        //设置委托代理
        [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
        
        //指定扫描对是OR码(移动营销) & Aztec 码(登机牌)
        NSArray *types = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeAztecCode,AVMetadataObjectTypeDataMatrixCode,AVMetadataObjectTypePDF417Code];
        
        self.metadataOutput.metadataObjectTypes = types;
        
    }
    else
    {
        //错误时,存储错误信息
        NSDictionary *userInfo = @{NSLocalizedDescriptionKey:@"Faild to add metadata output."};
        *error = [NSError errorWithDomain:THCameraErrorDomain code:THCameraErrorFailedToAddOutput userInfo:userInfo];
    
        return NO;
    }
    
    return YES;
}
c、处理二维码的委托方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    if (metadataObjects.count > 0)
    {
        NSLog(@"二维码信息:%@",metadataObjects[0]);
    }

    //通过委托将二维码传递到preview
    [self.codeDetectionDelegate didDetectCodes:metadataObjects];
}

3、PreviewView中的二维码识别方法

- (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;
        }
        ......
    }
    
    //遍历lostCodes
    for (NSString *stringValue in lostCodes)
    {
        //将里面的条目图层从 previewLayer 中移除
        for (CALayer *layer in self.codeLayers[stringValue])
        {
            [layer removeFromSuperlayer];
        }
        
        //数组条目中也要移除
        [self.codeLayers removeObjectForKey:stringValue];
    }
    
}

根据当前的 stringValue 查找图层。如果没有对应的类目,则进行新建图层。

//根据当前的 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;

三、人脸识别

1、人脸识别简介

计算机视觉

计算机视觉指用摄影机和计算机代替人眼对目标进行识别、跟踪和测量等机器视觉,并进一步做图像处理。计算机视觉系统的具体实现方法由其功能决定——是预先固定的抑或是在运行过程中自动学习调整。尽管如此,计算机视觉的都需要具备以下处理步骤:

处理步骤
人脸识别

人脸识别是计算机视觉的一种应用,指利用分析比较人脸视觉特征信息进行身份鉴别的计算机技术,包括人脸图像采集、人脸定位、人脸识别预处理、身份确认以及身份查找等。iOS中常用的有四种实现方式:CoreImageVisionOpenCVAVFoundation


2、使用 CoreImage 实现静态人脸识别

CoreImage 实现静态人脸识别的关键类
CoreImage
操作部分
  • 滤镜(CIFliter):CIFilter产生一个CIImage,接受一到多的图片作为输入,经过一些过滤操作,产生指定输出的图片。
  • 检测(CIDetector):CIDetector检测处理图片的特性,如使用来检测图片中人脸的眼睛、嘴巴等等。
  • 特征(CIFeature):CIFeature 代表由 detector处理后产生的特征。
图像部分
  • 画布(CIContext):画布类可被用来处理Quartz 2D 或者OpenGL,也可以用它来关联CoreImage类,进行滤镜、颜色等渲染处理。
  • 颜色(CIColor): 图片的关联与画布、图片像素颜色的处理。
  • 向量(CIVector): 图片的坐标向量等几何方法处理。
  • 图片(CIImage):代表一个图像,可代表关联后输出的图像。

使用CoreImage 实现静态人脸识别的Demo演示
a、运行效果
2020-08-27 10:02:46.840694+0800 FaceRecognitionDemo[16278:796341] Metal API Validation Enabled
人脸区域为:Found bounds are (533.2980717893497, 906.6676903116021, 636.9456902844558, 625.2230840751323)
左眼位置: (747.0441848951896, 1391.4379881573832)
右眼位置: (1054.0997744993274, 1418.2982372168135)

b、检测人脸进行识别的方法
func detect()
{
    .....
}

获取人像图片

guard let personciImage = CIImage(image: personPic.image!) else { return }

设置人脸检测的精确度

let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]

进行人脸检测

let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)

在人像图片中锁定人脸,可能有多个人

let faces = faceDetector?.features(in: personciImage)

转换坐标系

let ciImageSize = personciImage.extent.size
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -ciImageSize.height)

c、针对每张人脸进行处理
for face in faces as! [CIFaceFeature]
{
    print("人脸区域为:Found bounds are \(face.bounds)")

    ....

    if face.hasLeftEyePosition
    {
        print("左眼位置: \(face.leftEyePosition)")
    }

    if face.hasRightEyePosition
    {
        print("右眼位置: \(face.rightEyePosition)")
    }
}

在图像视图中计算矩形的实际位置和大小

// 应用变换转换坐标
var faceViewBounds = face.bounds.applying(transform)

// 在图像视图中计算矩形的实际位置和大小
let viewSize = personPic.bounds.size
let scale = min(viewSize.width / ciImageSize.width, viewSize.height / ciImageSize.height)
let offsetX = (viewSize.width - ciImageSize.width * scale) / 2
let offsetY = (viewSize.height - ciImageSize.height * scale) / 2

faceViewBounds = faceViewBounds.applying(CGAffineTransform(scaleX: scale, y: scale))
faceViewBounds.origin.x += offsetX
faceViewBounds.origin.y += offsetY

添加人脸边框

let faceBox = UIView(frame: faceViewBounds)
faceBox.layer.borderWidth = 3
faceBox.layer.borderColor = UIColor.red.cgColor
faceBox.backgroundColor = UIColor.clear
personPic.addSubview(faceBox)

3、使用 Vision 实现静态人脸识别

Vision 实现静态人脸识别的关键类

Vision:是 Apple 在 WWDC 2017 推出的图像识别框架,它基于 Core ML
Vision支持多种图片类型:CIImageNSURLNSDataCGImageRefCVPixelBufferRef
Vision使用的角色有:RequestRequestHandlerresultsresults中的Observation数组。
Request类型:比如图中列出的 人脸识别、特征识别、文本识别、二维码识别等。

Vision结构

我们在使用过程中是给各种功能的 Request 提供给一个 RequestHandlerHandler 持有需要识别的图片信息,并将处理结果分发给每个 Requestcompletion Block 中,可以从 results 属性中得到 Observation 数组。

observations数组中的内容根据不同的request请求返回了不同的observation,如:VNFaceObservationVNTextObservationVNBarcodeObservationVNHorizonObservation,不同的Observation都继承于VNDetectedObjectObservation,而VNDetectedObjectObservation则是继承于VNObservation。每种ObservationboundingBoxlandmarks等属性,存储的是识别后物体的坐标,点位等,我们拿到坐标后,就可以进行一些UI绘制。


使用 Vision 实现静态人脸识别的Demo演示
a、运行效果
人脸识别
b、检测人脸
func detect()
{
    let handler = VNImageRequestHandler.init(cgImage: (imageView.image?.cgImage!)!, orientation: CGImagePropertyOrientation.up)
    
    // 创建检测人脸边框的请求
    let request = detectRequest()
    
    DispatchQueue.global(qos: .userInteractive).async {
        do
        {
            try handler.perform([request])
        }
        catch
        {
            print("出错了")
        }
    }
}
c、创建检测人脸边框的请求
func detectRequest() -> VNDetectFaceRectanglesRequest
{
    let request = VNDetectFaceRectanglesRequest { (request, error) in
        
        DispatchQueue.main.async {
            if let result = request.results
            {
                .......
            }
        }
    }
    return request
}

对结果进行旋转变换

let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -self.imageView!.frame.size.height)

对结果进行平移变换

let translate = CGAffineTransform.identity.scaledBy(x: self.imageView!.frame.size.width, y: self.imageView!.frame.size.height)

遍历所有识别结果,创建人脸边框,调整人脸边框位置,再将移动和旋转的仿射变化运用到边框上,即是人脸边框的最终位置。

//遍历所有识别结果
for item in result
{
    // 创建人脸边框
    let faceRect = UIView(frame: CGRect.zero)
    faceRect.layer.borderWidth = 3
    faceRect.layer.borderColor = UIColor.red.cgColor
    faceRect.backgroundColor = UIColor.clear
    self.imageView!.addSubview(faceRect)
    
    // 调整人脸边框位置
    if let faceObservation = item as? VNFaceObservation
    {
        // 将移动和旋转的仿射变化运用到边框上
        let finalRect = faceObservation.boundingBox.applying(translate).applying(transform)
        
        // 人脸边框的最终位置
        faceRect.frame = finalRect
    }
}

4、使用 OpenCV 实现静态人脸识别

OpenCV 的概念

OpenCV (Open Source Computer Vision Library)是一个在BSD许可下发布的开源库,因此它免费提供给学术和商业用途。有C++CPythonJava接口,支持WindowsLinuxMacOSiOSAndroid等系统。OpenCV是为计算效率而设计的,而且密切关注实时应用程序的发展和支持。该库用优化的C/C++编写,可以应用于多核处理。基于OpenCV,iOS应用程序可以实现很多有趣的功能,也可以把很多复杂的工作简单化。

  • 对图片进行灰度处理
  • 人脸识别,即特征跟踪
  • 训练图片特征库(可用于模式识别)
  • 提取特定图像内容(根据需求还原有用图像信息)

使用 OpenCV 实现静态人脸识别的关键类
  • core:简洁的核心模块,定义了基本的数据结构,包括稠密多维数组 Mat 和其他模块需要的基本函数。
  • imgproc:图像处理模块,包括线性和非线性图像滤波、几何图像转换 (缩放、仿射与透视变换、一般性基于表的重映射)、颜色空间转换、直方图等等。
  • video:视频分析模块,包括运动估计、背景消除、物体跟踪算法。
  • calib3d:包括基本的多视角几何算法、单体和立体相机的标定、对象姿态估计、双目立体匹配算法和元素的三维重建。
  • features2d:包含了显著特征检测算法、描述算子和算子匹配算法。
  • objdetect:物体检测和一些预定义的物体的检测 (如人脸、眼睛、杯子、人、汽车等)。
  • ml:多种机器学习算法,如 K 均值、支持向量机和神经网络。
  • highgui:一个简单易用的接口,提供视频捕捉、图像和视频编码等功能,还有简单的 UI 接口 (iOS 上可用的仅是其一个子集)。
  • gpuOpenCV中不同模块的 GPU 加速算法 (iOS 上不可用)。
  • ocl:使用 OpenCL 实现的通用算法 (iOS 上不可用)。
  • 一些其它辅助模块,如 Python 绑定和用户贡献的算法。

cv::MatOpenCV 的核心数据结构,用来表示任意 N 维矩阵。因为图像只是 2 维矩阵的一个特殊场景,所以也是使用 cv::Mat 来表示的。也就是说,cv::Mat 将是你在 OpenCV 中用到最多的类。

如前面所说,OpenCV 是一个 C++API,因此不能直接在 SwiftObjective-C 代码中使用,但能在 Objective-C++ 文件中使用。

Objective-C++Objective-CC++ 的混合物,让你可以在 Objective-C 类中使用 C++ 对象。clang 编译器会把所有后缀名为.mm 的文件都当做是 Objective-C++。一般来说,它会如你所期望的那样运行,但还是有一些使用 Objective-C++ 的注意事项。内存管理是你最应该格外注意的点,因为 ARC 只对 Objective-C 对象有效。当你使用一个 C++ 对象作为类属性的时候,其唯一有效的属性就是 assign。因此,你的 dealloc 函数应确保 C++ 对象被正确地释放了。

第二重要的点就是,如果你在 Objective-C++ 头文件中引入了 C++ 头文件,当你在工程中使用该 Objective-C++ 文件的时候就泄露了 C++ 的依赖。任何引入你的 Objective-C++ 类的 Objective-C 类也会引入该 C++ 类,因此该 Objective-C 文件也要被声明为 Objective-C++ 的文件。这会像森林大火一样在工程中迅速蔓延。所以,应该把你引入C++ 文件的地方都用 #ifdef __cplusplus 包起来,并且只要可能,就尽量只在.mm实现文件中引入 C++ 头文件。


使用 OpenCV 实现静态人脸识别的Demo演示
a、运行效果
人脸识别
b、导入头文件和扩展里面的属性
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif

#import "ViewController.h"
#import <opencv2/imgcodecs/ios.h>

using namespace cv;
using namespace std;

@interface ViewController ()
{
    CascadeClassifier _faceDetector;
    
    vector<cv::Rect> _faceRects;
    vector<cv::Mat> _faceImgs;
}

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end
b、viewDidLoad
- (void)viewDidLoad 
{
    [super viewDidLoad];
    
    //预设置face探测的参数
    [self preSetFace];
    
    //image转mat
    cv::Mat mat;
    UIImageToMat(self.imageView.image, mat);
    
    //执行face
    [self processImage:mat];
}
c、preSetFace
- (void)preSetFace 
{
    NSString *faceCascadePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_frontalface_alt2"
                                                                ofType:@"xml"];
    
    const CFIndex CASCADE_NAME_LEN = 2048;
    char *CASCADE_NAME = (char *) malloc(CASCADE_NAME_LEN);
    CFStringGetFileSystemRepresentation( (CFStringRef)faceCascadePath, CASCADE_NAME, CASCADE_NAME_LEN);
    
    _faceDetector.load(CASCADE_NAME);
    
    free(CASCADE_NAME);
}
d、processImage
- (void)processImage:(cv::Mat&)inputImage 
{
    // Do some OpenCV stuff with the image
    cv::Mat frame_gray;

    //转换为灰度图像
    cv::cvtColor(inputImage, frame_gray, CV_BGR2GRAY);
    
    //图像均衡化
    cv::equalizeHist(frame_gray, frame_gray);

    //分类器识别
    _faceDetector.detectMultiScale(frame_gray, _faceRects,1.1,2,0,cv::Size(30,30));

    vector<cv::Rect> faces;
    faces = _faceRects;
    
    // 在每个人脸上画一个红色四方形
    for(unsigned int i= 0;i < faces.size();I++)
    {
        const cv::Rect& face = faces[I];
        cv::Point tl(face.x,face.y);
        cv::Point br = tl + cv::Point(face.width,face.height);
        // 四方形的画法
        cv::Scalar magenta = cv::Scalar(255, 0, 0, 255);
        cv::rectangle(inputImage, tl, br, magenta, 3, 8, 0);
    }
    UIImage *outputImage = MatToUIImage(inputImage);
    self.imageView.image = outputImage;
}

使用 OpenCV 遇到的问题

无法将项目上传到Github上,因为OpenCL框架太大了,超出了上传的最大限制100MB,我尝试了两种方案来解决都无法顺利搞定,最后只能上传了一份不带该框架的源码Demo

xiejiapei@xiejiapeis-iMac dafsfs % git push
Enumerating objects: 286, done.
Counting objects: 100% (286/286), done.
Delta compression using up to 6 threads
Compressing objects: 100% (267/267), done.
Writing objects: 100% (283/283), 120.12 MiB | 331.00 KiB/s, done.
Total 283 (delta 59), reused 0 (delta 0)
remote: Resolving deltas: 100% (59/59), completed with 2 local objects.
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 7470123636eba7f03883923fcf95870e
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File OpenCVTest/Pods/OpenCV/opencv2.framework/Versions/A/opencv2 is 261.37 MB; this exceeds GitHub's file size limit of 100.00 MB
To https://github.com/xiejiapei-creator/dafsfs.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://github.com/xiejiapei-creator/dafsfs.git'

解决方案一:通过.gitignore不上传pod库,结果报同样的错。
解决方案二:当我拥有像opencv2这样的大型框架文件时,如何将更改推送到GitHub。谷歌上这类资料比较少且零散,在一篇日文博客上看到了类似情况,总结解决步骤如下:安装lfsgit install lfs),将大文件复制到仓库目录中,让lfs跟踪文件,然后,提交并推送。实践证明,该方案不可,真不可。劝你别瞎折腾了,很无用功很耗时。

xiejiapei@xiejiapeis-iMac DesignPatternsDemo % cd /Users/xiejiapei/Desktop/Github/Multi-MediaDemo
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git lfs install
Updated git hooks.
Git LFS initialized.
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git lfs track "Pods/OpenCV/opencv2.framework/Versions/A/opencv2"
Tracking "Pods/OpenCV/opencv2.framework/Versions/A/opencv2"
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git add .gitattributes
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git push

5、使用 AVFoundation 实现动态人脸识别

实现动态人脸识别的简介
a、运行效果

AVFoundation 支持最多同时识别10张人脸。


b、动态人脸识别的简介

AVFoundation框架搭建之初照片和视频捕捉功能就是它的强项。 通过一个特定的AVCaptureOutput类型的AVCaptureMetadataOutput可以实现人脸检测功能。支持硬件加速以及同时对10个人脸进行实时检测.

当使用人脸检测时,会输出一个具体的子类类型AVMetadataFaceObject,该类型定义了多个用于描述被检测到的人脸的属性,包括人脸的边界(设备坐标系)、斜倾角(roll angle:表示人头部向肩膀方向的侧倾角度)、偏转角(yaw angle:表示人脸绕Y轴旋转的角度)。


c、动态人脸识别的步骤
  1. 视频采集
  2. session添加一个元数据的输出AVCaptureMetadataOutput
  3. 设置元数据的范围(人脸数据、二维码数据....)
  4. 开始捕捉(设置捕捉完成代理)
  5. 在代理方法didoutputMetadataObjects中可以获取到捕捉人脸的相关信息,进行对人脸数据的处理

d、 AVFoundation实现视频捕捉的步骤

捕捉设备:AVCaptureDevice为摄像头、麦克风等物理设备提供接口。大部分我们使用的设备都是内置于MAC或者iPhoneiPad上的,当然也可能出现外部设备。AVCaptureDevice 针对物理设备提供了大量的控制方法,比如控制摄像头聚焦、曝光、白平衡、闪光灯等。

捕捉设备的输入:为捕捉设备添加输入,不能添加到AVCaptureSession 中,必须通过将它封装到一个AVCaptureDeviceInputs实例中,这个对象在设备输出数据和捕捉会话间扮演接线板的作用。

捕捉设备的输出:AVCaptureOutput 是一个抽象类,用于为捕捉会话得到的数据寻找输出的目的地。框架定义了一些抽象类的高级扩展类,例如使用 AVCaptureStillImageOutputAVCaptureMovieFileOutput类来捕捉静态照片、视频,使用 AVCaptureAudioDataOutputAVCaptureVideoDataOutput 来直接访问硬件捕捉到的数字样本。

捕捉连接:AVCaptureConnection类捕捉会话会先确定由给定捕捉设备输入渲染的媒体类型,并自动建立其到能够接收该媒体类型的捕捉输出端的连接。

捕捉预览:如果不能在影像捕捉中看到正在捕捉的场景,那么应用程序用户体验就会很差。幸运的是框架定义了AVCaptureVideoPreviewLayer类来满足该需求,这样就可以对捕捉的数据进行实时预览。


e、扩展里面的属性和委托
@interface THCameraController ()<AVCaptureMetadataOutputObjectsDelegate>

@property(nonatomic,strong)AVCaptureMetadataOutput  *metadataOutput;

@end

使用 AVFoundation 实现动态人脸识别简易版的Demo演示
a、导入框架和扩展中的属性
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()<AVCaptureMetadataOutputObjectsDelegate>

@property (nonatomic,strong) AVCaptureSession *session;
// 对捕捉的数据进行实时预览
@property (nonatomic,strong) AVCaptureVideoPreviewLayer *previewLayer;

// 所有脸庞
@property (nonatomic,copy) NSMutableArray *facesViewArray;

@end
b、viewDidLoad
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _facesViewArray = [NSMutableArray arrayWithCapacity:0];
    
    //1.获取输入设备(摄像头)
    NSArray *devices = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack].devices;
    AVCaptureDevice *deviceFace = devices[0];
    
    //2.根据输入设备创建输入对象
    AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:deviceFace error:nil];
    
    //3.创建原数据的输出对象
    AVCaptureMetadataOutput *metaout = [[AVCaptureMetadataOutput alloc] init];
    
    //4.设置代理监听输出对象输出的数据,在主线程中刷新
    [metaout setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    
    //5.设置输出质量(高像素输出)
    self.session = [[AVCaptureSession alloc] init];
    if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480])
    {
        [self.session setSessionPreset:AVCaptureSessionPreset640x480];
    }
    
    //6.添加输入和输出到会话
    [self.session beginConfiguration];
    if ([self.session canAddInput:input])
    {
        [self.session addInput:input];
    }
    if ([self.session canAddOutput:metaout])
    {
        [self.session addOutput:metaout];
    }
    [self.session commitConfiguration];
    
    //7.告诉输出对象要输出什么样的数据,识别人脸,最多可识别10张人脸
    [metaout setMetadataObjectTypes:@[AVMetadataObjectTypeFace]];
    
    //8.创建预览图层
    AVCaptureSession *session = (AVCaptureSession *)self.session;
    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    _previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:_previewLayer above:0];
    
    //9.设置有效扫描区域(默认整个屏幕区域)(每个取值0~1, 以屏幕右上角为坐标原点)
    metaout.rectOfInterest = self.view.bounds;
    
    //10.前置摄像头一定要设置一下,要不然画面是镜像
    for (AVCaptureVideoDataOutput *output in session.outputs)
    {
        for (AVCaptureConnection *connection in output.connections)
        {
            //判断是否是前置摄像头状态
            if (connection.supportsVideoMirroring)
            {
                //镜像设置
                connection.videoOrientation = AVCaptureVideoOrientationPortrait;
            }
        }
    }
    
    //11.开始扫描
    [self.session startRunning];
}
c、AVCaptureMetadataOutputObjectsDelegate
//当检测到了人脸会走这个回调
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    //移除旧画框
    for (UIView *faceView in self.facesViewArray)
    {
        [faceView removeFromSuperview];
    }
    [self.facesViewArray removeAllObjects];
    
    //将脸庞数据转换为脸图像,并创建脸方框
    for (AVMetadataFaceObject *faceobject in metadataObjects)
    {
        //转换为脸图像
        AVMetadataObject *face = [self.previewLayer transformedMetadataObjectForMetadataObject:faceobject];
        
        //创建脸方框
        UIView *faceBox = [[UIView alloc] initWithFrame:face.bounds];
        faceBox.layer.borderWidth = 3;
        faceBox.layer.borderColor = [UIColor redColor].CGColor;
        faceBox.backgroundColor = [UIColor clearColor];
        [self.view addSubview:faceBox];
        [self.facesViewArray addObject:faceBox];
    }
}

为捕捉会话添加输出设备

为捕捉会话添加输出设备,报错则打印错误信息。

- (BOOL)setupSessionOutputs:(NSError **)error
{
    self.metadataOutput = [[AVCaptureMetadataOutput alloc]init];
    if ([self.captureSession canAddOutput:self.metadataOutput])
    {
        [self.captureSession addOutput:self.metadataOutput];
        ......
        return YES;
    }
    else
    {
        //报错
        if (error)
        {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey:@"Failed to still image output"};
            
            *error = [NSError errorWithDomain:THCameraErrorDomain code:THCameraErrorFailedToAddOutput userInfo:userInfo];
            
        }
        return NO;
    }
}

设置 metadataObjectTypes 指定对象输出的元数据类型,支持多种元数据。限制检查到的元数据类型集合可以屏蔽我们不感兴趣的对象数量。这里只保留人脸元数据。

NSArray *metadatObjectTypes = @[AVMetadataObjectTypeFace];
self.metadataOutput.metadataObjectTypes = metadatObjectTypes;

创建主队列。因为人脸检测用到了硬件加速,而且许多重要的任务都在主线程中执行,所以需要为这次参数指定主队列。

dispatch_queue_t mainQueue = dispatch_get_main_queue();

通过设置AVCaptureVideoDataOutput的代理,就能获取捕获到一帧一帧数据,检测每一帧中是否包含人脸数据,如果包含则调用回调方法。

[self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];

捕捉到人脸数据调用代理方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    .......
}

MetadataObject包含了捕获到的人脸数据。人脸数据可能存在重复,因为人脸如果没有移动的话,下一秒还是会捕获到上一秒的信息。使用循环,打印人脸数据。

for (AVMetadataFaceObject *face in metadataObjects)
{
    NSLog(@"Face detected with ID:%li",(long)face.faceID);
    NSLog(@"Face bounds:%@",NSStringFromCGRect(face.bounds));
}

已经获取到视频中人脸的位置和个数,现在进行对人脸数据的处理。在预览图层 THPreviewView 上进行人脸的处理,所以需要通过代理将捕捉到的人脸元数据传递给 THPreviewView

@protocol THFaceDetectionDelegate <NSObject>

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

@end

@property (weak, nonatomic) id <THFaceDetectionDelegate> faceDetectionDelegate;

[self.faceDetectionDelegate didDetectFaces:metadataObjects];

@interface THPreviewView : UIView <THFaceDetectionDelegate>

创建人脸图层
- (void)setupView
{
    .....
}

初始化faceLayers属性,其为一个字典,用来存储每张人脸的图层,faceID 和人脸图层一一对应。

self.faceLayers = [NSMutableDictionary dictionary];

设置 videoGravity。使用 AVLayerVideoGravityResizeAspectFill 铺满整个预览层的边界范围。

self.overlayLayer = [CALayer layer];

一般在 previewLayer 上添加一个透明的图层。设置它的framepreviewLayerbounds相同。

self.overlayLayer = [CALayer layer];
self.overlayLayer.frame = self.bounds;

假设你的图层上的图像会发生3D变换,则需要设置其投影方式。

self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);

将子图层添加到预览图层来。

[self.previewLayer addSublayer:self.overlayLayer];

将检测到的人脸进行可视化
- (void)didDetectFaces:(NSArray *)faces
{
    .......
}

获取到的人脸数据位置信息是摄像头坐标系的,需要将其转化为屏幕坐标系。创建一个本地数组,保存转换后的人脸数据。

NSArray *transformedFaces = [self transformedFacesFromFaces:faces];

如果这个人脸从摄像头中消失掉了,则需要根据faceID删除掉它的图层。因为支持同时识别多张人脸,所以可以获取faceLayersallKey,假定刚开始所有的人脸都需要删除,然后再一一从删除列表中进行移除。

NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];

遍历每个转换的人脸对象,获取其关联的faceID。这个属性唯一标识一个检测到的人脸。

for (AVMetadataFaceObject *face in transformedFaces)
{
    NSNumber *faceID = @(face.faceID);
    .....
}

如果faceID存在,则表示人脸没有从摄像头中移除,不需要进行删除,将其从待移除人脸列表中移出。

[lostFaces removeObject:faceID];

如果是之前旧的人脸,则从人脸图层字典中获取到旧的人脸图层。拿到当前faceID对应的layer

CALayer *layer = self.faceLayers[faceID];

如果给定的faceID没有找到对应的图层,说明是新的人脸,需要创建人脸图层,再将新的人脸图层添加到 overlayLayer上,最后将layer加入到字典中,更新字典。

if (!layer)
{
    //调用makeFaceLayer 创建一个新的人脸图层
    layer = [self makeFaceLayer];
    
    //将新的人脸图层添加到 overlayLayer上
    [self.overlayLayer addSublayer:layer];
    
    //将layer加入到字典中
    self.faceLayers[faceID] = layer;
}

图层的大小 = 人脸的大小,所以根据人脸的bounds设置layerframe

layer.frame = face.bounds;

人脸识别是3D动态识别,属于立体图像,涉及到欧拉角。欧拉角的Yaw表示绕Y轴旋转的角度(人脸左右旋转),Pitch表示绕X轴旋转角度(人脸左右摇摆),Roll表示绕Z轴旋转的角度(人脸上下摇摆)。设置图层的transform属性为 CATransform3DIdentity,使用Core Animation对图层进行变化。

layer.transform = CATransform3DIdentity;

判断人脸对象是否具有有效的斜倾交(人的头部向肩部方向倾斜)。如果为YES,则获取相应的CATransform3D值,将它与标识变化关联在一起,并设置transform属性。实现当人脸倾斜的时候,捕获人脸的红框也发生同样的倾斜。

if (face.hasRollAngle)
{
    //如果为YES,则获取相应的CATransform3D 值
    CATransform3D t = [self transformForRollAngle:face.rollAngle];
    
    //将它与标识变化关联在一起,并设置transform属性
    layer.transform = CATransform3DConcat(layer.transform, t);
}

判断人脸对象是否具有有效的偏转角。如果为YES,则获取相应的 CATransform3D 值,再将它与标识变化关联在一起,并设置transform属性。

if (face.hasYawAngle)
{
    //如果为YES,则获取相应的CATransform3D 值
    CATransform3D  t = [self transformForYawAngle:face.yawAngle];
    
    //将它与标识变化关联在一起,并设置transform属性
    layer.transform = CATransform3DConcat(layer.transform, t);
}

处理那些已经从镜头中消失的人脸图层。遍历数组,将剩下的人脸ID集合从上一个图层和faceLayers字典中移除。

for (NSNumber *faceID in lostFaces)
{
    CALayer *layer = self.faceLayers[faceID];
    [layer removeFromSuperlayer];
    [self.faceLayers  removeObjectForKey:faceID];
}

辅助方法
❶ 摄像头坐标系转化为屏幕坐标系

将摄像头的人脸数据转换为视图上的可展示的数据。因为UIKit的坐标与摄像头坐标系统(0,0)-(1,1)不一样,所以需要转换,转换成功后,加入到数组中。

- (NSArray *)transformedFacesFromFaces:(NSArray *)faces
{
    NSMutableArray *transformeFaces = [NSMutableArray array];
    
    for (AVMetadataObject *face in faces)
    {
        AVMetadataObject *transformedFace = [self.previewLayer transformedMetadataObjectForMetadataObject:face];
        [transformeFaces addObject:transformedFace];
    }
    
    return transformeFaces;
}
❷ 调用makeFaceLayer 创建一个新的人脸图层
- (CALayer *)makeFaceLayer
{
    //创建一个layer
    CALayer *layer = [CALayer layer];
    
    //边框宽度为5.0f
    layer.borderWidth = 5.0f;
    
    //边框颜色为红色
    layer.borderColor = [UIColor redColor].CGColor;
    
    // 为view添加背景图片
    layer.contents = (id)[UIImage imageNamed:@"551.png"].CGImage;
    
    //返回layer
    return layer;
}
❸ 将投影方式修改为透视投影
self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);

static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition)
{
    //CATransform3D 图层的旋转,缩放,偏移,歪斜
    //CATransform3DIdentity是单位矩阵,该矩阵没有缩放,旋转,歪斜,透视。该矩阵应用到图层上,就是设置默认值
    CATransform3D transform = CATransform3DIdentity;
    
    
    //透视效果(就是近大远小),是通过设置m34 m34 = -1.0/D 默认是0.D越小透视效果越明显
    //D:eyePosition 观察者到投射面的距离
    transform.m34 = -1.0/eyePosition;
    
    return transform;
}
❹ 为设备方向计算一个相应的旋转变换
- (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);
}
❺ 将 RollAngle 的 rollAngleInDegrees 值转换为 CATransform3D
static CGFloat THDegreesToRadians(CGFloat degrees)
{
    return degrees * M_PI / 180;
}

- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees
{
    //将人脸对象得到的RollAngle 单位“度” 转为Core Animation需要的弧度值
    CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);

    //rollAngle围绕Z轴旋转
    //将结果赋给CATransform3DMakeRotation x,y,z轴为0,0,1 得到绕Z轴倾斜角旋转转换
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}
❻ 将 YawAngle 的 yawAngleInDegrees 值转换为 CATransform3D

YawAngleyawAngleInDegrees 值转换为 CATransform3D。将角度转换为弧度值,再将弧度值结果进行CATransform3DMakeRotationx,y,z轴为0,-1,0 得到绕Y轴旋转结果。因为应用程序的界面固定为垂直方向,所以需要为设备方向计算一个相应的旋转变换,最后将设备的旋转变换和人脸的旋转变化进行矩阵相乘得到最后结果,如果不这样,会造成人脸图层的偏转效果不正确。

- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees
{
    CGFloat yawAngleInRaians = THDegreesToRadians(yawAngleInDegrees);
    CATransform3D yawTransform = CATransform3DMakeRotation(yawAngleInRaians, 0.0f, -1.0f, 0.0f);
    return CATransform3DConcat(yawTransform, [self orientationTransform]);
}

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

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

推荐阅读更多精彩内容