版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2021.03.10 星期三 |
前言
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扫描(二)
开始
首先看下主要内容:
在
Vision框架的帮助下,了解如何检测显示在相机上的手指的数量。内容来自翻译。
接着看下写作环境:
Swift 5, iOS 14, Xcode 12
下面就是正文啦。
机器学习(Machine learning)无处不在,因此当Apple在2017年宣布其Core ML框架时,这并不奇怪。CoreML附带了许多工具,包括Vision(图像分析框架)。视觉分析静止图像以检测面部,读取条形码,跟踪物体等。多年来,Apple在此框架中添加了许多很酷的功能,包括2020年引入的Hand and Body Detection API。在本教程中,您将使用Vision框架中的这些Hand and Body Detection API为您带来魔力一个名为StarCount的游戏。您将用手和手指计算从天上掉下来的星星的数量。
注意:此
Vision教程假定您具有SwiftUI,UIKit和Combine的工作知识。有关SwiftUI的更多信息,请参见SwiftUI: Getting Started。
StarCount需要具有前置摄像头的设备才能运行,因此您不能随身携带模拟器。最后,如果您可以将设备支撑在某个地方,那将很有帮助,您将需要双手来匹配这些高数字!
在Xcode中打开starter项目。
构建并运行。 点击左上角的Rain,欣赏场景。 不要忘了对那些星星的祝福!

星星下雨的魔力在StarAnimatorView.swift中。它使用UIKit Dynamics API。如果您有兴趣,请随时查看。
该应用程序看起来不错,但可以想象一下,如果在后台显示您的实时视频,效果会更好!如果手机看不到手指,Vision无法计数手指。
Getting Ready for Detection
Vision使用静止图像进行检测。信不信由你,您在相机取景器中看到的实际上是一堆静止图像。在检测到任何东西之前,您需要将摄影机会话集成到游戏中。
1. Creating the Camera Session
要在应用程序中显示摄像机预览,请使用CALayer的子类AVCaptureVideoPreviewLayer。您可以将此预览层与capture session结合使用。
由于CALayer是UIKit的一部分,因此您需要创建一个包装器才能在SwiftUI中使用它。幸运的是,Apple提供了一种使用UIViewRepresentable和UIViewControllerRepresentable的简便方法。
实际上,StarAnimator是一个UIViewRepresentable,因此您可以在SwiftUI中使用StarAnimatorView(UIView的子类)。
注意:您可以在以下精彩的视频课程中了解有关将
UIKit与SwiftUI集成的更多信息:Integrating UIKit & SwiftUI。
您将在以下部分中创建三个文件:CameraPreview.swift,CameraViewController.swift和CameraView.swift。 从CameraPreview.swift开始。
CameraPreview
在StarCount组中创建一个名为CameraPreview.swift的新文件,然后添加:
// 1
import UIKit
import AVFoundation
final class CameraPreview: UIView {
// 2
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
// 3
var previewLayer: AVCaptureVideoPreviewLayer {
layer as! AVCaptureVideoPreviewLayer
}
}
在这里,您:
- 1) 由于
CameraPreview是UIView的子类,因此请导入UIKit。 您还可以导入AVFoundation,因为AVCaptureVideoPreviewLayer是此模块的一部分。 - 2) 接下来,您覆盖静态
layerClass。 这使得该视图的根层类型为AVCaptureVideoPreviewLayer。 - 3) 然后,创建一个称为
PreviewLayer的计算属性,并将此视图的根层强制转换为您在第二步中定义的类型。 现在,当您以后需要使用它时,可以使用此属性直接访问该层。
接下来,您将创建一个视图控制器来管理CameraPreview。
CameraViewController
AVFoundation的相机捕获代码旨在与UIKit配合使用,因此要使其在您的SwiftUI应用中正常工作,您需要制作一个视图控制器并将其包装在UIViewControllerRepresentable中。
在StarCount组中创建CameraViewController.swift并添加:
import UIKit
final class CameraViewController: UIViewController {
// 1
override func loadView() {
view = CameraPreview()
}
// 2
private var cameraView: CameraPreview { view as! CameraPreview }
}
在这里你:
- 1) 重写
loadView以使视图控制器将CameraPreview用作其根视图。 - 2) 创建一个名为
cameraPreview的计算属性,以CameraPreview的身份访问根视图。 您可以安全地在此处强制赋值,因为您最近在第一步中分配了CameraPreview实例给view。
现在,您将制作一个SwiftUI视图以包装新的视图控制器,以便可以在StarCount中使用它。
CameraView
在StarCount组中创建CameraView.swift并添加:
import SwiftUI
// 1
struct CameraView: UIViewControllerRepresentable {
// 2
func makeUIViewController(context: Context) -> CameraViewController {
let cvc = CameraViewController()
return cvc
}
// 3
func updateUIViewController(
_ uiViewController: CameraViewController,
context: Context
) {
}
}
这就是上面的代码中发生的事情:
- 1) 您创建一个名为
CameraView的结构体,该结构体符合UIViewControllerRepresentable。 这是用于制作包装UIKit视图控制器的SwiftUI View类型的协议。 - 2) 您实现第一个协议方法,
makeUIViewController。 在这里,您将初始化CameraViewController的实例,并执行一次仅一次的设置。 - 3)
updateUIViewController(_:context :)是该协议的另一个必需方法,您可以在其中基于SwiftUI数据或层次结构的更改对视图控制器进行任何更新。 对于此应用,您无需在此处做任何事情。
完成所有这些工作之后,该在ContentView中使用CameraView了。
打开ContentView.swift。 在body中ZStack的开头插入CameraView:
CameraView()
.edgesIgnoringSafeArea(.all)
那是一个很长的部分。 构建并运行以查看您的相机预览。

