Vision框架详细解析(十) —— 基于Vision的Body

2021-03-10  本文已影响0人  刀客传奇

版本记录

版本号 时间
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教程假定您具有SwiftUIUIKitCombine的工作知识。有关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结合使用。

由于CALayerUIKit的一部分,因此您需要创建一个包装器才能在SwiftUI中使用它。幸运的是,Apple提供了一种使用UIViewRepresentableUIViewControllerRepresentable的简便方法。

实际上,StarAnimator是一个UIViewRepresentable,因此您可以在SwiftUI中使用StarAnimatorViewUIView的子类)。

注意:您可以在以下精彩的视频课程中了解有关将UIKitSwiftUI集成的更多信息:Integrating UIKit & SwiftUI

您将在以下部分中创建三个文件:CameraPreview.swiftCameraViewController.swiftCameraView.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 
  }
}

在这里,您:

接下来,您将创建一个视图控制器来管理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 }
}

在这里你:

现在,您将制作一个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
  ) {
  }
}

这就是上面的代码中发生的事情:

完成所有这些工作之后,该在ContentView中使用CameraView了。

打开ContentView.swift。 在bodyZStack的开头插入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 {
}

以下是代码细分:

现在,您将添加缺少的代码以准备相机。

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
}

在您上面的代码中:

构建并运行。 现在,您可以看到自己在雨星的背后。

注意:您需要用户权限才能访问设备上的相机。 首次启动摄像头会话时,iOS会提示用户授予对摄像头的访问权限。 您必须向用户说明您希望获得摄像头许可的原因。

Info.plist中的键值对存储原因。 在入门项目中已经存在。

有了这一点之后,就该转移到Vision了。


Detecting Hands

要在Vision中使用任何算法,通常需要遵循以下三个步骤:

您将首先处理该请求。

1. Request

用于检测手的请求的类型为VNDetectHumanHandPoseRequest

仍在CameraViewController.swift中,在import AVFoundation之后添加以下内容以访问Vision框架:

import Vision

然后,在类定义内,创建以下实例属性:

private let handPoseRequest: VNDetectHumanHandPoseRequest = {
  // 1
  let request = VNDetectHumanHandPoseRequest()
  
  // 2
  request.maximumHandCount = 2
  return request
}()

在这里你:

现在,是时候设置处理handlerobservation了。

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()
  }
}

以下是代码细分:

构建并运行。将您的手放在相机前面,然后查看Xcode控制台。

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

3. Anatomy to the Rescue!

Vision框架会详细检测手。 查看以下插图:

此图像上的每个圆圈都是一个LandmarkVision可以检测到每只手的21landmarks:每个手指四个,拇指四个和手腕一个。

这些手指中的每个手指都在一个Joints Group中,由VNHumanHandPoseObservation.JointsGroupName中的API将其描述为:

在每个关节组中,每个关节都有一个名称:

拇指有点不同。 它有一个TIP,但其他关节具有不同的名称:

许多开发人员认为自己的职业不需要数学。 谁会想到解剖学也是前提?

了解了解剖结构,是时候检测指尖了。

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

在这里你:

您需要使用这些指尖进行操作,因此将以下内容添加到CameraViewController中:

// 1
var pointsProcessorHandler: (([CGPoint]) -> Void)?

func processPoints(_ fingerTips: [CGPoint]) {
  // 2
  let convertedPoints = fingerTips.map {
    cameraView.previewLayer.layerPointConverted(fromCaptureDevicePoint: $0)
  }

  // 3
  pointsProcessorHandler?(convertedPoints)
}

在这里你:

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
    }
  }
}

这是一个细分:

打开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)
)

这是您添加的内容:

接下来,删除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和这些特定的API有很多很棒的资源。 要更深入地探讨此主题,请尝试:

后记

本篇主要讲述了基于VisionBody DetectHand Pose,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读