iOS视觉教程:身体和手部姿势检测

2,203 阅读12分钟

入门

使用此页面顶部或底部“下载材料”按钮下载的项目。然后,在Xcode中打开启动项目。

制作并运行。点击左上并欣赏照片。别角向那些星星许愿!

01.gif

星星下雨的魔力在StarAnimatorView.swift中。它使用 UIKit 动态 API。如果您有兴趣,请随时查看。

该应用程序看起来不错,但想象一下如果在后台显示您的实时视频会更好看!如果手机看不到手指,视觉就无法数出你的手指。

为检测做准备

视觉使用静止图像进行检测。信不信由你,您在相机取景器中看到的本质上是一连串静止图像。在检测到任何东西之前,您需要将相机会话集成到游戏中。

创建相机会话

要在应用程序中显示相机预览,请使用AVCaptureVideoPreviewLayer. 的子类CALayer。您将此预览层与捕获会话结合使用。

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

事实上,StarAnimator是SwiftUI 中的一个子类,UIViewRepresentable所以你可以使用它。StarAnimatorView``UIView

注意:您可以在这个精彩的视频课程中了解更多有关将 UIKit 与 SwiftUI 集成的信息:集成 UIKit 和 SwiftUI

您将在以下部分创建三个文件:CameraPreview.swiftCameraViewController.swiftCameraView.swift从CameraPreview.swift开始。

相机预览

StarCount组中创建一个名为CameraPreview.swift的新文件并添加:

// 1
导入UIKit
导入AVFoundation

final  class  CameraPreview : UIView {
   // 2
  覆盖 类 var  layerClass : AnyClass {
     AVCaptureVideoPreviewLayer . 自己
  }
  
  // 3 
  var previewLayer: AVCaptureVideoPreviewLayer {
    层为! AVCaptureVideoPreviewLayer 
  }
}

在这里,你:

  1. 导入UIKit以来CameraPreviewUIView. 您还可以导入AVFoundation,因为AVCaptureVideoPreviewLayer它是该模块的一部分。
  2. 接下来,您覆盖静态layerClass. 这使得该视图的根层类型为AVCaptureVideoPreviewLayer
  3. 然后,您创建一个名为的计算属性previewLayer,并将该视图的根层强制转换为您在第二步中定义的类型。现在,您可以在以后需要使用该层时直接使用此属性访问该层。

接下来,您将创建一个视图控制器来管理您的CameraPreview.

相机视图控制器

来自的相机捕获代码AVFoundation旨在UIKitUIViewControllerRepresentable.

StarCount组中创建CameraViewController.swift并添加:

导入UIKit

final  class  CameraViewController : UIViewController {
   // 1
  覆盖 func  loadView () {
    视图= 相机预览()
  }
  
  // 2 
  private  var cameraView: CameraPreview { view as!  相机预览}
}

你在这里:

  1. 覆盖loadView以使视图控制器CameraPreview用作其根视图。
  2. 创建一个计算属性,调用cameraPreview以访问根视图作为CameraPreview. 您可以安全地强制强制转换,因为您最近在第一步中分配了CameraPreviewto的实例。view

现在,您将制作一个 SwiftUI 视图来包装您的新视图控制器,以便您可以在 StarCount 中使用它。

相机视图

StarCount组中创建CameraView.swift并添加:

导入SwiftUI

// 1 
struct  CameraView : UIViewControllerRepresentable {
   // 2 
  func  makeUIViewController ( context : Context ) -> CameraViewController {
     let cvc =  CameraViewController ()
     return cvc
  }

  // 
  3 func  updateUIViewController (
     _uiViewController  : CameraViewController , 
     context :上下文
  ) {
  }
}

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

  1. 您创建一个名为的结构CameraView,它符合UIViewControllerRepresentable. 这是一个用于制作包装 UIKit 视图控制器的 SwiftUIView类型的协议。
  2. 您实现了第一个协议方法,makeUIViewController. 在这里,您初始化一个实例CameraViewController并执行任何一次仅设置。
  3. updateUIViewController(_: context:)是此协议的另一个必需方法,您可以在其中根据对 SwiftUI 数据或层次结构的更改对视图控制器进行任何更新。对于这个应用程序,您无需在此处执行任何操作。

