版本記錄
版本號 | 時間 |
---|---|
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
。這個實用程序類只有一個目的——成為 CameraViewController
中 AVCaptureVideoDataOutput
設(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ù)空間中報告痊班。使用VNDetectFaceRectanglesRequestRevision2
,roll 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:)
中,將 detectFaceRectanglesRequest
的revision
屬性更新為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ì)量在 0
到 1
之間變化堪侯。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) 更新
faceDetectedState
和faceQualityState
以記錄人臉檢測炎疆。 質(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)用detectedFaceQualityRequest
的completion 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有所了解啃洋。
Metal
是 Apple
提供的一個非常強大的 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 Image
的smile detector來防止微笑照片。 或者您可以在不隱藏背景時反轉(zhuǎn)蒙版以檢查真實背景是否為白色。
您還可以查看通過Combine流發(fā)布 hasDetectedValidFace
蓖救。 通過限制流洪规,您可以阻止 UI 在面部處于可接受的邊緣時快速閃爍。
Apple documentation是了解更多有關(guān) Vision
框架的重要資源循捺。 如果您想了解有關(guān) Metal
的更多信息斩例,請嘗試這個出色的教程來幫助您入門。
后記
本篇主要講述了基于
Vision
的Face Detection
新特性从橘,感興趣的給個贊或者關(guān)注~~~