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

版本記錄

版本號 時間
V1.0 2022.02.26 星期六

前言

iOS 11+macOS 10.13+ 新出了Vision框架扎拣,提供了人臉識別熔号、物體檢測稽鞭、物體跟蹤等技術(shù),它是基于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(二)

開始

首先看下主要內(nèi)容

了解Face Detection的新增功能以及Vision框架的最新添加如何幫助您在圖像分割和分析中獲得更好的結(jié)果涉茧。內(nèi)容來自翻譯

接著看下寫作環(huán)境

Swift 5.5, iOS 15, Xcode 13

下面就是正文了

拍護照照片很痛苦疹娶。 有很多規(guī)則要遵守伴栓,很難知道你的照片是否會被接受。 幸運的是雨饺,您生活在 21 世紀钳垮! 通過使用 Vision 框架中的面部檢測來控制您的護照照片體驗。 在將照片發(fā)送到護照辦公室之前额港,請了解您的照片是否可以接受饺窿!

注意:本教程的項目需要訪問相機。 它只能在真機上運行移斩,不能在模擬器上運行肚医。 此外,還假設(shè)了 Vision 框架的一些現(xiàn)有知識叹哭。 如果您以前從未使用過 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 的項目。 在本教程中超升,您將構(gòu)建一個簡單的拍照應(yīng)用程序入宦,該應(yīng)用程序僅在生成的圖像對護照照片有效時才允許用戶拍照。 您將遵循的有效性規(guī)則適用于英國護照照片室琢,但很容易復(fù)制到任何其他國家/地區(qū)乾闰。

打開啟動項目,從可用的運行targets中選擇您的手機并構(gòu)建并運行盈滴。

注意:您可能需要設(shè)置 Xcode 來簽署應(yīng)用程序涯肩,然后才能在您的設(shè)備上運行它。 最簡單的方法是為您的target打開 Signing & Capabilities 編輯器巢钓。 然后病苗,選擇Automatically Manage Signing

該應(yīng)用程序顯示前置攝像頭視圖症汹。 紅色矩形和綠色橢圓形覆蓋在屏幕中央硫朦。 頂部的橫幅包含說明。 底部橫幅包含控制背镇。

中央按鈕是用于拍照的快門釋放按鈕咬展。 在左側(cè)泽裳,頂部按鈕切換背景,底部(由瓢蟲表示)打開和關(guān)閉調(diào)試視圖破婆。 快門右側(cè)的按鈕是一個占位符涮总,替換為最近一張照片的縮略圖。

把手機舉到你面前荠割。 黃色邊界框開始跟蹤您的臉部妹卿。 一些人臉檢測已經(jīng)在進行中!

1. A Tour of the App

現(xiàn)在讓我們?yōu)g覽一下該應(yīng)用程序蔑鹦,讓您了解方向夺克。

Xcode 中,打開 PassportPhotosAppView.swift嚎朽。這是應(yīng)用程序的根視圖铺纽。它包含一堆視圖。 CameraView 位于底部哟忍。然后狡门,一個 LayoutGuide 視圖(在屏幕上繪制綠色橢圓)和一個可選的 DebugView。最后锅很,CameraOverlayView 位于頂部其馏。

該應(yīng)用程序中還有一些其他文件。其中大部分是用于 UI 各個部分的簡單視圖爆安。在本教程中叛复,您將主要更新三個類:CameraViewModel、CameraViewController UIKit 視圖控制器和 FaceDetector扔仓。

打開 CameraViewModel.swift褐奥。此類控制整個應(yīng)用程序的狀態(tài)。它定義了應(yīng)用程序中的視圖可以訂閱的一些已發(fā)布屬性翘簇。視圖可以通過調(diào)用單個公共方法——perform(action:)來更新應(yīng)用程序的狀態(tài)撬码。

接下來,打開 CameraView.swift版保。這是一個簡單的 SwiftUI UIViewControllerRepresentable結(jié)構(gòu)呜笑。它用 FaceDetector 實例化一個 CameraViewController