完成所有这些工作之后,是时候使用CameraView.ContentView

打开ContentView.swift。在inCameraView开头插入:ZStack``body

相机视图()
  .edgesIgnoringSafeArea(.all)

呸!那是一个很长的部分。构建并运行以查看您的相机预览。

02.jpeg

哼!所有这些工作,没有任何改变!为什么?在相机预览工作之前,还有一个难题需要添加,一个AVCaptureSession. 接下来您将添加它。

连接到相机会话

您将在这里做出的改变看起来很长,但不要害怕。它们主要是样板代码。

打开CameraViewController.swift。在 之后添加以下内容import UIKit

导入AVFoundation 

然后,在类中添加一个 type 的实例属性AVCaptureSession

私有 变量cameraFeedSession: AVCaptureSession

当视图控制器出现在屏幕上时运行捕获会话并在视图不再可见时停止会话是一种很好的做法,因此添加以下内容:

覆盖 函数 viewDidAppear(_ 动画:布尔){
  超级.viewDidAppear(动画)
  
  do {
     // 1 
    if cameraFeedSession ==  nil {
       // 2
      尝试setupAVSession()
       // 3 
      cameraView.previewLayer.session = cameraFeedSession
      cameraView.previewLayer.videoGravity = .resizeAspectFill
    }
    
    // 4 
    cameraFeedSession ? .startRunning()
  }捕捉{
    打印(error.localizedDescription)
  }
}

// 5
覆盖 func  viewWillDisappear ( _animated  : Bool ) {
  cameraFeedSession ?.stopRunning()
  超级.viewWillDisappear(动画)
}

func  setupAVSession ()抛出{
}

下面是代码分解:

  1. viewDidAppear(_:)中,您检查是否已经初始化cameraFeedSession.
  2. 你调用setupAVSession(),它现在是空的,但你很快就会实现它。
  3. 然后,您将会话设置为ofsession并设置视频的调整大小模式。previewLayer``cameraView
  4. 接下来,您开始运行会话。这使相机馈送可见。
  5. viewWillDisappear(_:)中,关闭摄像头馈送以保持电池寿命并成为一个好公民。

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

准备相机

为 Vision 将在其上处理相机样本的调度队列添加一个新属性:

私有 让videoDataOutputQueue =  DispatchQueue (
  标签:“CameraFeedOutput”,
  服务质量:.userInteractive
)

添加扩展以使视图控制器符合AVCaptureVideoDataOutputSampleBufferDelegate

扩展 
CameraViewController : AVCaptureVideoDataOutputSampleBufferDelegate {
}

有了这两件事,您现在可以替换 empty setupAVSession()

