Vision框架详细解析(十二) —— 基于Vision的Face Detection新特性(一)

版本记录

版本号 时间
V1.0 2022.02.26 星期六

前言

iOS 11+macOS 10.13+ 新出了Vision框架,提供了人脸识别、物体检测、物体跟踪等技术,它是基于Core ML的。可以说是人工智能的一部分,接下来几篇我们就详细的解析一下Vision框架。感兴趣的看下面几篇文章。
1. Vision框架详细解析(一) —— 基本概览(一)
2. Vision框架详细解析(二) —— 基于Vision的人脸识别(一)
3. Vision框架详细解析(三) —— 基于Vision的人脸识别(二)
4. Vision框架详细解析(四) —— 在iOS中使用Vision和Metal进行照片堆叠(一)
5. Vision框架详细解析(五) —— 在iOS中使用Vision和Metal进行照片堆叠(二)
6. Vision框架详细解析(六) —— 基于Vision的显著性分析(一)
7. Vision框架详细解析(七) —— 基于Vision的显著性分析(二)
8. Vision框架详细解析(八) —— 基于Vision的QR扫描(一)
9. Vision框架详细解析(九) —— 基于Vision的QR扫描(二)
10. Vision框架详细解析(十) —— 基于Vision的Body Detect和Hand Pose(一)
11. Vision框架详细解析(十一) —— 基于Vision的Body Detect和Hand Pose(二)

开始

首先看下主要内容

了解Face Detection的新增功能以及Vision框架的最新添加如何帮助您在图像分割和分析中获得更好的结果。内容来自翻译。

接着看下写作环境

Swift 5.5, iOS 15, Xcode 13

下面就是正文了

拍护照照片很痛苦。 有很多规则要遵守,很难知道你的照片是否会被接受。 幸运的是,您生活在 21 世纪! 通过使用 Vision 框架中的面部检测来控制您的护照照片体验。 在将照片发送到护照办公室之前,请了解您的照片是否可以接受!

注意:本教程的项目需要访问相机。 它只能在真机上运行,不能在模拟器上运行。 此外,还假设了 Vision 框架的一些现有知识。 如果您以前从未使用过 Vision 框架,您可能希望从这个较早的教程earlier tutorial开始。

在本教程中,您将:

  • Learn how to detect roll, pitch and yaw of faces.
  • Add quality calculation to face detection.
  • Use person segmentation to mask an image

让我们开始吧!

打开起始材料包含一个名为 PassportPhotos 的项目。 在本教程中,您将构建一个简单的拍照应用程序,该应用程序仅在生成的图像对护照照片有效时才允许用户拍照。 您将遵循的有效性规则适用于英国护照照片,但很容易复制到任何其他国家/地区。

打开启动项目,从可用的运行targets中选择您的手机并构建并运行。

注意:您可能需要设置 Xcode 来签署应用程序,然后才能在您的设备上运行它。 最简单的方法是为您的target打开 Signing & Capabilities 编辑器。 然后,选择Automatically Manage Signing

该应用程序显示前置摄像头视图。 红色矩形和绿色椭圆形覆盖在屏幕中央。 顶部的横幅包含说明。 底部横幅包含控制。

中央按钮是用于拍照的快门释放按钮。 在左侧,顶部按钮切换背景,底部(由瓢虫表示)打开和关闭调试视图。 快门右侧的按钮是一个占位符,替换为最近一张照片的缩略图。

把手机举到你面前。 黄色边界框开始跟踪您的脸部。 一些人脸检测已经在进行中!

1. A Tour of the App

现在让我们浏览一下该应用程序,让您了解方向。

Xcode 中,打开 PassportPhotosAppView.swift。这是应用程序的根视图。它包含一堆视图。 CameraView 位于底部。然后,一个 LayoutGuide 视图(在屏幕上绘制绿色椭圆)和一个可选的 DebugView。最后,CameraOverlayView 位于顶部。

该应用程序中还有一些其他文件。其中大部分是用于 UI 各个部分的简单视图。在本教程中,您将主要更新三个类:CameraViewModel、CameraViewController UIKit 视图控制器和 FaceDetector

打开 CameraViewModel.swift。此类控制整个应用程序的状态。它定义了应用程序中的视图可以订阅的一些已发布属性。视图可以通过调用单个公共方法——perform(action:)来更新应用程序的状态。