所有的工作都没有改变! 为什么? 在相机预览工作之前,还需要添加另一个难题,即AVCaptureSession。 接下来,您将添加该内容。
2. Connecting to the Camera Session
您将在此处进行的更改似乎很长,但是请不要害怕。 它们大多是样板代码。
打开CameraViewController.swift。 在import UIKit之后添加以下内容:
import AVFoundation
然后,在类内添加AVCaptureSession类型的实例属性:
private var cameraFeedSession: AVCaptureSession?
最好在此视图控制器出现在屏幕上时运行capture session,并在视图不再可见时停止session,因此添加以下内容:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
do {
// 1
if cameraFeedSession == nil {
// 2
try setupAVSession()
// 3
cameraView.previewLayer.session = cameraFeedSession
cameraView.previewLayer.videoGravity = .resizeAspectFill
}
// 4
cameraFeedSession?.startRunning()
} catch {
print(error.localizedDescription)
}
}
// 5
override func viewWillDisappear(_ animated: Bool) {
cameraFeedSession?.stopRunning()
super.viewWillDisappear(animated)
}
func setupAVSession() throws {
}
以下是代码细分:
- 1) 在
viewDidAppear(_ :)中,检查是否已经初始化了cameraFeedSession。 - 2) 您调用
setupAVSession(),该函数目前为空,但很快就会实现。 - 3) 然后,将会话设置为
cameraView的PreviewLayer的会话,并设置视频的调整大小模式。 - 4) 接下来,您开始运行会话。 这使
camera feed可见。 - 5) 在
viewWillDisappear(_ :)中,关闭camera feed以延长电池寿命。
现在,您将添加缺少的代码以准备相机。
Preparing the Camera
为调度队列添加一个新属性,Vision将在该属性上处理摄像机采样:
private let videoDataOutputQueue = DispatchQueue(
label: "CameraFeedOutput",
qos: .userInteractive
)
添加扩展以使视图控制器符合AVCaptureVideoDataOutputSampleBufferDelegate:
extension
CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
}
有了这两件事之后,您现在可以替换空的setupAVSession()了:
func setupAVSession() throws {
// 1
guard let videoDevice = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .front)
else {
throw AppError.captureSessionSetup(
reason: "Could not find a front facing camera."
)
}
// 2
guard
let deviceInput = try? AVCaptureDeviceInput(device: videoDevice)
else {
throw AppError.captureSessionSetup(
reason: "Could not create video device input."
)
}
// 3
let session = AVCaptureSession()
session.beginConfiguration()
session.sessionPreset = AVCaptureSession.Preset.high
// 4
guard session.canAddInput(deviceInput) else {
throw AppError.captureSessionSetup(
reason: "Could not add video device input to the session"
)
}
session.addInput(deviceInput)
// 5
let dataOutput = AVCaptureVideoDataOutput()
if session.canAddOutput(dataOutput) {
session.addOutput(dataOutput)
dataOutput.alwaysDiscardsLateVideoFrames = true
dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
} else {
throw AppError.captureSessionSetup(
reason: "Could not add video data output to the session"
)
}
// 6
session.commitConfiguration()
cameraFeedSession = session
}
在您上面的代码中:
- 1) 检查设备是否带有前置摄像头。 如果不是,则抛出错误。
- 2) 接下来,检查是否可以使用相机创建捕捉设备输入
(capture device input)。 - 3) 创建一个
capture session并开始使用高质量预设(high quality preset)进行配置。 - 4) 然后检查会话是否可以
capture device输入。 如果是,请将您在第二步中创建的输入添加到会话中。 您需要输入和输出才能使会话正常工作。 - 5) 接下来,创建数据输出并将其添加到会话中。 数据输出将从相机源中获取图像样本,并将它们提供给您在先前设置的已定义调度队列中的委托中。
- 6) 最后,完成配置会话并将其分配给您之前创建的属性。
构建并运行。 现在,您可以看到自己在雨星的背后。