現(xiàn)在打開 CameraViewController.swift彻犁。 CameraViewController 配置和控制 AV capture session蹈垢。這會將像素從相機繪制到屏幕上。在 viewDidLoad() 中袖裕,設(shè)置了人臉檢測器對象的代理。然后它配置并啟動AV capture session溉瓶。 configureCaptureSession()執(zhí)行大部分設(shè)置急鳄。這是您之前希望看到的所有基本設(shè)置代碼谤民。

該類還包含一些與設(shè)置 Metal相關(guān)的方法。暫時不要擔(dān)心這個疾宏。

最后张足,打開 FaceDetector.swift。這個實用程序類只有一個目的——成為 CameraViewControllerAVCaptureVideoDataOutput 設(shè)置的代理坎藐。這就是面部檢測魔術(shù)發(fā)生的地方为牍。更多關(guān)于這下面。

隨意瀏覽應(yīng)用程序的其余部分岩馍。


Reviewing the Vision Framework

Vision 框架自 iOS 11 以來一直存在碉咆。它提供了對圖像和視頻執(zhí)行各種計算機視覺算法的功能。例如人臉landmark檢測蛀恩、文本檢測疫铜、條碼識別等。

iOS 15 之前双谆,Vision 框架允許您查詢檢測到的人臉的roll and yaw壳咕。它還提供了某些landmark的位置,例如眼睛顽馋、耳朵和鼻子谓厘。這方面的一個例子已經(jīng)在應(yīng)用程序中實現(xiàn)。

打開 FaceDetector.swift 并找到 captureOutput(_:didOutput:from:)寸谜。在此方法中竟稳,面部檢測器在 AVCaptureSession 提供的圖像緩沖區(qū)上設(shè)置 VNDetectFaceRectanglesRequest