接下来,打开 CameraView.swift。这是一个简单的 SwiftUI UIViewControllerRepresentable结构。它用 FaceDetector 实例化一个 CameraViewController

现在打开 CameraViewController.swiftCameraViewController 配置和控制 AV capture session。这会将像素从相机绘制到屏幕上。在 viewDidLoad() 中,设置了人脸检测器对象的代理。然后它配置并启动AV capture sessionconfigureCaptureSession()执行大部分设置。这是您之前希望看到的所有基本设置代码。

该类还包含一些与设置 Metal相关的方法。暂时不要担心这个。

最后,打开 FaceDetector.swift。这个实用程序类只有一个目的——成为 CameraViewControllerAVCaptureVideoDataOutput 设置的代理。这就是面部检测魔术发生的地方。更多关于这下面。

随意浏览应用程序的其余部分。


Reviewing the Vision Framework

Vision 框架自 iOS 11 以来一直存在。它提供了对图像和视频执行各种计算机视觉算法的功能。例如人脸landmark检测、文本检测、条码识别等。

iOS 15 之前,Vision 框架允许您查询检测到的人脸的roll and yaw。它还提供了某些landmark的位置,例如眼睛、耳朵和鼻子。这方面的一个例子已经在应用程序中实现。

打开 FaceDetector.swift 并找到 captureOutput(_:didOutput:from:)。在此方法中,面部检测器在 AVCaptureSession 提供的图像缓冲区上设置 VNDetectFaceRectanglesRequest