注意:您需要用户权限才能访问设备上的相机。 首次启动摄像头会话时,iOS会提示用户授予对摄像头的访问权限。 您必须向用户说明您希望获得摄像头许可的原因。
Info.plist中的键值对存储原因。 在入门项目中已经存在。
有了这一点之后,就该转移到Vision了。
Detecting Hands
要在Vision中使用任何算法,通常需要遵循以下三个步骤:
- 1) Request:您通过定义请求特征来请求框架为您检测到某些东西。 您使用
VNRequest的适当子类。 - 2) Handler:接下来,您要求框架在请求完成执行或处理请求之后执行一种方法。
- 3) Observation:最后,您可以获得潜在的结果或观察结果。 这些观察是基于您的请求的
VNObservation的实例。
您将首先处理该请求。
1. Request
用于检测手的请求的类型为VNDetectHumanHandPoseRequest。
仍在CameraViewController.swift中,在import AVFoundation之后添加以下内容以访问Vision框架:
import Vision
然后,在类定义内,创建以下实例属性:
private let handPoseRequest: VNDetectHumanHandPoseRequest = {
// 1
let request = VNDetectHumanHandPoseRequest()
// 2
request.maximumHandCount = 2
return request
}()
在这里你:
- 1) 创建一个检测人手的请求。
- 2) 将要检测的最大手数设置为两个。
Vision框架功能强大。 它可以检测到图像中的许多手。 由于最多十颗星落在任何一滴中,因此两只手和十根手指就足够了。
现在,是时候设置处理handler 和 observation了。
2. Handler and Observation
您可以使用AVCaptureVideoDataOutputSampleBufferDelegate从采集流中获取样本并开始检测过程。
在您之前创建的CameraViewController扩展中实现此方法:
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// 1
let handler = VNImageRequestHandler(
cmSampleBuffer: sampleBuffer,
orientation: .up,
options: [:]
)
do {
// 2
try handler.perform([handPoseRequest])
// 3
guard
let results = handPoseRequest.results?.prefix(2),
!results.isEmpty
else {
return
}
print(results)
} catch {
// 4
cameraFeedSession?.stopRunning()
}
}
以下是代码细分:
1) 只要有采样,就会调用
captureOutput(_:didOutput:from :)。在此方法中,您将创建一个handler,这是使用Vision所需的第二步。您将获得的样本缓冲区(sample buffer)作为输入参数传递,以对单个图像执行请求。2) 然后,您执行请求。如果有任何错误,此方法将引发错误,因此它位于
do-catch块中。执行请求是同步操作。还记得您提供给代理回调的调度队列吗?这样可以确保您不会阻塞主队列。Vision完成了该后台队列上的检测过程。3) 您可以使用请求的
results获得检测结果或观察结果。在这里,您可以获得前两项,并确保结果数组不为空。当您在创建请求时只要求两只手时,这是一种额外的预防措施,可确保您得到的结果项不超过两个。接下来,将结果打印到控制台。4) 如果请求失败,则意味着发生了一些不好的事情。在生产环境中,您可以更好地处理此错误。目前,您可以停止摄像头会话。
构建并运行。将您的手放在相机前面,然后查看Xcode控制台。


在控制台中,您将看到可见的VNHumanHandPoseObservation类型的观察对象。 接下来,您将从这些观察结果中提取手指数据。 但是首先,您需要阅读一下解剖学!

3. Anatomy to the Rescue!
Vision框架会详细检测手。 查看以下插图:

此图像上的每个圆圈都是一个Landmark。Vision可以检测到每只手的21个landmarks:每个手指四个,拇指四个和手腕一个。
这些手指中的每个手指都在一个Joints Group中,由VNHumanHandPoseObservation.JointsGroupName中的API将其描述为:
.thumb.indexFinger.middleFinger.ringFinger.littleFinger
在每个关节组中,每个关节都有一个名称:
- TIP:指尖。
- DIP:指间远端关节或指尖后的第一个关节。
- PIP:指间近关节或中间关节。
- MIP:掌指关节位于手指底部,与手掌相连。