func  setupAVSession () throws {
   // 1 
  guard  let videoDevice =  AVCaptureDevice .default(
    .builtInWideAngleCamera,
    对于:.video,
    位置:.front)
  否则{
    抛出 AppError .captureSessionSetup(
      原因:“找不到前置摄像头”
    )
  }

  // 2 
  guard  
    let deviceInput =  try?  AVCaptureDeviceInput (device: videoDevice)
   else {
     throw  AppError .captureSessionSetup(
      原因:“无法创建视频设备输入”
    )
  }

  // 3 
  let session =  AVCaptureSession ()
  session.beginConfiguration()
  session.sessionPreset =  AVCaptureSession预设.high

  // 4
  保护session.canAddInput(deviceInput) else {
     throw  AppError .captureSessionSetup(

```      原因:“无法将视频设备输入添加到会话”
    )
  }
  session.addInput(设备输入)

  // 5 
  let dataOutput =  AVCaptureVideoDataOutput ()
   if session.canAddOutput(dataOutput) {
    session.addOutput(数据输出)
    dataOutput.alwaysDiscardsLateVideoFrames =  true 
    dataOutput.setSampleBufferDelegate( self , queue: videoDataOutputQueue)
  } else {
    抛出 AppError .captureSessionSetup(
      原因:“无法将视频数据输出添加到会话”
    )
  }
  
  // 6
  session.commitConfiguration()
  cameraFeedSession =会话
}

在你上面的代码中:

  1. 检查设备是否有前置摄像头。如果没有,你会抛出一个错误。
  2. 接下来,检查您是否可以使用相机创建捕获设备输入。
  3. 创建一个捕获会话并开始使用高质量预设对其进行配置。
  4. 然后检查会话是否可以集成捕获设备输入。如果是,请将您在第二步中创建的输入添加到会话中。您需要输入和输出才能使会话正常工作。
  5. 接下来,创建一个数据输出并将其添加到会话中。数据输出将从相机源中获取图像样本,并在您之前设置的已定义调度队列上的委托中提供它们。
  6. 最后,完成会话配置并将其分配给您之前创建的属性。

构建并运行。现在你可以看到自己在下雨的星星后面。

03.png

注意:您需要用户权限才能访问设备上的相机。当您第一次启动相机会话时,iOS 会提示用户授予对相机的访问权限。你必须给用户一个你想要相机权限的理由。

Info.plist 中的键值对存储了原因。它已经存在于启动项目中。

有了这些,是时候进入 Vision 了。

检测手

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

  1. 请求:您请求框架通过定义请求特征为您检测某些内容。您使用适当的子类VNRequest.
  2. Handler:接下来,您要求框架在请求完成执行或处理请求后执行一个方法。
  3. 观察:最后,你会得到潜在的结果或观察结果。这些观察是VNObservation基于您提出的请求的实例。

您将首先处理请求。

要求

检测手的请求是 type VNDetectHumanHandPoseRequest

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

进口愿景

然后,在类定义中,创建此实例属性:

private  let handPoseRequest: VNDetectHumanHandPoseRequest  = {
   // 1 
  let request =  VNDetectHumanHandPoseRequest ()
  
  // 2 
  request.maximumHandCount =  2
  返回请求
}()

你在这里:

  1. 创建检测人手的请求。
  2. 将要检测的最大手数设置为两个。视觉框架功能强大。它可以检测图像中的许多手。由于任何一滴最多落下十颗星,两只手十根手指就足够了。

现在,是时候设置处理程序和观察了。

处理程序和观察

您可以使用AVCaptureVideoDataOutputSampleBufferDelegate从捕获流中获取样本并开始检测过程。

CameraViewController在您之前创建的扩展中实现此方法:

func  captureOutput(
   _ 输出:AVCaptureOutput, 
   didOutput  sampleBuffer:CMSampleBuffer, 
  来自 连接:AVCaptureConnection
) {
  // 1
  让handler =  VNImageRequestHandler (
    cmSampleBuffer:样本缓冲区,
    方向:.up,
    选项: [:]
  )

  do {
     // 2
    尝试handler.perform([handPoseRequest])

    // 3 
    guard  
      let results = handPoseRequest.results ? .前缀(2) 
       ,!results.isEmpty 
     else {
      返回
    }

    打印(结果)
  } catch {
     // 4 
    cameraFeedSession ? .stopRunning()
  }
}

下面是代码分解:

  1. captureOutput(_:didOutput:from:)每当有样本可用时调用。在此方法中,您将创建一个处理程序,这是使用 Vision 所需的第二步。您将获得的样本缓冲区作为输入参数传递以对单个图像执行请求。

  2. 然后,您执行请求。如果有任何错误,这个方法会抛出它们,所以它在一个 do-catch 块中。

    执行请求是一个同步操作。还记得您提供给委托回调的调度队列吗?这样可以确保您不会阻塞主队列。

    Vision 在该后台队列上完成检测过程。

  3. 您可以使用请求的results. 在这里,您获得了前两项并确保结果数组不为空。由于您在创建请求时只要求使用两只手,因此这是一项额外的预防措施,可确保您获得的结果项不会超过两个。 接下来,将结果打印到控制台。

  4. 如果请求失败,则意味着发生了不好的事情。在生产环境中,您会更好地处理此错误。现在,您可以停止摄像头会话。

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

04.png

14.jpg

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

05.gif

解剖救援!

视觉框架以详细的方式检测手。看看这个插图:

06.jpg

这张图片上的每个圆圈都是一个地标。视觉可以为每只手检测到总共 21 个地标:每个手指四个,拇指四个,手腕一个。

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

  • .thumb
  • .indexFinger
  • .middleFinger
  • .ringFinger
  • .littleFinger

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

  • 提示:指尖。
  • DIP:远端指间关节或指尖后的第一个关节。
  • PIP:近端指间关节或中间关节。
  • MIP:掌指关节位于手指底部,与手掌相连。

07.png

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

  • 提示:拇指尖。
  • IP:指间关节到拇指尖后的第一个关节。
  • MP:掌指关节位于拇指底部,与手掌相连。
  • CMC:腕掌关节靠近手腕。

08.jpg

许多开发人员认为他们的职业生涯不需要数学。谁会想到解剖学也是先决条件?

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

检测指尖

为简单起见,您将检测指尖并在顶部绘制叠加层。

CameraViewController.swift中,将以下内容添加到顶部captureOutput(_:didOutput:from:)

var指尖:[ CGPoint ] = []

这将存储检测到的指尖。现在将print(results)您在前面步骤中添加的 替换为:

变种识别点:[ VNRecognizedPoint ] = []

try results.forEach { 观察// 
  1
  让手指= 尝试观察.recognizedPoints(.all)

  // 2 
  if  let thumbTipPoint = finger[.thumbTip] {
    识别点.append(thumbTipPoint)
  }
  如果 让indexTipPoint =手指[.indexTip] {
    公认的Points.append(indexTipPoint)
  }
  如果 让middleTipPoint =手指[.middleTip] {
    识别点.append(middleTipPoint)
  }
  如果 让ringTipPoint =手指[.ringTip] {
    识别点.append(ringTipPoint)
  }
  如果 让littleTipPoint =手指[.littleTip] {
    识别点.append(littleTipPoint)
  }
}

// 3 
fingerTips = RecognizedPoints.filter {
   // 忽略低置信点。
  $0 .信心>  0.9
}
。地图 {
  // 4 
  CGPoint (x: $0 .location.x, y: 1  -  $0 .location.y)
}

你在这里:

  1. 获取所有手指的分数。
  2. 寻找提示点。
  3. 每个VNRecognizedPoint都有一个confidence。您只需要具有高置信度的观察结果。
  4. 视觉算法使用左下原点的坐标系,并返回相对于输入图像像素尺寸的归一化值。AVFoundation 坐标具有左上角的原点,因此您转换 y 坐标。

你需要用这些指尖做一些事情,所以将以下内容添加到CameraViewController

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

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

  // 3
  点处理器处理器? (转换点)
}

你在这里:

  1. 添加一个属性,让闭包在框架检测到点时运行。
  2. 从 AVFoundation 相对坐标转换为 UIKit 坐标,以便您可以在屏幕上绘制它们。您使用layerPointConverted,这是AVCaptureVideoPreviewLayer.
  3. 您使用转换后的点调用闭包。

captureOutput(_:didOutput:from:)中,在您声明该fingerTips属性之后,添加:

defer {
   DispatchQueue .main.sync {
     self .processPoints(fingerTips)
  }
}

一旦方法完成,这将发送您的指尖以在主队列上进行处理。

是时候向用户展示这些指尖了!

显示指尖

pointsProcessorHandler将在屏幕上获取您检测到的指纹。你必须将 SwiftUI 的闭包传递给这个视图控制器。

返回CameraView.swift并添加一个新属性:

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

这为您提供了将闭包存储在视图中的位置。

然后makeUIViewController(context:)通过在 return 语句之前添加这一行来更新:

cvc.pointsProcessorHandler = pointsProcessorHandler

这会将闭包传递给视图控制器。

打开ContentView.swift并将以下属性添加到视图定义中:

@State  private  var overlayPoints: [ CGPoint ] = []

此状态变量将保存在 中抓取的点CameraView。用以下内容替换该CameraView()行:

相机视图{
  叠加点=  $0
}

这个闭包是pointsProcessorHandler你之前添加的,当你有检测到的点时被调用。在闭包中,您将点分配给overlayPoints

最后,在修饰符之前添加这个edgesIgnoringSafeArea(.all)修饰符:

.overlay(
   FingersOverlay (with:overlayPoints)
    .foregroundColor(.orange)
)

您正在向CameraView添加叠加修饰符。在该修改器中,您使用检测到的点进行初始化FingersOverlay并将颜色设置为橙色。 FingersOverlay.swift在启动项目中。它唯一的工作是在屏幕上绘制点。

构建并运行。检查手指上的橙色圆点。移动你的手,注意点跟随你的手指。

09(1).gif

注意.overlay:如果需要,请随意更改修饰符中的颜色。

终于到了添加游戏逻辑的时候了。

添加游戏逻辑

虽然游戏的逻辑很长,但它非常简单。

打开GameLogicController.swift并将类实现替换为:

// 1
个私有 变量goalCount =  0

// 2 
@Published  var makeItRain =  false

// 3 
@Published  private(set)  var successBadge: Int ?

// 4
私有 变量shouldEvaluateResult =  true

// 5 
func  start () {
  makeItRain = 真
}

// 6 
func  didRainStars ( count : Int ) {
  目标计数=计数
}

// 7 
func  checkStarsCount ( _count  : Int ) {
   if !  应该评估结果 {
    返回
  }
  如果计数==目标计数{
    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 )
}

此代码向游戏引擎报告下雨星的数量。

接下来,您将让玩家知道他们的答案是正确的。

添加成功徽章

添加计算属性successBadge如下:

@ViewBuilder 
private  var successBadge: some  View {
   if  let number = gameLogicController.successBadge {
     Image (systemName: " \(number) .circle.fill" )
      .resizable()
      .imageScale(.large)
      .foregroundColor(.white)
      .frame(宽度:200,高度:200.shadow(半径:5)
  }其他{
    空视图()
  }
}

如果successBadge游戏逻辑控制器的 具有值,则使用 SFSymbols 中可用的系统映像创建映像。否则,您返回一个EmptyView,这意味着什么都不画。

将这两个修饰符添加到 root ZStack

.onAppear {
   // 1
  游戏逻辑控制器.start()
}
。覆盖(
  // 2
  成功徽章
    .animation(.default)
)

这是您添加的内容:

  1. 当游戏的起始页面出现时,您就开始了游戏。
  2. 你在一切之上绘制成功徽章。接下来successBadge是实施。

接下来,删除的覆盖,因为它现在会自动下雨。

最后一步

要使游戏正常运行,您需要将检测到的点数传递给游戏引擎。CameraView更新在ContentView中初始化时传递的闭包:

相机视图{
  overlayPoints =  $0 
  gameLogicController.checkStarsCount( $0 .count )
}

构建并运行。玩的开心。

10(1).gif

更多用例

您几乎没有触及 Vision 中手部和身体检测 API 的表面。该框架可以检测几个身体标志,如下图所示:

11.jpg

以下是您可以使用这些 API 执行哪些操作的一些示例:

  • 使用 Vision 框架在您的应用中安装 UI 控件。例如,某些相机应用程序包含可让您显示手势以拍照的功能。
  • 构建一个有趣的表情符号应用程序,用户可以在其中用手显示表情符号。
  • 构建一个锻炼分析应用程序,用户可以在其中发现他或她是否正确地执行了特定操作。
  • 构建一个音乐应用程序来教用户弹吉他或尤克里里。

12.jpg

13.jpg

Vision 的可能性是无限的。

结论

您可以使用教程中的所有代码部分下载最终项目。

这里也推荐一些面试相关的内容!