当检测到人脸矩形时,将调用完成处理程序completion -detectedFaceRectangles(request:error:)。该方法从人脸观察结果中拉出人脸的边界框,并对CameraViewModel执行faceObservationDetected` 动作。


Looking Forward

是时候添加你的第一段代码了!

护照规定要求人们直视相机。是时候添加这个功能了。打开CameraViewModel.swift

找到 FaceGeometryModel结构体定义。通过添加以下新属性来更新结构体:

let roll: NSNumber
let pitch: NSNumber
let yaw: NSNumber

此更改允许您在视图模型中存储在面部检测到的roll, pitch and yaw值。

在类顶部的 hasDetectedValidFace 属性下,添加以下新的已发布属性:

@Published private(set) var isAcceptableRoll: Bool {
  didSet {
    calculateDetectedFaceValidity()
  }
}
@Published private(set) var isAcceptablePitch: Bool {
  didSet {
    calculateDetectedFaceValidity()
  }
}
@Published private(set) var isAcceptableYaw: Bool {
  didSet {
    calculateDetectedFaceValidity()
  }
}

这增加了三个新属性来存储检测到的面部的roll, pitch and yaw是否可以用于护照照片。 当每一个更新时,都会调用calculateDetectedFaceValidity()方法。

接下来,将以下内容添加到 init() 的底部:

isAcceptableRoll = false
isAcceptablePitch = false
isAcceptableYaw = false

这只是设置您刚刚添加的属性的初始值。

现在,找到 invalidateFaceGeometryState()方法。 目前是一个stub。 将以下代码添加到该函数中:

isAcceptableRoll = false
isAcceptablePitch = false
isAcceptableYaw = false

因为没有检测到面部,所以您将可接受的roll, pitch and yaw值设置为 false

1. Processing Faces

接下来,通过将 faceFound() case 替换为以下内容来更新 processUpdatedFaceGeometry()

case .faceFound(let faceGeometryModel):
  let roll = faceGeometryModel.roll.doubleValue
  let pitch = faceGeometryModel.pitch.doubleValue
  let yaw = faceGeometryModel.yaw.doubleValue
  updateAcceptableRollPitchYaw(using: roll, pitch: pitch, yaw: yaw)

在这里,您将检测到的面部的roll, pitch and yaw值从 faceGeometryModel 中提取为双精度值。 然后将这些值传递给 updateAcceptableRollPitchYaw(using:pitch:yaw:)

现在将以下内容添加到 updateAcceptableRollPitchYaw(using:pitch:yaw:) 的实现存根中:

isAcceptableRoll = (roll > 1.2 && roll < 1.6)
isAcceptablePitch = abs(CGFloat(pitch)) < 0.2
isAcceptableYaw = abs(CGFloat(yaw)) < 0.15

在这里,您根据从面部提取的值设置可接受的roll, pitch and yaw状态。

最后,替换 calculateDetectedFaceValidity() 以使用 roll、pitch 和 yaw 值来确定面部是否有效:

hasDetectedValidFace =
  isAcceptableRoll &&
  isAcceptablePitch &&
  isAcceptableYaw

现在,打开 FaceDetector.swift。 在 detectedFaceRectangles(request:error:) 中,将 faceObservationModel 的定义替换为以下内容:

let faceObservationModel = FaceGeometryModel(
  boundingBox: convertedBoundingBox,
  roll: result.roll ?? 0,
  pitch: result.pitch ?? 0,
  yaw: result.yaw ?? 0
)

这只是将现在需要的roll, pitch and yaw参数添加到 FaceGeometryModel对象的初始化中。

2. Debug those Faces

最好将一些有关roll, pitch and yaw的信息添加到调试视图中,这样您就可以在使用应用程序时看到这些值。

打开 DebugView.swift 并将body声明中的 DebugSection 替换为:

DebugSection(observation: model.faceGeometryState) { geometryModel in
  DebugText("R: \(geometryModel.roll)")
    .debugTextStatus(status: model.isAcceptableRoll ? .passing : .failing)
  DebugText("P: \(geometryModel.pitch)")
    .debugTextStatus(status: model.isAcceptablePitch ? .passing : .failing)
  DebugText("Y: \(geometryModel.yaw)")
    .debugTextStatus(status: model.isAcceptableYaw ? .passing : .failing)
}

这已更新调试文本以将当前值打印到屏幕上,并根据该值是否可接受来设置文本颜色。

构建并运行。

直视相机,注意椭圆是绿色的。 现在从一边到另一边旋转你的头,注意当你不直看相机时椭圆是如何变成红色的。 如果您打开了调试模式,请注意yaw数如何改变值和颜色。


Selecting a Size

接下来,您希望应用程序检测一张脸在照片框架内的大小。 打开 Camera ViewModel.swift 并在 isAcceptableYaw 声明下添加以下属性:

@Published private(set) var isAcceptableBounds: FaceBoundsState {
  didSet {
    calculateDetectedFaceValidity()
  }
}

然后,在 init() 的底部设置该属性的初始值:

isAcceptableBounds = .unknown

和之前一样,将以下内容添加到 invalidateFaceGeometryState() 的末尾:

isAcceptableBounds = .unknown

接下来,在 processUpdatedFaceGeometry() 中,将以下内容添加到 faceFound case的末尾:

let boundingBox = faceGeometryModel.boundingBox
updateAcceptableBounds(using: boundingBox)

然后用以下代码填写 updateAcceptableBounds(using:) 的存根:

// 1
if boundingBox.width > 1.2 * faceLayoutGuideFrame.width {
  isAcceptableBounds = .detectedFaceTooLarge
} else if boundingBox.width * 1.2 < faceLayoutGuideFrame.width {
  isAcceptableBounds = .detectedFaceTooSmall
} else {
  // 2
  if abs(boundingBox.midX - faceLayoutGuideFrame.midX) > 50 {
    isAcceptableBounds = .detectedFaceOffCentre
  } else if abs(boundingBox.midY - faceLayoutGuideFrame.midY) > 50 {
    isAcceptableBounds = .detectedFaceOffCentre
  } else {
    isAcceptableBounds = .detectedFaceAppropriateSizeAndPosition
  }
}

使用此代码,您可以:

  • 1) 首先检查人脸的边界框是否与布局指南的宽度大致相同。
  • 2) 然后,检查人脸的边界框是否在框架中大致居中。

如果这两项检查都通过,则将 isAcceptableBounds 设置为 FaceBoundsState.detectedFaceApppropriateSizeAndPosition。 否则,将其设置为相应的错误情况。

最后,将 calculateDetectedFaceValidity() 更新为如下所示:

hasDetectedValidFace =
  isAcceptableBounds == .detectedFaceAppropriateSizeAndPosition &&
  isAcceptableRoll &&
  isAcceptablePitch &&
  isAcceptableYaw

这增加了对边界是否可接受的检查。

构建并运行。 将手机靠近和远离您的脸部,并注意椭圆形如何改变颜色。


Detecting Differences

目前,FaceDetector 正在使用 VNDetectFaceRectanglesRequestRevision2 检测人脸矩形。 iOS 15 引入了一个新版本,VNDetectFaceRectanglesRequestRevision3。那么有什么区别呢?

Version 3 为检测面部矩形提供了许多有用的更新,包括:

  • 1) 现在确定检测到的面部的pitch。您可能没有注意到,但到目前为止,pitch的值始终为 0,因为它不存在于面部观察中。
  • 2) Roll, pitch and yaw值在连续空间中报告。使用 VNDetectFaceRectanglesRequestRevision2roll and yaw仅在discrete bins内提供。您可以使用应用程序内左右转动头部自己观察这一点。yaw总是在 0±0.785 弧度之间跳跃。
  • 3) 在检测人脸landmarks时,可以准确地检测到瞳孔的位置。以前,即使向外看脸的一侧,瞳孔也会设置在眼睛的中心。

是时候更新应用程序以使用 VNDetectFaceRectanglesRequestRevision3。您将利用检测到的音高并观察连续的空间更新。

打开 FaceDetector.swift。在 captureOutput(_:didOutput:from:) 中,将 detectFaceRectanglesRequestrevision属性更新为revision 3

detectFaceRectanglesRequest.revision = VNDetectFaceRectanglesRequestRevision3

构建并运行。

将手机举到脸前。 请注意调试输出中打印的值如何在每一帧上更新。 低头(下巴放在胸前)。 请注意pitch数字也是如何更新的。


Masking Mayhem

除非您一直生活在岩石下,否则您一定注意到越来越多的人戴着口罩。这对于对抗 COVID 非常有用,但对于人脸识别却很糟糕!

幸运的是,Apple 支持您。借助 VNDetectFaceRectanglesRequestRevision3Vision 框架现在可以检测被口罩覆盖的人脸。虽然这对于通用人脸检测很有用,但对于您的护照照片应用程序来说却是一场灾难。护照照片绝对不允许戴口罩!那么如何防止戴口罩的人拍照呢?

幸运的是,Apple 还提高了面部捕捉质量。面部捕捉质量为检测到的面部提供分数。它考虑了光照、遮挡、模糊等属性。

请注意,质量检测会将同一主题与自身的副本进行比较。它不会将一个人与另一个人进行比较。捕获质量在 01 之间变化。iOS 15 中的最新版本是 VNDetectFaceCaptureQualityRequestRevision2


Assuring Quality

在请求质量分数之前,您的应用需要一个地方来存储当前帧的质量。首先,更新模型以保存有关面部质量的信息。

打开 CameraViewModel.swift。在 FaceGeometryModel 结构体下,添加以下内容以存储质量状态:

struct FaceQualityModel {
  let quality: Float
}

此结构体包含一个浮点属性,用于存储最近检测到的质量。

faceGeometryState的声明下,添加一个发布人脸质量状态的属性:

// 1
@Published private(set) var faceQualityState: FaceObservation {
  didSet {
    // 2
    processUpdatedFaceQuality()
  }
}
  • 1) 这遵循类似于上面的 faceGeometryState 属性的模式。 FaceObservation 枚举包装了底层模型值。 FaceObservation 是一个提供类型安全的通用包装器。 它包含三种状态:face found, face not found and error
  • 2) faceQualityState 的更新调用 processUpdatedFaceQuality()

不要忘记在 init()中初始化 faceQualityState

faceQualityState = .faceNotFound

这会将 faceQualityState 的初始值设置为 .faceNotFound

接下来,添加一个新的已发布属性以获得可接受的质量:

@Published private(set) var isAcceptableQuality: Bool {
  didSet {
    calculateDetectedFaceValidity()
  }
}

与其他属性一样,在 init()方法中对其进行初始化:

isAcceptableQuality = false

现在,您可以编写 processUpdatedFaceQuality() 的实现:

switch faceQualityState {
case .faceNotFound:
  isAcceptableQuality = false
case .errored(let error):
  print(error.localizedDescription)
  isAcceptableQuality = false
case .faceFound(let faceQualityModel):
  if faceQualityModel.quality < 0.2 {
    isAcceptableQuality = false
  }

  isAcceptableQuality = true
}

在这里,您列举了 FaceObservation 的不同状态。 可接受的质量得分为 0.2 或更高。

更新 calculateDetectedFaceValidity() 以考虑可接受的质量,将最后一行替换为:

isAcceptableYaw && isAcceptableQuality

1. Handling Quality Result

faceQualityState 属性现在设置为存储检测到的人脸质量。 但是,没有任何方法可以更新该状态。 是时候解决这个问题了。

CameraViewModelAction 枚举中,在 faceObservationDetected 之后添加一个新动作:

case faceQualityObservationDetected(FaceQualityModel)

并且,更新 perform(action:)方法开关以处理新操作:

case .faceQualityObservationDetected(let faceQualityObservation):
  publishFaceQualityObservation(faceQualityObservation)

在这里,只要模型执行 faceQualityObservationDetected 操作,您就会调用 publishFaceQualityObservation()。 将 publishFaceQualityObservation() 的函数定义和空实现替换为:

// 1
private func publishFaceQualityObservation(_ faceQualityModel: FaceQualityModel) {
  // 2
  DispatchQueue.main.async { [self] in
    // 3
    faceDetectedState = .faceDetected
    faceQualityState = .faceFound(faceQualityModel)
  }
}

在这里,你是:

  • 1) 更新函数定义以传入FaceQualityModel
  • 2) 调度到主线程以确保安全。
  • 3) 更新 faceDetectedStatefaceQualityState 以记录人脸检测。 质量状态存储质量模型。

2. Detecting Quality

现在视图模型都设置好了,是时候进行一些检测了。 打开 FaceDetector.swift

在为 detectFaceRectanglesRequest 设置修订后,在 captureOutput(_:didOutput:from:) 中添加一个新请求:

let detectCaptureQualityRequest =
  VNDetectFaceCaptureQualityRequest(completionHandler: detectedFaceQualityRequest)
detectCaptureQualityRequest.revision =
  VNDetectFaceCaptureQualityRequestRevision2

在这里,您将创建一个新的人脸质量请求,其中包含一个调用detectedFaceQualityRequestcompletion handler。 然后,将其设置为使用修订版 2

将请求添加到传递给 sequenceHandler 的数组中,如下几行:

[detectFaceRectanglesRequest, detectCaptureQualityRequest],

最后,编写completion handler的实现,detectedFaceQualityRequest(request:error:)

// 1
guard let model = model else {
  return
}

// 2
guard
  let results = request.results as? [VNFaceObservation],
  let result = results.first
else {
  model.perform(action: .noFaceDetected)
  return
}

// 3
let faceQualityModel = FaceQualityModel(
  quality: result.faceCaptureQuality ?? 0
)

// 4
model.perform(action: .faceQualityObservationDetected(faceQualityModel))

此实现遵循上面的人脸矩形completion handler的模式。

在这里,你:

  • 1) 确保视图模型不为nil,否则会提前返回。
  • 2) 检查以确认请求包含有效的 VNFaceObservation 结果并提取第一个结果。
  • 3) 从结果中拉出 faceCaptureQuality(如果不存在,则默认为 0)。 使用它来初始化 FaceQualityModel
  • 4) 最后,通过新的 faceQualityModel 执行您创建的 faceQualityObservationDetected 操作。

打开 DebugView.swift。 在roll/pitch/yaw DebugSection之后,在VStack的最后,添加一个section来输出当前的质量:

DebugSection(observation: model.faceQualityState) { qualityModel in
  DebugText("Q: \(qualityModel.quality)")
    .debugTextStatus(status: model.isAcceptableQuality ? .passing : .failing)
}

构建并运行。 调试文本现在显示检测到的人脸的质量。 仅当质量高于 0.2 时才启用快门。


Offering Helpful Hints

如果其中一个可接受性标准失败,则应用程序始终显示相同的消息。 因为模型对每个都有状态,所以您可以使应用程序更有用。

打开 UserInstructionsView.swift 并找到 faceDetectionStateLabel()。 用以下内容替换整个 faceDetected case

if model.hasDetectedValidFace {
  return "Please take your photo :]"
} else if model.isAcceptableBounds == .detectedFaceTooSmall {
  return "Please bring your face closer to the camera"
} else if model.isAcceptableBounds == .detectedFaceTooLarge {
  return "Please hold the camera further from your face"
} else if model.isAcceptableBounds == .detectedFaceOffCentre {
  return "Please move your face to the centre of the frame"
} else if !model.isAcceptableRoll || !model.isAcceptablePitch || !model.isAcceptableYaw {
  return "Please look straight at the camera"
} else if !model.isAcceptableQuality {
  return "Image quality too low"
} else {
  return "We cannot take your photo right now"
}

此代码根据失败的标准选择特定指令。 构建并运行应用程序并尝试将您的脸移入和移出可接受的区域。


Segmenting Sapiens

iOS 15 中的新功能,Vision 框架现在支持人物分割(person segmentation)。分割只是意味着将主题与图像中的其他所有内容分开。例如,替换图像的背景但保持前景不变——你肯定在去年的视频通话中看到过这种技术!

Vision 框架中,可以使用 GeneratePersonSegmentationRequest 进行人员分割。此功能通过一次分析单个帧来工作。提供三种质量选项。视频流的分割需要逐帧分析视频。

人分割请求的结果包括一个pixelBuffer。这包含原始图像的蒙版。白色像素代表原始图像中的一个人,黑色代表背景。

护照照片需要在纯白色背景下拍摄的人。人物分割是替换背景但保持人物完整的好方法。


Using Metal

在替换图像中的背景之前,您需要对 Metal有所了解。

MetalApple 提供的一个非常强大的 API。它在 GPU 上执行图形密集型操作,以实现高性能图像处理。它的速度足以实时处理视频中的每一帧。这听起来很有用!

打开 CameraViewController.swift。查看 configureCaptureSession() 的底部。相机视图控制器显示来自 AVCaptureSession 的预览层。

该类支持两种模式。一种使用Metal,一种不使用Metal。目前它设置为不使用 Metal。你现在会改变它。

viewDidLoad() 中,在调用 configureCaptureSession() 之前添加以下代码:

configureMetal()

这会将应用程序配置为使用 Metal。 视图控制器现在从 Metal 而不是 AVCaptureSession 中绘制结果。 不过,这不是关于 Metal 的教程,所以设置代码已经编写好了。 如果您好奇,请随意阅读 configureMetal() 中的实现。

Metal 配置为绘制视图后,您可以完全控制视图显示的内容。


Building Better Backgrounds

Hide Background按钮位于相机控件左侧的调试按钮上方。 触发按钮什么也没做。

打开 FaceDetector.swift。 找到 captureOutput(_:didOutput:from:)。 在 detectCaptureQualityRequest 的声明下,添加一个新的人脸分割请求:

// 1
let detectSegmentationRequest = VNGeneratePersonSegmentationRequest(completionHandler: detectedSegmentationRequest)
// 2
detectSegmentationRequest.qualityLevel = .balanced

在这里,你:

  • 1) 创建分割请求。 完成后调用detectedSegmentationRequest 方法。
  • 2) VNGeneratePersonSegmentationRequest 提供三个质量级别:accurate, balanced and fast。 算法运行得越快,生成的掩码质量就越低。 fast and balanced的质量级别都运行得足够快,可以用于视频数据。 accurate的质量级别需要静态图像。

接下来,更新对 sequenceHandler 的 perform(_:on:orientation:)方法的调用以包含新的分隔请求:

[detectFaceRectanglesRequest, detectCaptureQualityRequest, detectSegmentationRequest],

1. Handling the Segmentation Request Result

然后将以下内容添加到detectedSegmentationRequest(request:error:)

// 1
guard
  let model = model,
  let results = request.results as? [VNPixelBufferObservation],
  let result = results.first,
  let currentFrameBuffer = currentFrameBuffer
else {
  return
}

// 2
if model.hideBackgroundModeEnabled {
  // 3
  let originalImage = CIImage(cvImageBuffer: currentFrameBuffer)
  let maskPixelBuffer = result.pixelBuffer
  let outputImage = removeBackgroundFrom(image: originalImage, using: maskPixelBuffer)
  viewDelegate?.draw(image: outputImage.oriented(.upMirrored))
} else {
  // 4
  let originalImage = CIImage(cvImageBuffer: currentFrameBuffer).oriented(.upMirrored)
  viewDelegate?.draw(image: originalImage)
}

在此代码中,您:

  • 1) 拉出模型、第一次观察和当前帧缓冲区,或者如果有nil的话提前返回。
  • 2) 查询模型以获取后台隐藏模式的状态。
  • 3) 如果隐藏,则从相机创建原始帧的core image表示。 此外,创建人物分割结果的掩码。 然后,使用这两个来创建删除背景的输出图像。
  • 4) 否则,在不隐藏背景的情况下,将重新创建原始图像而不会更改。

在任何一种情况下,都会调用视图上的代理方法来在frame中绘制图像。 这将使用上一节中讨论的 Metal 管道。

2. Removing the Background

替换removeBackgroundFrom(image:using:)的实现:

// 1
var maskImage = CIImage(cvPixelBuffer: maskPixelBuffer)

// 2
let originalImage = image.oriented(.right)

// 3.
let scaleX = originalImage.extent.width / maskImage.extent.width
let scaleY = originalImage.extent.height / maskImage.extent.height
maskImage = maskImage.transformed(by: .init(scaleX: scaleX, y: scaleY)).oriented(.upMirrored)

// 4
let backgroundImage = CIImage(color: .white).clampedToExtent().cropped(to: originalImage.extent)

// 5
let blendFilter = CIFilter.blendWithRedMask()
blendFilter.inputImage = originalImage
blendFilter.backgroundImage = backgroundImage
blendFilter.maskImage = maskImage

// 6
if let outputImage = blendFilter.outputImage?.oriented(.left) {
  return outputImage
}

// 7
return originalImage

在这里,你:

  • 1) 使用像素缓冲区中的分割蒙版创建蒙版的core image
  • 2) 然后,将原始图像向右旋转。分割蒙版结果相对于相机旋转 90 度。因此,您需要在混合之前对齐图像和蒙版。
  • 3) 同样,蒙版图像的大小与直接从相机中提取的视频帧的大小不同。因此,缩放蒙版图像以适合。
  • 4) 接下来,创建与原始图像相同大小的纯白色图像。 clampedToExtent() 创建一个具有无限宽度和高度的图像。然后将其裁剪为原始图像的大小。
  • 5) 现在是实际工作。创建一个core image过滤器,将原始图像与全白图像混合。使用分割蒙版图像作为蒙版。
  • 6) 最后,将过滤器的输出重新向左旋转并返回
  • 7) 或者,如果无法创建混合图像,则返回原始图像。

构建并运行。打开和关闭Hide Background按钮。观察你身体周围的背景消失。


Saving the Picture

您的护照照片应用程序即将完成!

还有一项任务——拍摄并保存照片。 首先打开 CameraViewModel.swift 并在 isAcceptableQuality 属性声明下添加一个新的已发布属性:

@Published private(set) var passportPhoto: UIImage?

passportPhoto是一个可选的 UIImage 代表最近一张照片。 在拍摄第一张照片之前为nil
接下来,将另外两个action作为case添加到 CameraViewModelAction 枚举:

case takePhoto
case savePhoto(UIImage)

第一个action在用户按下快门按钮时执行。 第二个action在处理完图像准备保存到相机胶卷后执行。

接下来,将新操作的处理程序添加到 perform(action:)switch 语句的末尾:

case .takePhoto:
  takePhoto()
case .savePhoto(let image):
  savePhoto(image)

然后,将实现添加到 takePhoto() 方法。 这个很简单:

shutterReleased.send()

shutterReleased 是一个发布 void 值的 Combine PassthroughSubject。 应用程序中持有对视图模型的引用的任何部分都可以订阅用户释放快门的事件。

添加 savePhoto(_:) 的实现,它几乎一样简单:

// 1
UIImageWriteToSavedPhotosAlbum(photo, nil, nil, nil)
// 2
DispatchQueue.main.async { [self] in
  // 3
  passportPhoto = photo
}

在这里,你:

  • 1) 将提供的 UIImage 写入手机相册。
  • 2) 根据所有 UI 操作的需要分派到主线程。
  • 3) 将当前护照照片设置为传入该方法的照片。

接下来,打开 CameraControlsFooterView.swift 并连接控件。 将 ShutterButton action闭包中的 print("TODO") 替换为以下内容:

model.perform(action: .takePhoto)

这告诉视图模型执行快门释放。

然后,通过从模型中传递护照照片来更新 ThumbnailView 以显示护照照片:

ThumbnailView(passportPhoto: model.passportPhoto)

最后,打开 FaceDetector.swift 并进行必要的更改以捕获和处理照片数据。 首先,在定义 currentFrameBuffer 属性后向类添加一个新属性:

var isCapturingPhoto = false

此标志指示下一帧应拍摄照片。 每当视图模型的 shutterReleased 属性发布一个值时,您就可以设置它。

找到weak var model: CameraViewModel?属性并像这样更新它:

weak var model: CameraViewModel? {
  didSet {
    // 1
    model?.shutterReleased.sink { completion in
      switch completion {
      case .finished:
        return
      case .failure(let error):
        print("Received error: \(error)")
      }
    } receiveValue: { _ in
      // 2
      self.isCapturingPhoto = true
    }
    .store(in: &subscriptions)
  }
}

在这里,你:

  • 1) 在设置后观察模型的 shutterReleased 属性的更新。
  • 2) 释放快门时将 isCapturingPhoto 属性设置为 true

1. Saving to Camera Roll

接下来,在 captureOutput(_:didOutput:from:) 中,紧接在初始化 detectFaceRectanglesRequest之前,添加以下内容:

if isCapturingPhoto {
  isCapturingPhoto = false
  savePassportPhoto(from: imageBuffer)
}

在这里,如果需要,您可以重置 isCapturingPhoto 标志,并调用一个方法来保存带有图像缓冲区数据的护照照片。

最后,编写 savePassportPhoto(from:) 的实现:

// 1
guard let model = model else {
  return
}

// 2
imageProcessingQueue.async { [self] in
  // 3
  let originalImage = CIImage(cvPixelBuffer: pixelBuffer)
  var outputImage = originalImage

  // 4
  if model.hideBackgroundModeEnabled {
    // 5
    let detectSegmentationRequest = VNGeneratePersonSegmentationRequest()
    detectSegmentationRequest.qualityLevel = .accurate

    // 6
    try? sequenceHandler.perform(
      [detectSegmentationRequest],
      on: pixelBuffer,
      orientation: .leftMirrored
    )

    // 7
    if let maskPixelBuffer = detectSegmentationRequest.results?.first?.pixelBuffer {
      outputImage = removeBackgroundFrom(image: originalImage, using: maskPixelBuffer)
    }
  }

  // 8
  let coreImageWidth = outputImage.extent.width
  let coreImageHeight = outputImage.extent.height

  let desiredImageHeight = coreImageWidth * 4 / 3

  // 9
  let yOrigin = (coreImageHeight - desiredImageHeight) / 2
  let photoRect = CGRect(x: 0, y: yOrigin, width: coreImageWidth, height: desiredImageHeight)

  // 10
  let context = CIContext()
  if let cgImage = context.createCGImage(outputImage, from: photoRect) {
    // 11
    let passportPhoto = UIImage(cgImage: cgImage, scale: 1, orientation: .upMirrored)

    // 12
    DispatchQueue.main.async {
      model.perform(action: .savePhoto(passportPhoto))
    }
  }
}

它看起来像很多代码!这是正在做的事情:

  • 1) 首先,如果模型尚未设置,请尽早return
  • 2) 接下来,分派到后台队列以保持 UI 流畅。
  • 3) 创建输入图像的core image表示和存储输出图像的变量。
  • 4) 然后,如果用户请求删除背景......
  • 5) 创建一个新的人物分割请求,这次没有完成处理程序。您希望护照照片的质量尽可能好,因此请将质量设置为accurate。这在这里有效,因为您只处理单个图像,并且您在后台线程上执行它。
  • 6) 执行分割请求。
  • 7) 同步读取结果。如果存在掩码像素缓冲区,请从原始图像中删除背景。通过调用 removeBackgroundFrom(image:using:)来执行此操作,将更准确的掩码传递给它。
  • 8) 此时, outputImage 包含具有所需背景的护照照片。下一步是设置护照照片的宽度和高度。请记住,护照照片的纵横比可能与相机不同。
  • 9) 使用图像的全宽和垂直中心计算照片的frame
  • 10) 将输出图像(Core Image 对象)转换为 Core Graphics 图像。
  • 11) 然后,从core graphics图像创建一个 UIImage
  • 12) 调度回主线程并要求模型执行保存照片操作。

完毕!

构建并运行。正确对齐您的脸部并在启用和不启用背景隐藏的情况下拍照。拍照后,缩略图将出现在页脚的右侧。单击缩略图将加载图像的详细视图。如果您打开照片Photos应用程序,您还会发现您的照片已保存到相机中。

请注意,静止图像中的背景替换质量如何优于视频源中的背景替换质量。

仍有改进应用程序的方法。 例如,您可以查看使用 Core Image 的smile detector来防止微笑照片。 或者您可以在不隐藏背景时反转蒙版以检查真实背景是否为白色。

您还可以查看通过Combine流发布 hasDetectedValidFace。 通过限制流,您可以阻止 UI 在面部处于可接受的边缘时快速闪烁。

Apple documentation是了解更多有关 Vision 框架的重要资源。 如果您想了解有关 Metal 的更多信息,请尝试这个出色的教程来帮助您入门。

后记

本篇主要讲述了基于VisionFace Detection新特性,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(Vision框架详细解析(十二) —— 基于Vision的Face Detection新特性(一))