拇指有点不同。 它有一个TIP,但其他关节具有不同的名称:
- TIP:拇指尖。
- IP:指间关节至拇指尖后的第一个关节。
- MP:掌指关节位于拇指底部,与手掌相连。
- CMC:腕掌关节在手腕附近。

许多开发人员认为自己的职业不需要数学。 谁会想到解剖学也是前提?
了解了解剖结构,是时候检测指尖了。
4. Detecting Fingertips
为简单起见,您将检测到指尖并在顶部绘制一个覆盖图。
在CameraViewController.swift中,将以下内容添加到captureOutput(_:didOutput:from :)的顶部:
var fingerTips: [CGPoint] = []
这将存储检测到的指尖。 现在,将您在上一步中添加的print(results)替换为:
var recognizedPoints: [VNRecognizedPoint] = []
try results.forEach { observation in
// 1
let fingers = try observation.recognizedPoints(.all)
// 2
if let thumbTipPoint = fingers[.thumbTip] {
recognizedPoints.append(thumbTipPoint)
}
if let indexTipPoint = fingers[.indexTip] {
recognizedPoints.append(indexTipPoint)
}
if let middleTipPoint = fingers[.middleTip] {
recognizedPoints.append(middleTipPoint)
}
if let ringTipPoint = fingers[.ringTip] {
recognizedPoints.append(ringTipPoint)
}
if let littleTipPoint = fingers[.littleTip] {
recognizedPoints.append(littleTipPoint)
}
}
// 3
fingerTips = recognizedPoints.filter {
// Ignore low confidence points.
$0.confidence > 0.9
}
.map {
// 4
CGPoint(x: $0.location.x, y: 1 - $0.location.y)
}
在这里你:
- 1) 获取所有手指的分数。
- 2) 寻找尖点。
- 3) 每个
VNRecognizedPoint都有一个置信度confidence。 您只需要具有高置信度的观察值。 - 4)
Vision算法使用左下原点的坐标系,并返回相对于输入图像像素尺寸的归一化值。AVFoundation坐标具有左上角的原点,因此您可以转换y坐标。
您需要使用这些指尖进行操作,因此将以下内容添加到CameraViewController中:
// 1
var pointsProcessorHandler: (([CGPoint]) -> Void)?
func processPoints(_ fingerTips: [CGPoint]) {
// 2
let convertedPoints = fingerTips.map {
cameraView.previewLayer.layerPointConverted(fromCaptureDevicePoint: $0)
}
// 3
pointsProcessorHandler?(convertedPoints)
}
在这里你:
- 1) 为闭包添加一个属性,以在框架检测到点时运行。
- 2) 从
AVFoundation相对坐标转换为UIKit坐标,以便可以在屏幕上绘制它们。 您使用layerPointConverted,这是AVCaptureVideoPreviewLayer中的一种方法。 - 3) 您可以使用转换后的点来调用闭包。
在captureOutput(_:didOutput:from :)中,在声明fingerTips属性之后,添加:
defer {
DispatchQueue.main.sync {
self.processPoints(fingerTips)
}
}
方法完成后,这会将您的指尖发送到主队列中进行处理。
是时候向用户展示这些指尖了!
5. Displaying Fingertips
pointsProcessorHandler将在屏幕上获取检测到的指纹。 您必须将闭包从SwiftUI传递到此视图控制器。
返回CameraView.swift并添加一个新属性:
var pointsProcessorHandler: (([CGPoint]) -> Void)?
这为您提供了在视图中存储闭包的位置。
然后通过在return语句之前添加以下行来更新makeUIViewController(context :):
cvc.pointsProcessorHandler = pointsProcessorHandler
这会将闭包传递给视图控制器。
打开ContentView.swift并将以下属性添加到视图定义:
@State private var overlayPoints: [CGPoint] = []
此状态变量将保存在CameraView中获取的点。 用以下内容替换CameraView()行:
CameraView {
overlayPoints = $0
}
该闭包是您之前添加的pointsProcessorHandler,当您检测到点时会调用该闭包。 在闭包中,将点分配给overlayPoints。
最后,在edgesIgnoringSafeArea(.all)修饰符之前添加此修饰符:
.overlay(
FingersOverlay(with: overlayPoints)
.foregroundColor(.orange)
)
您正在将叠加层修改器添加到CameraView。 在该修饰符内,使用检测到的点初始化FingersOverlay并将颜色设置为橙色。FingersOverlay.swift在启动项目中。 它的唯一工作是在屏幕上绘制点。
构建并运行。 检查手指上的橙色点。 移动您的手,并注意点跟随您的手指。