當(dāng)檢測到人臉矩形時程帕,將調(diào)用完成處理程序completion -detectedFaceRectangles(request:error:)住练。該方法從人臉觀察結(jié)果中拉出人臉的邊界框,并對CameraViewModel執(zhí)行faceObservationDetected` 動作愁拭。


Looking Forward

是時候添加你的第一段代碼了讲逛!

護照規(guī)定要求人們直視相機。是時候添加這個功能了岭埠。打開CameraViewModel.swift盏混。

找到 FaceGeometryModel結(jié)構(gòu)體定義。通過添加以下新屬性來更新結(jié)構(gòu)體:

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

此更改允許您在視圖模型中存儲在面部檢測到的roll, pitch and yaw值惜论。

在類頂部的 hasDetectedValidFace 屬性下许赃,添加以下新的已發(fā)布屬性:

@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是否可以用于護照照片。 當(dāng)每一個更新時馆类,都會調(diào)用calculateDetectedFaceValidity()方法混聊。

接下來,將以下內(nèi)容添加到 init() 的底部:

isAcceptableRoll = false
isAcceptablePitch = false
isAcceptableYaw = false

這只是設(shè)置您剛剛添加的屬性的初始值乾巧。

現(xiàn)在句喜,找到 invalidateFaceGeometryState()方法预愤。 目前是一個stub。 將以下代碼添加到該函數(shù)中:

isAcceptableRoll = false
isAcceptablePitch = false
isAcceptableYaw = false

因為沒有檢測到面部咳胃,所以您將可接受的roll, pitch and yaw值設(shè)置為 false植康。

1. Processing Faces

接下來,通過將 faceFound() case 替換為以下內(nèi)容來更新 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:)

現(xiàn)在將以下內(nèi)容添加到 updateAcceptableRollPitchYaw(using:pitch:yaw:) 的實現(xiàn)存根中:

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

在這里存崖,您根據(jù)從面部提取的值設(shè)置可接受的roll, pitch and yaw狀態(tài)冻记。

最后,替換 calculateDetectedFaceValidity() 以使用 roll金句、pitch 和 yaw 值來確定面部是否有效:

hasDetectedValidFace =
  isAcceptableRoll &&
  isAcceptablePitch &&
  isAcceptableYaw

現(xiàn)在檩赢,打開 FaceDetector.swift。 在 detectedFaceRectangles(request:error:) 中违寞,將 faceObservationModel 的定義替換為以下內(nèi)容:

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

這只是將現(xiàn)在需要的roll, pitch and yaw參數(shù)添加到 FaceGeometryModel對象的初始化中贞瞒。

2. Debug those Faces

最好將一些有關(guān)roll, pitch and yaw的信息添加到調(diào)試視圖中,這樣您就可以在使用應(yīng)用程序時看到這些值趁曼。

打開 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)
}

這已更新調(diào)試文本以將當(dāng)前值打印到屏幕上军浆,并根據(jù)該值是否可接受來設(shè)置文本顏色。

構(gòu)建并運行挡闰。

直視相機乒融,注意橢圓是綠色的。 現(xiàn)在從一邊到另一邊旋轉(zhuǎn)你的頭摄悯,注意當(dāng)你不直看相機時橢圓是如何變成紅色的赞季。 如果您打開了調(diào)試模式,請注意yaw數(shù)如何改變值和顏色奢驯。


Selecting a Size

接下來申钩,您希望應(yīng)用程序檢測一張臉在照片框架內(nèi)的大小。 打開 Camera ViewModel.swift 并在 isAcceptableYaw 聲明下添加以下屬性:

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

然后瘪阁,在 init() 的底部設(shè)置該屬性的初始值:

isAcceptableBounds = .unknown

和之前一樣撒遣,將以下內(nèi)容添加到 invalidateFaceGeometryState() 的末尾:

isAcceptableBounds = .unknown

接下來,在 processUpdatedFaceGeometry() 中管跺,將以下內(nèi)容添加到 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 設(shè)置為 FaceBoundsState.detectedFaceApppropriateSizeAndPosition。 否則火的,將其設(shè)置為相應(yīng)的錯誤情況壶愤。

最后,將 calculateDetectedFaceValidity() 更新為如下所示:

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

這增加了對邊界是否可接受的檢查馏鹤。

構(gòu)建并運行。 將手機靠近和遠離您的臉部娇哆,并注意橢圓形如何改變顏色湃累。


Detecting Differences

目前,FaceDetector 正在使用 VNDetectFaceRectanglesRequestRevision2 檢測人臉矩形碍讨。 iOS 15 引入了一個新版本治力,VNDetectFaceRectanglesRequestRevision3跳夭。那么有什么區(qū)別呢屈溉?

Version 3 為檢測面部矩形提供了許多有用的更新,包括:

  • 1) 現(xiàn)在確定檢測到的面部的pitch拳缠。您可能沒有注意到覆获,但到目前為止马澈,pitch的值始終為 0,因為它不存在于面部觀察中弄息。
  • 2) Roll, pitch and yaw值在連續(xù)空間中報告痊班。使用 VNDetectFaceRectanglesRequestRevision2roll and yaw僅在discrete bins內(nèi)提供摹量。您可以使用應(yīng)用程序內(nèi)左右轉(zhuǎn)動頭部自己觀察這一點涤伐。yaw總是在 0±0.785 弧度之間跳躍。
  • 3) 在檢測人臉landmarks時缨称,可以準(zhǔn)確地檢測到瞳孔的位置凝果。以前,即使向外看臉的一側(cè)睦尽,瞳孔也會設(shè)置在眼睛的中心器净。

是時候更新應(yīng)用程序以使用 VNDetectFaceRectanglesRequestRevision3。您將利用檢測到的音高并觀察連續(xù)的空間更新骂删。

打開 FaceDetector.swift掌动。在 captureOutput(_:didOutput:from:) 中,將 detectFaceRectanglesRequestrevision屬性更新為revision 3

detectFaceRectanglesRequest.revision = VNDetectFaceRectanglesRequestRevision3

構(gòu)建并運行宁玫。

將手機舉到臉前粗恢。 請注意調(diào)試輸出中打印的值如何在每一幀上更新。 低頭(下巴放在胸前)欧瘪。 請注意pitch數(shù)字也是如何更新的眷射。


Masking Mayhem

除非您一直生活在巖石下,否則您一定注意到越來越多的人戴著口罩。這對于對抗 COVID 非常有用妖碉,但對于人臉識別卻很糟糕涌庭!

幸運的是,Apple 支持您欧宜。借助 VNDetectFaceRectanglesRequestRevision3坐榆,Vision 框架現(xiàn)在可以檢測被口罩覆蓋的人臉。雖然這對于通用人臉檢測很有用冗茸,但對于您的護照照片應(yīng)用程序來說卻是一場災(zāi)難席镀。護照照片絕對不允許戴口罩!那么如何防止戴口罩的人拍照呢夏漱?

幸運的是豪诲,Apple 還提高了面部捕捉質(zhì)量。面部捕捉質(zhì)量為檢測到的面部提供分數(shù)挂绰。它考慮了光照屎篱、遮擋、模糊等屬性葵蒂。

請注意交播,質(zhì)量檢測會將同一主題與自身的副本進行比較。它不會將一個人與另一個人進行比較刹勃。捕獲質(zhì)量在 01 之間變化堪侯。iOS 15 中的最新版本是 VNDetectFaceCaptureQualityRequestRevision2


Assuring Quality

在請求質(zhì)量分數(shù)之前荔仁,您的應(yīng)用需要一個地方來存儲當(dāng)前幀的質(zhì)量伍宦。首先,更新模型以保存有關(guān)面部質(zhì)量的信息乏梁。

打開 CameraViewModel.swift次洼。在 FaceGeometryModel 結(jié)構(gòu)體下,添加以下內(nèi)容以存儲質(zhì)量狀態(tài):

struct FaceQualityModel {
  let quality: Float
}

此結(jié)構(gòu)體包含一個浮點屬性遇骑,用于存儲最近檢測到的質(zhì)量卖毁。

faceGeometryState的聲明下,添加一個發(fā)布人臉質(zhì)量狀態(tài)的屬性:

// 1
@Published private(set) var faceQualityState: FaceObservation<FaceQualityModel> {
  didSet {
    // 2
    processUpdatedFaceQuality()
  }
}
  • 1) 這遵循類似于上面的 faceGeometryState 屬性的模式落萎。 FaceObservation 枚舉包裝了底層模型值亥啦。 FaceObservation 是一個提供類型安全的通用包裝器。 它包含三種狀態(tài):face found, face not found and error练链。
  • 2) faceQualityState 的更新調(diào)用 processUpdatedFaceQuality()翔脱。

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

faceQualityState = .faceNotFound

這會將 faceQualityState 的初始值設(shè)置為 .faceNotFound

接下來媒鼓,添加一個新的已發(fā)布屬性以獲得可接受的質(zhì)量:

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

與其他屬性一樣届吁,在 init()方法中對其進行初始化:

isAcceptableQuality = false

現(xiàn)在错妖,您可以編寫 processUpdatedFaceQuality() 的實現(xiàn):

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 的不同狀態(tài)疚沐。 可接受的質(zhì)量得分為 0.2 或更高暂氯。

更新 calculateDetectedFaceValidity() 以考慮可接受的質(zhì)量,將最后一行替換為:

isAcceptableYaw && isAcceptableQuality

1. Handling Quality Result

faceQualityState 屬性現(xiàn)在設(shè)置為存儲檢測到的人臉質(zhì)量亮蛔。 但是痴施,沒有任何方法可以更新該狀態(tài)。 是時候解決這個問題了尔邓。

CameraViewModelAction 枚舉中晾剖,在 faceObservationDetected 之后添加一個新動作:

case faceQualityObservationDetected(FaceQualityModel)

并且,更新 perform(action:)方法開關(guān)以處理新操作:

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

在這里梯嗽,只要模型執(zhí)行 faceQualityObservationDetected 操作,您就會調(diào)用 publishFaceQualityObservation()沽损。 將 publishFaceQualityObservation() 的函數(shù)定義和空實現(xiàn)替換為:

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

在這里灯节,你是:

  • 1) 更新函數(shù)定義以傳入FaceQualityModel
  • 2) 調(diào)度到主線程以確保安全绵估。
  • 3) 更新 faceDetectedStatefaceQualityState 以記錄人臉檢測炎疆。 質(zhì)量狀態(tài)存儲質(zhì)量模型。

2. Detecting Quality

現(xiàn)在視圖模型都設(shè)置好了国裳,是時候進行一些檢測了形入。 打開 FaceDetector.swift

在為 detectFaceRectanglesRequest 設(shè)置修訂后缝左,在 captureOutput(_:didOutput:from:) 中添加一個新請求:

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

在這里亿遂,您將創(chuàng)建一個新的人臉質(zhì)量請求,其中包含一個調(diào)用detectedFaceQualityRequestcompletion handler渺杉。 然后蛇数,將其設(shè)置為使用修訂版 2

將請求添加到傳遞給 sequenceHandler 的數(shù)組中是越,如下幾行:

[detectFaceRectanglesRequest, detectCaptureQualityRequest],

最后耳舅,編寫completion handler的實現(xiàn),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))

此實現(xiàn)遵循上面的人臉矩形completion handler的模式倚评。

在這里浦徊,你:

  • 1) 確保視圖模型不為nil,否則會提前返回天梧。
  • 2) 檢查以確認請求包含有效的 VNFaceObservation 結(jié)果并提取第一個結(jié)果盔性。
  • 3) 從結(jié)果中拉出 faceCaptureQuality(如果不存在,則默認為 0)腿倚。 使用它來初始化 FaceQualityModel纯出。
  • 4) 最后蚯妇,通過新的 faceQualityModel 執(zhí)行您創(chuàng)建的 faceQualityObservationDetected 操作。

打開 DebugView.swift暂筝。 在roll/pitch/yaw DebugSection之后箩言,在VStack的最后,添加一個section來輸出當(dāng)前的質(zhì)量:

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

構(gòu)建并運行焕襟。 調(diào)試文本現(xiàn)在顯示檢測到的人臉的質(zhì)量陨收。 僅當(dāng)質(zhì)量高于 0.2 時才啟用快門。


Offering Helpful Hints

如果其中一個可接受性標(biāo)準(zhǔn)失敗鸵赖,則應(yīng)用程序始終顯示相同的消息务漩。 因為模型對每個都有狀態(tài),所以您可以使應(yīng)用程序更有用它褪。

打開 UserInstructionsView.swift 并找到 faceDetectionStateLabel()饵骨。 用以下內(nèi)容替換整個 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"
}

此代碼根據(jù)失敗的標(biāo)準(zhǔn)選擇特定指令。 構(gòu)建并運行應(yīng)用程序并嘗試將您的臉移入和移出可接受的區(qū)域茫打。


Segmenting Sapiens

iOS 15 中的新功能居触,Vision 框架現(xiàn)在支持人物分割(person segmentation)。分割只是意味著將主題與圖像中的其他所有內(nèi)容分開老赤。例如轮洋,替換圖像的背景但保持前景不變——你肯定在去年的視頻通話中看到過這種技術(shù)!

Vision 框架中抬旺,可以使用 GeneratePersonSegmentationRequest 進行人員分割弊予。此功能通過一次分析單個幀來工作。提供三種質(zhì)量選項开财。視頻流的分割需要逐幀分析視頻汉柒。

人分割請求的結(jié)果包括一個pixelBuffer。這包含原始圖像的蒙版床未。白色像素代表原始圖像中的一個人竭翠,黑色代表背景。

護照照片需要在純白色背景下拍攝的人薇搁。人物分割是替換背景但保持人物完整的好方法斋扰。


Using Metal

在替換圖像中的背景之前,您需要對 Metal有所了解啃洋。

MetalApple 提供的一個非常強大的 API传货。它在 GPU 上執(zhí)行圖形密集型操作,以實現(xiàn)高性能圖像處理宏娄。它的速度足以實時處理視頻中的每一幀问裕。這聽起來很有用!

打開 CameraViewController.swift孵坚。查看 configureCaptureSession() 的底部粮宛。相機視圖控制器顯示來自 AVCaptureSession 的預(yù)覽層窥淆。

該類支持兩種模式。一種使用Metal巍杈,一種不使用Metal忧饭。目前它設(shè)置為不使用 Metal。你現(xiàn)在會改變它筷畦。

viewDidLoad() 中词裤,在調(diào)用 configureCaptureSession() 之前添加以下代碼:

configureMetal()

這會將應(yīng)用程序配置為使用 Metal。 視圖控制器現(xiàn)在從 Metal 而不是 AVCaptureSession 中繪制結(jié)果鳖宾。 不過吼砂,這不是關(guān)于 Metal 的教程,所以設(shè)置代碼已經(jīng)編寫好了鼎文。 如果您好奇渔肩,請隨意閱讀 configureMetal() 中的實現(xiàn)。

Metal 配置為繪制視圖后拇惋,您可以完全控制視圖顯示的內(nèi)容赖瞒。


Building Better Backgrounds

Hide Background按鈕位于相機控件左側(cè)的調(diào)試按鈕上方。 觸發(fā)按鈕什么也沒做蚤假。

打開 FaceDetector.swift。 找到 captureOutput(_:didOutput:from:)吧兔。 在 detectCaptureQualityRequest 的聲明下磷仰,添加一個新的人臉分割請求:

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

在這里,你:

  • 1) 創(chuàng)建分割請求境蔼。 完成后調(diào)用detectedSegmentationRequest 方法灶平。
  • 2) VNGeneratePersonSegmentationRequest 提供三個質(zhì)量級別:accurate, balanced and fast。 算法運行得越快箍土,生成的掩碼質(zhì)量就越低逢享。 fast and balanced的質(zhì)量級別都運行得足夠快,可以用于視頻數(shù)據(jù)吴藻。 accurate的質(zhì)量級別需要靜態(tài)圖像瞒爬。

接下來,更新對 sequenceHandler 的 perform(_:on:orientation:)方法的調(diào)用以包含新的分隔請求:

[detectFaceRectanglesRequest, detectCaptureQualityRequest, detectSegmentationRequest],

1. Handling the Segmentation Request Result

然后將以下內(nèi)容添加到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) 拉出模型侧但、第一次觀察和當(dāng)前幀緩沖區(qū),或者如果有nil的話提前返回航罗。
  • 2) 查詢模型以獲取后臺隱藏模式的狀態(tài)禀横。
  • 3) 如果隱藏,則從相機創(chuàng)建原始幀的core image表示粥血。 此外柏锄,創(chuàng)建人物分割結(jié)果的掩碼酿箭。 然后,使用這兩個來創(chuàng)建刪除背景的輸出圖像趾娃。
  • 4) 否則缭嫡,在不隱藏背景的情況下,將重新創(chuàng)建原始圖像而不會更改茫舶。

在任何一種情況下械巡,都會調(diào)用視圖上的代理方法來在frame中繪制圖像。 這將使用上一節(jié)中討論的 Metal 管道饶氏。

2. Removing the Background

替換removeBackgroundFrom(image:using:)的實現(xiàn):

// 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) 使用像素緩沖區(qū)中的分割蒙版創(chuàng)建蒙版的core image
  • 2) 然后疹启,將原始圖像向右旋轉(zhuǎn)古程。分割蒙版結(jié)果相對于相機旋轉(zhuǎn) 90 度。因此喊崖,您需要在混合之前對齊圖像和蒙版挣磨。
  • 3) 同樣,蒙版圖像的大小與直接從相機中提取的視頻幀的大小不同荤懂。因此茁裙,縮放蒙版圖像以適合。
  • 4) 接下來节仿,創(chuàng)建與原始圖像相同大小的純白色圖像晤锥。 clampedToExtent() 創(chuàng)建一個具有無限寬度和高度的圖像。然后將其裁剪為原始圖像的大小廊宪。
  • 5) 現(xiàn)在是實際工作矾瘾。創(chuàng)建一個core image過濾器,將原始圖像與全白圖像混合箭启。使用分割蒙版圖像作為蒙版壕翩。
  • 6) 最后,將過濾器的輸出重新向左旋轉(zhuǎn)并返回
  • 7) 或者傅寡,如果無法創(chuàng)建混合圖像放妈,則返回原始圖像。

構(gòu)建并運行赏僧。打開和關(guān)閉Hide Background按鈕大猛。觀察你身體周圍的背景消失。


Saving the Picture

您的護照照片應(yīng)用程序即將完成淀零!

還有一項任務(wù)——拍攝并保存照片挽绩。 首先打開 CameraViewModel.swift 并在 isAcceptableQuality 屬性聲明下添加一個新的已發(fā)布屬性:

@Published private(set) var passportPhoto: UIImage?

passportPhoto是一個可選的 UIImage 代表最近一張照片。 在拍攝第一張照片之前為nil驾中。
接下來唉堪,將另外兩個action作為case添加到 CameraViewModelAction 枚舉:

case takePhoto
case savePhoto(UIImage)

第一個action在用戶按下快門按鈕時執(zhí)行模聋。 第二個action在處理完圖像準(zhǔn)備保存到相機膠卷后執(zhí)行。

接下來唠亚,將新操作的處理程序添加到 perform(action:)switch 語句的末尾:

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

然后链方,將實現(xiàn)添加到 takePhoto() 方法。 這個很簡單:

shutterReleased.send()

shutterReleased 是一個發(fā)布 void 值的 Combine PassthroughSubject灶搜。 應(yīng)用程序中持有對視圖模型的引用的任何部分都可以訂閱用戶釋放快門的事件祟蚀。

添加 savePhoto(_:) 的實現(xiàn),它幾乎一樣簡單:

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

在這里割卖,你:

  • 1) 將提供的 UIImage 寫入手機相冊前酿。
  • 2) 根據(jù)所有 UI 操作的需要分派到主線程。
  • 3) 將當(dāng)前護照照片設(shè)置為傳入該方法的照片鹏溯。

接下來罢维,打開 CameraControlsFooterView.swift 并連接控件。 將 ShutterButton action閉包中的 print("TODO") 替換為以下內(nèi)容:

model.perform(action: .takePhoto)

這告訴視圖模型執(zhí)行快門釋放丙挽。

然后肺孵,通過從模型中傳遞護照照片來更新 ThumbnailView 以顯示護照照片:

ThumbnailView(passportPhoto: model.passportPhoto)

最后,打開 FaceDetector.swift 并進行必要的更改以捕獲和處理照片數(shù)據(jù)颜阐。 首先平窘,在定義 currentFrameBuffer 屬性后向類添加一個新屬性:

var isCapturingPhoto = false

此標(biāo)志指示下一幀應(yīng)拍攝照片。 每當(dāng)視圖模型的 shutterReleased 屬性發(fā)布一個值時凳怨,您就可以設(shè)置它初婆。

找到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) 在設(shè)置后觀察模型的 shutterReleased 屬性的更新猿棉。
  • 2) 釋放快門時將 isCapturingPhoto 屬性設(shè)置為 true

1. Saving to Camera Roll

接下來南用,在 captureOutput(_:didOutput:from:) 中莹菱,緊接在初始化 detectFaceRectanglesRequest之前寻馏,添加以下內(nèi)容:

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

在這里,如果需要杖爽,您可以重置 isCapturingPhoto 標(biāo)志,并調(diào)用一個方法來保存帶有圖像緩沖區(qū)數(shù)據(jù)的護照照片紫皇。

最后慰安,編寫 savePassportPhoto(from:) 的實現(xiàn):

// 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) 首先聪铺,如果模型尚未設(shè)置化焕,請盡早return
  • 2) 接下來铃剔,分派到后臺隊列以保持 UI 流暢撒桨。
  • 3) 創(chuàng)建輸入圖像的core image表示和存儲輸出圖像的變量查刻。
  • 4) 然后,如果用戶請求刪除背景......
  • 5) 創(chuàng)建一個新的人物分割請求凤类,這次沒有完成處理程序穗泵。您希望護照照片的質(zhì)量盡可能好,因此請將質(zhì)量設(shè)置為accurate谜疤。這在這里有效佃延,因為您只處理單個圖像,并且您在后臺線程上執(zhí)行它夷磕。
  • 6) 執(zhí)行分割請求履肃。
  • 7) 同步讀取結(jié)果。如果存在掩碼像素緩沖區(qū)企锌,請從原始圖像中刪除背景榆浓。通過調(diào)用 removeBackgroundFrom(image:using:)來執(zhí)行此操作,將更準(zhǔn)確的掩碼傳遞給它撕攒。
  • 8) 此時陡鹃, outputImage 包含具有所需背景的護照照片。下一步是設(shè)置護照照片的寬度和高度抖坪。請記住萍鲸,護照照片的縱橫比可能與相機不同。
  • 9) 使用圖像的全寬和垂直中心計算照片的frame擦俐。
  • 10) 將輸出圖像(Core Image 對象)轉(zhuǎn)換為 Core Graphics 圖像脊阴。
  • 11) 然后,從core graphics圖像創(chuàng)建一個 UIImage蚯瞧。
  • 12) 調(diào)度回主線程并要求模型執(zhí)行保存照片操作嘿期。

完畢!

構(gòu)建并運行埋合。正確對齊您的臉部并在啟用和不啟用背景隱藏的情況下拍照备徐。拍照后,縮略圖將出現(xiàn)在頁腳的右側(cè)甚颂。單擊縮略圖將加載圖像的詳細視圖蜜猾。如果您打開照片Photos應(yīng)用程序,您還會發(fā)現(xiàn)您的照片已保存到相機中振诬。

請注意蹭睡,靜止圖像中的背景替換質(zhì)量如何優(yōu)于視頻源中的背景替換質(zhì)量。

仍有改進應(yīng)用程序的方法赶么。 例如肩豁,您可以查看使用 Core Imagesmile detector來防止微笑照片。 或者您可以在不隱藏背景時反轉(zhuǎn)蒙版以檢查真實背景是否為白色。

您還可以查看通過Combine流發(fā)布 hasDetectedValidFace蓖救。 通過限制流洪规,您可以阻止 UI 在面部處于可接受的邊緣時快速閃爍。

Apple documentation是了解更多有關(guān) Vision 框架的重要資源循捺。 如果您想了解有關(guān) Metal 的更多信息斩例,請嘗試這個出色的教程來幫助您入門。

后記

本篇主要講述了基于VisionFace Detection新特性从橘,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末念赶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恰力,更是在濱河造成了極大的恐慌叉谜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件踩萎,死亡現(xiàn)場離奇詭異停局,居然都是意外死亡,警方通過查閱死者的電腦和手機香府,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門董栽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人企孩,你說我怎么就攤上這事锭碳。” “怎么了勿璃?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵擒抛,是天一觀的道長。 經(jīng)常有香客問我补疑,道長歧沪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任莲组,我火速辦了婚禮槽畔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胁编。我一直安慰自己,他們只是感情好鳞尔,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布嬉橙。 她就那樣靜靜地躺著,像睡著了一般寥假。 火紅的嫁衣襯著肌膚如雪市框。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天糕韧,我揣著相機與錄音枫振,去河邊找鬼喻圃。 笑死,一個胖子當(dāng)著我的面吹牛粪滤,可吹牛的內(nèi)容都是我干的斧拍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杖小,長吁一口氣:“原來是場噩夢啊……” “哼肆汹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起予权,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤昂勉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扫腺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岗照,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年笆环,在試婚紗的時候發(fā)現(xiàn)自己被綠了攒至。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡咧织,死狀恐怖嗓袱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情习绢,我是刑警寧澤渠抹,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站闪萄,受9級特大地震影響梧却,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜败去,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一放航、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧圆裕,春花似錦广鳍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至行拢,卻和暖如春祖秒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工竭缝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留房维,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓抬纸,卻偏偏與公主長得像咙俩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子松却,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容