版本记录
版本号 | 时间 |
---|---|
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
,感兴趣的给个赞或者关注~~~
网友评论