注意:如果需要,可以随时在
.overlay修改器中更改颜色。
终于可以添加游戏逻辑了。
Adding Game Logic
游戏的逻辑很长,但是非常简单。
打开GameLogicController.swift并将类实现替换为:
// 1
private var goalCount = 0
// 2
@Published var makeItRain = false
// 3
@Published private(set) var successBadge: Int?
// 4
private var shouldEvaluateResult = true
// 5
func start() {
makeItRain = true
}
// 6
func didRainStars(count: Int) {
goalCount = count
}
// 7
func checkStarsCount(_ count: Int) {
if !shouldEvaluateResult {
return
}
if count == goalCount {
shouldEvaluateResult = false
successBadge = count
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.successBadge = nil
self.makeItRain = true
self.shouldEvaluateResult = true
}
}
}
这是一个细分:
- 1) 此属性存储掉落的星星数。玩家必须通过显示适当的手指数来猜测该值。
- 2) 每当将发布的属性设置为
true时,StarAnimator就会开始下雨。 - 3) 如果玩家正确猜测掉落的星星数量,则可以为此分配目标数。该值出现在屏幕上,指示成功。
- 4) 此属性可防止过多的评估。如果玩家正确猜出了该值,则此属性将使评估停止。
- 5) 游戏就是这样开始的。当出现开始屏幕时,您可以调用此命令。
- 6) 当
StarAnimator落下特定数量的星星时,它将调用此方法以将目标计数保存在游戏引擎中。 - 7) 这就是魔术发生的地方。只要有新的点,就可以调用此方法。首先检查是否可以评估结果。如果猜测值正确,它将停止评估,设置成功标志值,并在三秒钟后将引擎状态重置为初始值。
打开ContentView.swift连接GameLogicController。
将对StarAnimator的调用(包括其结尾的闭包)替换为:
StarAnimator(makeItRain: $gameLogicController.makeItRain) {
gameLogicController.didRainStars(count: $0)
}
此代码向游戏引擎报告下雨天的数量。
接下来,您将告知玩家正确的答案。
1. Adding a Success Badge
为successBadge添加计算的属性,如下所示:
@ViewBuilder
private var successBadge: some View {
if let number = gameLogicController.successBadge {
Image(systemName: "\(number).circle.fill")
.resizable()
.imageScale(.large)
.foregroundColor(.white)
.frame(width: 200, height: 200)
.shadow(radius: 5)
} else {
EmptyView()
}
}
如果游戏逻辑控制器的successBadge具有值,则可以使用SFSymbols中可用的系统映像来创建映像。 否则,您将返回EmptyView,这意味着什么都没有绘制。
将这两个修饰符添加到根ZStack中:
.onAppear {
// 1
gameLogicController.start()
}
.overlay(
// 2
successBadge
.animation(.default)
)
这是您添加的内容:
- 1) 当游戏的开始页面出现时,您开始游戏。
- 2) 您将
success badge置于一切之上。 接下来是successBadge实现。
接下来,删除Rain的叠加层,因为现在它会自动下雨。
2. Final Step
要使游戏正常运行,您需要将检测到的点数传递给游戏引擎。 更新在ContentView中初始化CameraView时传递的闭包:
CameraView {
overlayPoints = $0
gameLogicController.checkStarsCount($0.count)
}
构建并运行。 玩的开心。

More Use Cases
您几乎刚刚涉及到Vision中的Hand and Body Detection APIs。 该框架可以检测到多个body landmarks,如下所示:

以下是您可以使用这些API进行操作的一些示例:
- 使用
Vision框架在您的应用中安装UI控件。 例如,某些相机应用程序包含一些功能,可让您显示手势来拍照。 - 构建一个有趣的表情符号应用程序,使用户可以用手显示表情符号。
- 构建一个锻炼分析应用程序,用户可以在其中找到他或她是否在执行特定的操作。
- 构建一个音乐应用程序,教用户弹吉他或夏威夷四弦琴。


Vision和这些特定的API有很多很棒的资源。 要更深入地探讨此主题,请尝试:
- WWDC 2020 Video on Body and Hand Detection APIs:该视频包含更多示例。
-
Vision Framework Documentation:
Vision框架中所有可用功能的概述。 - Detecting Human Body Poses in Images:Apple提供的文档和示例应用程序。
- raywenderlich.com Forums:向我们很棒的社区寻求帮助。
后记
本篇主要讲述了基于
Vision的Body Detect和Hand Pose,感兴趣的给个赞或者关注~~~
