Vision框架詳細(xì)解析(十) —— 基于Vision的Body Detect和Hand Pose(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2021.03.10 星期三

前言

iOS 11+macOS 10.13+ 新出了Vision框架绽昼,提供了人臉識(shí)別念链、物體檢測蒂培、物體跟蹤等技術(shù)沙合,它是基于Core ML的奠伪。可以說是人工智能的一部分首懈,接下來幾篇我們就詳細(xì)的解析一下Vision框架绊率。感興趣的看下面幾篇文章。
1. Vision框架詳細(xì)解析(一) —— 基本概覽(一)
2. Vision框架詳細(xì)解析(二) —— 基于Vision的人臉識(shí)別(一)
3. Vision框架詳細(xì)解析(三) —— 基于Vision的人臉識(shí)別(二)
4. Vision框架詳細(xì)解析(四) —— 在iOS中使用Vision和Metal進(jìn)行照片堆疊(一)
5. Vision框架詳細(xì)解析(五) —— 在iOS中使用Vision和Metal進(jìn)行照片堆疊(二)
6. Vision框架詳細(xì)解析(六) —— 基于Vision的顯著性分析(一)
7. Vision框架詳細(xì)解析(七) —— 基于Vision的顯著性分析(二)
8. Vision框架詳細(xì)解析(八) —— 基于Vision的QR掃描(一)
9. Vision框架詳細(xì)解析(九) —— 基于Vision的QR掃描(二)

開始

首先看下主要內(nèi)容:

Vision框架的幫助下究履,了解如何檢測顯示在相機(jī)上的手指的數(shù)量即舌。內(nèi)容來自翻譯

接著看下寫作環(huán)境:

Swift 5, iOS 14, Xcode 12

下面就是正文啦挎袜。

機(jī)器學(xué)習(xí)(Machine learning)無處不在顽聂,因此當(dāng)Apple在2017年宣布其Core ML框架時(shí),這并不奇怪盯仪。CoreML附帶了許多工具紊搪,包括Vision(圖像分析框架)。視覺分析靜止圖像以檢測面部全景,讀取條形碼耀石,跟蹤物體等。多年來爸黄,Apple在此框架中添加了許多很酷的功能滞伟,包括2020年引入的Hand and Body Detection API。在本教程中炕贵,您將使用Vision框架中的這些Hand and Body Detection API為您帶來魔力一個(gè)名為StarCount的游戲梆奈。您將用手和手指計(jì)算從天上掉下來的星星的數(shù)量。

注意:此Vision教程假定您具有SwiftUI称开,UIKitCombine的工作知識(shí)亩钟。有關(guān)SwiftUI的更多信息乓梨,請(qǐng)參見SwiftUI: Getting Started

StarCount需要具有前置攝像頭的設(shè)備才能運(yùn)行清酥,因此您不能隨身攜帶模擬器扶镀。

最后,如果您可以將設(shè)備支撐在某個(gè)地方焰轻,那將很有幫助臭觉,您將需要雙手來匹配這些高數(shù)字!

在Xcode中打開starter項(xiàng)目辱志。

構(gòu)建并運(yùn)行胧谈。 點(diǎn)擊左上角的Rain,欣賞場景荸频。 不要忘了對(duì)那些星星的祝福!

星星下雨的魔力在StarAnimatorView.swift中客冈。它使用UIKit Dynamics API旭从。如果您有興趣,請(qǐng)隨時(shí)查看场仲。

該應(yīng)用程序看起來不錯(cuò)和悦,但可以想象一下,如果在后臺(tái)顯示您的實(shí)時(shí)視頻渠缕,效果會(huì)更好鸽素!如果手機(jī)看不到手指,Vision無法計(jì)數(shù)手指亦鳞。


Getting Ready for Detection

Vision使用靜止圖像進(jìn)行檢測馍忽。信不信由你,您在相機(jī)取景器中看到的實(shí)際上是一堆靜止圖像燕差。在檢測到任何東西之前遭笋,您需要將攝影機(jī)會(huì)話集成到游戲中。

1. Creating the Camera Session

要在應(yīng)用程序中顯示攝像機(jī)預(yù)覽徒探,請(qǐng)使用CALayer的子類AVCaptureVideoPreviewLayer瓦呼。您可以將此預(yù)覽層與capture session結(jié)合使用。

由于CALayerUIKit的一部分测暗,因此您需要?jiǎng)?chuàng)建一個(gè)包裝器才能在SwiftUI中使用它央串。幸運(yùn)的是,Apple提供了一種使用UIViewRepresentableUIViewControllerRepresentable的簡便方法碗啄。

實(shí)際上质和,StarAnimator是一個(gè)UIViewRepresentable,因此您可以在SwiftUI中使用StarAnimatorViewUIView的子類)稚字。

注意:您可以在以下精彩的視頻課程中了解有關(guān)將UIKitSwiftUI集成的更多信息:Integrating UIKit & SwiftUI侦另。

您將在以下部分中創(chuàng)建三個(gè)文件:CameraPreview.swiftCameraViewController.swiftCameraView.swift。 從CameraPreview.swift開始褒傅。

CameraPreview

StarCount組中創(chuàng)建一個(gè)名為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) 由于CameraPreviewUIView的子類殿托,因此請(qǐng)導(dǎo)入UIKit霹菊。 您還可以導(dǎo)入AVFoundation,因?yàn)?code>AVCaptureVideoPreviewLayer是此模塊的一部分支竹。
  • 2) 接下來旋廷,您覆蓋靜態(tài)layerClass。 這使得該視圖的根層類型為AVCaptureVideoPreviewLayer礼搁。
  • 3) 然后饶碘,創(chuàng)建一個(gè)稱為PreviewLayer的計(jì)算屬性,并將此視圖的根層強(qiáng)制轉(zhuǎn)換為您在第二步中定義的類型馒吴。 現(xiàn)在扎运,當(dāng)您以后需要使用它時(shí),可以使用此屬性直接訪問該層饮戳。

接下來豪治,您將創(chuàng)建一個(gè)視圖控制器來管理CameraPreview

CameraViewController

AVFoundation的相機(jī)捕獲代碼旨在與UIKit配合使用扯罐,因此要使其在您的SwiftUI應(yīng)用中正常工作负拟,您需要制作一個(gè)視圖控制器并將其包裝在UIViewControllerRepresentable中。

StarCount組中創(chuàng)建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) 創(chuàng)建一個(gè)名為cameraPreview的計(jì)算屬性掩浙,以CameraPreview的身份訪問根視圖。 您可以安全地在此處強(qiáng)制賦值秸歧,因?yàn)槟罱诘谝徊街蟹峙淞?code>CameraPreview實(shí)例給view涣脚。

現(xiàn)在,您將制作一個(gè)SwiftUI視圖以包裝新的視圖控制器寥茫,以便可以在StarCount中使用它遣蚀。

CameraView

StarCount組中創(chuàng)建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
  ) {
  }
}

這就是上面的代碼中發(fā)生的事情:

  • 1) 您創(chuàng)建一個(gè)名為CameraView的結(jié)構(gòu)體,該結(jié)構(gòu)體符合UIViewControllerRepresentable纱耻。 這是用于制作包裝UIKit視圖控制器的SwiftUI View類型的協(xié)議芭梯。
  • 2) 您實(shí)現(xiàn)第一個(gè)協(xié)議方法,makeUIViewController弄喘。 在這里玖喘,您將初始化CameraViewController的實(shí)例,并執(zhí)行一次僅一次的設(shè)置蘑志。
  • 3) updateUIViewController(_:context :)是該協(xié)議的另一個(gè)必需方法累奈,您可以在其中基于SwiftUI數(shù)據(jù)或?qū)哟谓Y(jié)構(gòu)的更改對(duì)視圖控制器進(jìn)行任何更新贬派。 對(duì)于此應(yīng)用,您無需在此處做任何事情澎媒。

完成所有這些工作之后搞乏,該在ContentView中使用CameraView了。

打開ContentView.swift戒努。 在bodyZStack的開頭插入CameraView

CameraView()
  .edgesIgnoringSafeArea(.all)

那是一個(gè)很長的部分请敦。 構(gòu)建并運(yùn)行以查看您的相機(jī)預(yù)覽。

所有的工作都沒有改變储玫! 為什么侍筛? 在相機(jī)預(yù)覽工作之前,還需要添加另一個(gè)難題撒穷,即AVCaptureSession匣椰。 接下來,您將添加該內(nèi)容端礼。

2. Connecting to the Camera Session

您將在此處進(jìn)行的更改似乎很長禽笑,但是請(qǐng)不要害怕。 它們大多是樣板代碼齐媒。

打開CameraViewController.swift。 在import UIKit之后添加以下內(nèi)容:

import AVFoundation 

然后纷跛,在類內(nèi)添加AVCaptureSession類型的實(shí)例屬性:

private var cameraFeedSession: AVCaptureSession?

最好在此視圖控制器出現(xiàn)在屏幕上時(shí)運(yùn)行capture session喻括,并在視圖不再可見時(shí)停止session,因此添加以下內(nèi)容:

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

以下是代碼細(xì)分:

  • 1) 在viewDidAppear(_ :)中贫奠,檢查是否已經(jīng)初始化了cameraFeedSession唬血。
  • 2) 您調(diào)用setupAVSession(),該函數(shù)目前為空唤崭,但很快就會(huì)實(shí)現(xiàn)拷恨。
  • 3) 然后,將會(huì)話設(shè)置為cameraViewPreviewLayer的會(huì)話谢肾,并設(shè)置視頻的調(diào)整大小模式腕侄。
  • 4) 接下來,您開始運(yùn)行會(huì)話芦疏。 這使camera feed可見冕杠。
  • 5) 在viewWillDisappear(_ :)中,關(guān)閉camera feed以延長電池壽命酸茴。

現(xiàn)在分预,您將添加缺少的代碼以準(zhǔn)備相機(jī)。

Preparing the Camera

為調(diào)度隊(duì)列添加一個(gè)新屬性薪捍,Vision將在該屬性上處理攝像機(jī)采樣:

private let videoDataOutputQueue = DispatchQueue(
  label: "CameraFeedOutput", 
  qos: .userInteractive
)

添加擴(kuò)展以使視圖控制器符合AVCaptureVideoDataOutputSampleBufferDelegate

extension 
CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
}

有了這兩件事之后笼痹,您現(xiàn)在可以替換空的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) 檢查設(shè)備是否帶有前置攝像頭配喳。 如果不是,則拋出錯(cuò)誤凳干。
  • 2) 接下來晴裹,檢查是否可以使用相機(jī)創(chuàng)建捕捉設(shè)備輸入(capture device input)
  • 3) 創(chuàng)建一個(gè)capture session并開始使用高質(zhì)量預(yù)設(shè)(high quality preset)進(jìn)行配置纺座。
  • 4) 然后檢查會(huì)話是否可以capture device輸入息拜。 如果是,請(qǐng)將您在第二步中創(chuàng)建的輸入添加到會(huì)話中净响。 您需要輸入和輸出才能使會(huì)話正常工作少欺。
  • 5) 接下來,創(chuàng)建數(shù)據(jù)輸出并將其添加到會(huì)話中馋贤。 數(shù)據(jù)輸出將從相機(jī)源中獲取圖像樣本赞别,并將它們提供給您在先前設(shè)置的已定義調(diào)度隊(duì)列中的委托中。
  • 6) 最后配乓,完成配置會(huì)話并將其分配給您之前創(chuàng)建的屬性仿滔。

構(gòu)建并運(yùn)行。 現(xiàn)在犹芹,您可以看到自己在雨星的背后崎页。

注意:您需要用戶權(quán)限才能訪問設(shè)備上的相機(jī)。 首次啟動(dòng)攝像頭會(huì)話時(shí)腰埂,iOS會(huì)提示用戶授予對(duì)攝像頭的訪問權(quán)限飒焦。 您必須向用戶說明您希望獲得攝像頭許可的原因。

Info.plist中的鍵值對(duì)存儲(chǔ)原因屿笼。 在入門項(xiàng)目中已經(jīng)存在牺荠。

有了這一點(diǎn)之后,就該轉(zhuǎn)移到Vision了驴一。


Detecting Hands

要在Vision中使用任何算法休雌,通常需要遵循以下三個(gè)步驟:

  • 1) Request:您通過定義請(qǐng)求特征來請(qǐng)求框架為您檢測到某些東西。 您使用VNRequest的適當(dāng)子類肝断。
  • 2) Handler:接下來杈曲,您要求框架在請(qǐng)求完成執(zhí)行或處理請(qǐng)求之后執(zhí)行一種方法。
  • 3) Observation:最后胸懈,您可以獲得潛在的結(jié)果或觀察結(jié)果鱼蝉。 這些觀察是基于您的請(qǐng)求的VNObservation的實(shí)例。

您將首先處理該請(qǐng)求箫荡。

1. Request

用于檢測手的請(qǐng)求的類型為VNDetectHumanHandPoseRequest魁亦。

仍在CameraViewController.swift中,在import AVFoundation之后添加以下內(nèi)容以訪問Vision框架:

import Vision

然后羔挡,在類定義內(nèi)洁奈,創(chuàng)建以下實(shí)例屬性:

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

在這里你:

  • 1) 創(chuàng)建一個(gè)檢測人手的請(qǐng)求间唉。
  • 2) 將要檢測的最大手?jǐn)?shù)設(shè)置為兩個(gè)。 Vision框架功能強(qiáng)大利术。 它可以檢測到圖像中的許多手呈野。 由于最多十顆星落在任何一滴中,因此兩只手和十根手指就足夠了印叁。

現(xiàn)在被冒,是時(shí)候設(shè)置處理handlerobservation了。

2. Handler and Observation

您可以使用AVCaptureVideoDataOutputSampleBufferDelegate從采集流中獲取樣本并開始檢測過程轮蜕。

在您之前創(chuàng)建的CameraViewController擴(kuò)展中實(shí)現(xiàn)此方法:

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

以下是代碼細(xì)分:

  • 1) 只要有采樣昨悼,就會(huì)調(diào)用captureOutput(_:didOutput:from :)。在此方法中跃洛,您將創(chuàng)建一個(gè)handler率触,這是使用Vision所需的第二步。您將獲得的樣本緩沖區(qū)(sample buffer)作為輸入?yún)?shù)傳遞汇竭,以對(duì)單個(gè)圖像執(zhí)行請(qǐng)求葱蝗。

  • 2) 然后,您執(zhí)行請(qǐng)求细燎。如果有任何錯(cuò)誤两曼,此方法將引發(fā)錯(cuò)誤,因此它位于do-catch塊中玻驻。執(zhí)行請(qǐng)求是同步操作悼凑。還記得您提供給代理回調(diào)的調(diào)度隊(duì)列嗎?這樣可以確保您不會(huì)阻塞主隊(duì)列击狮。Vision完成了該后臺(tái)隊(duì)列上的檢測過程佛析。

  • 3) 您可以使用請(qǐng)求的results獲得檢測結(jié)果或觀察結(jié)果益老。在這里彪蓬,您可以獲得前兩項(xiàng),并確保結(jié)果數(shù)組不為空捺萌。當(dāng)您在創(chuàng)建請(qǐng)求時(shí)只要求兩只手時(shí)档冬,這是一種額外的預(yù)防措施,可確保您得到的結(jié)果項(xiàng)不超過兩個(gè)桃纯。接下來酷誓,將結(jié)果打印到控制臺(tái)。

  • 4) 如果請(qǐng)求失敗态坦,則意味著發(fā)生了一些不好的事情盐数。在生產(chǎn)環(huán)境中,您可以更好地處理此錯(cuò)誤伞梯。目前玫氢,您可以停止攝像頭會(huì)話帚屉。

構(gòu)建并運(yùn)行。將您的手放在相機(jī)前面漾峡,然后查看Xcode控制臺(tái)攻旦。

在控制臺(tái)中,您將看到可見的VNHumanHandPoseObservation類型的觀察對(duì)象生逸。 接下來牢屋,您將從這些觀察結(jié)果中提取手指數(shù)據(jù)。 但是首先槽袄,您需要閱讀一下解剖學(xué)烙无!

3. Anatomy to the Rescue!

Vision框架會(huì)詳細(xì)檢測手。 查看以下插圖:

此圖像上的每個(gè)圓圈都是一個(gè)Landmark掰伸。Vision可以檢測到每只手的21個(gè)landmarks:每個(gè)手指四個(gè)皱炉,拇指四個(gè)和手腕一個(gè)。

這些手指中的每個(gè)手指都在一個(gè)Joints Group中狮鸭,由VNHumanHandPoseObservation.JointsGroupName中的API將其描述為:

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

在每個(gè)關(guān)節(jié)組中合搅,每個(gè)關(guān)節(jié)都有一個(gè)名稱:

  • TIP:指尖。
  • DIP:指間遠(yuǎn)端關(guān)節(jié)或指尖后的第一個(gè)關(guān)節(jié)歧蕉。
  • PIP:指間近關(guān)節(jié)或中間關(guān)節(jié)灾部。
  • MIP:掌指關(guān)節(jié)位于手指底部,與手掌相連惯退。

拇指有點(diǎn)不同赌髓。 它有一個(gè)TIP,但其他關(guān)節(jié)具有不同的名稱:

  • TIP:拇指尖催跪。
  • IP:指間關(guān)節(jié)至拇指尖后的第一個(gè)關(guān)節(jié)锁蠕。
  • MP:掌指關(guān)節(jié)位于拇指底部,與手掌相連懊蒸。
  • CMC:腕掌關(guān)節(jié)在手腕附近荣倾。

許多開發(fā)人員認(rèn)為自己的職業(yè)不需要數(shù)學(xué)。 誰會(huì)想到解剖學(xué)也是前提骑丸?

了解了解剖結(jié)構(gòu)舌仍,是時(shí)候檢測指尖了。

4. Detecting Fingertips

為簡單起見通危,您將檢測到指尖并在頂部繪制一個(gè)覆蓋圖铸豁。

CameraViewController.swift中,將以下內(nèi)容添加到captureOutput(_:didOutput:from :)的頂部:

var fingerTips: [CGPoint] = []

這將存儲(chǔ)檢測到的指尖菊碟。 現(xiàn)在节芥,將您在上一步中添加的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) 獲取所有手指的分?jǐn)?shù)。
  • 2) 尋找尖點(diǎn)逆害。
  • 3) 每個(gè)VNRecognizedPoint都有一個(gè)置信度confidence头镊。 您只需要具有高置信度的觀察值增炭。
  • 4) Vision算法使用左下原點(diǎn)的坐標(biāo)系,并返回相對(duì)于輸入圖像像素尺寸的歸一化值拧晕。 AVFoundation坐標(biāo)具有左上角的原點(diǎn)隙姿,因此您可以轉(zhuǎn)換y坐標(biāo)。

您需要使用這些指尖進(jìn)行操作厂捞,因此將以下內(nèi)容添加到CameraViewController中:

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

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

  // 3
  pointsProcessorHandler?(convertedPoints)
}

在這里你:

  • 1) 為閉包添加一個(gè)屬性输玷,以在框架檢測到點(diǎn)時(shí)運(yùn)行。
  • 2) 從AVFoundation相對(duì)坐標(biāo)轉(zhuǎn)換為UIKit坐標(biāo)靡馁,以便可以在屏幕上繪制它們欲鹏。 您使用layerPointConverted,這是AVCaptureVideoPreviewLayer中的一種方法臭墨。
  • 3) 您可以使用轉(zhuǎn)換后的點(diǎn)來調(diào)用閉包赔嚎。

captureOutput(_:didOutput:from :)中,在聲明fingerTips屬性之后胧弛,添加:

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

方法完成后尤误,這會(huì)將您的指尖發(fā)送到主隊(duì)列中進(jìn)行處理。

是時(shí)候向用戶展示這些指尖了结缚!

5. Displaying Fingertips

pointsProcessorHandler將在屏幕上獲取檢測到的指紋损晤。 您必須將閉包從SwiftUI傳遞到此視圖控制器。

返回CameraView.swift并添加一個(gè)新屬性:

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

這為您提供了在視圖中存儲(chǔ)閉包的位置红竭。

然后通過在return語句之前添加以下行來更新makeUIViewController(context :)

cvc.pointsProcessorHandler = pointsProcessorHandler

這會(huì)將閉包傳遞給視圖控制器尤勋。

打開ContentView.swift并將以下屬性添加到視圖定義:

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

此狀態(tài)變量將保存在CameraView中獲取的點(diǎn)。 用以下內(nèi)容替換CameraView()行:

CameraView {
  overlayPoints = $0
}

該閉包是您之前添加的pointsProcessorHandler茵宪,當(dāng)您檢測到點(diǎn)時(shí)會(huì)調(diào)用該閉包最冰。 在閉包中,將點(diǎn)分配給overlayPoints稀火。

最后暖哨,在edgesIgnoringSafeArea(.all)修飾符之前添加此修飾符:

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

您正在將疊加層修改器添加到CameraView。 在該修飾符內(nèi)憾股,使用檢測到的點(diǎn)初始化FingersOverlay并將顏色設(shè)置為橙色鹿蜀。FingersOverlay.swift在啟動(dòng)項(xiàng)目中箕慧。 它的唯一工作是在屏幕上繪制點(diǎn)服球。

構(gòu)建并運(yùn)行。 檢查手指上的橙色點(diǎn)颠焦。 移動(dòng)您的手斩熊,并注意點(diǎn)跟隨您的手指。

注意:如果需要伐庭,可以隨時(shí)在.overlay修改器中更改顏色粉渠。

終于可以添加游戲邏輯了分冈。


Adding Game Logic

游戲的邏輯很長,但是非常簡單霸株。

打開GameLogicController.swift并將類實(shí)現(xiàn)替換為:

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

這是一個(gè)細(xì)分:

  • 1) 此屬性存儲(chǔ)掉落的星星數(shù)雕沉。玩家必須通過顯示適當(dāng)?shù)氖种笖?shù)來猜測該值。
  • 2) 每當(dāng)將發(fā)布的屬性設(shè)置為true時(shí)去件,StarAnimator就會(huì)開始下雨坡椒。
  • 3) 如果玩家正確猜測掉落的星星數(shù)量,則可以為此分配目標(biāo)數(shù)尤溜。該值出現(xiàn)在屏幕上倔叼,指示成功。
  • 4) 此屬性可防止過多的評(píng)估宫莱。如果玩家正確猜出了該值丈攒,則此屬性將使評(píng)估停止。
  • 5) 游戲就是這樣開始的授霸。當(dāng)出現(xiàn)開始屏幕時(shí)巡验,您可以調(diào)用此命令。
  • 6) 當(dāng)StarAnimator落下特定數(shù)量的星星時(shí)碘耳,它將調(diào)用此方法以將目標(biāo)計(jì)數(shù)保存在游戲引擎中深碱。
  • 7) 這就是魔術(shù)發(fā)生的地方。只要有新的點(diǎn)藏畅,就可以調(diào)用此方法敷硅。首先檢查是否可以評(píng)估結(jié)果。如果猜測值正確愉阎,它將停止評(píng)估绞蹦,設(shè)置成功標(biāo)志值,并在三秒鐘后將引擎狀態(tài)重置為初始值榜旦。

打開ContentView.swift連接GameLogicController幽七。

將對(duì)StarAnimator的調(diào)用(包括其結(jié)尾的閉包)替換為:

StarAnimator(makeItRain: $gameLogicController.makeItRain) {
  gameLogicController.didRainStars(count: $0)
}

此代碼向游戲引擎報(bào)告下雨天的數(shù)量。

接下來溅呢,您將告知玩家正確的答案澡屡。

1. Adding a Success Badge

successBadge添加計(jì)算的屬性,如下所示:

@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中可用的系統(tǒng)映像來創(chuàng)建映像驶鹉。 否則,您將返回EmptyView铣墨,這意味著什么都沒有繪制室埋。

將這兩個(gè)修飾符添加到根ZStack中:

.onAppear {
  // 1
  gameLogicController.start()
}
.overlay(
  // 2
  successBadge
    .animation(.default)
)

這是您添加的內(nèi)容:

  • 1) 當(dāng)游戲的開始頁面出現(xiàn)時(shí),您開始游戲。
  • 2) 您將success badge置于一切之上姚淆。 接下來是successBadge實(shí)現(xiàn)孕蝉。

接下來,刪除Rain的疊加層腌逢,因?yàn)楝F(xiàn)在它會(huì)自動(dòng)下雨降淮。

2. Final Step

要使游戲正常運(yùn)行,您需要將檢測到的點(diǎn)數(shù)傳遞給游戲引擎搏讶。 更新在ContentView中初始化CameraView時(shí)傳遞的閉包:

CameraView {
  overlayPoints = $0
  gameLogicController.checkStarsCount($0.count)
}

構(gòu)建并運(yùn)行骤肛。 玩的開心。


More Use Cases

您幾乎剛剛涉及到Vision中的Hand and Body Detection APIs窍蓝。 該框架可以檢測到多個(gè)body landmarks腋颠,如下所示:

以下是您可以使用這些API進(jìn)行操作的一些示例:

  • 使用Vision框架在您的應(yīng)用中安裝UI控件。 例如吓笙,某些相機(jī)應(yīng)用程序包含一些功能淑玫,可讓您顯示手勢來拍照。
  • 構(gòu)建一個(gè)有趣的表情符號(hào)應(yīng)用程序面睛,使用戶可以用手顯示表情符號(hào)絮蒿。
  • 構(gòu)建一個(gè)鍛煉分析應(yīng)用程序,用戶可以在其中找到他或她是否在執(zhí)行特定的操作叁鉴。
  • 構(gòu)建一個(gè)音樂應(yīng)用程序土涝,教用戶彈吉他或夏威夷四弦琴。

Vision和這些特定的API有很多很棒的資源幌墓。 要更深入地探討此主題但壮,請(qǐng)嘗試:

后記

本篇主要講述了基于VisionBody DetectHand Pose胳施,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胸蛛,一起剝皮案震驚了整個(gè)濱河市谨湘,隨后出現(xiàn)的幾起案子妻枕,更是在濱河造成了極大的恐慌杭棵,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椿胯,死亡現(xiàn)場離奇詭異筷登,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)压状,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門仆抵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人种冬,你說我怎么就攤上這事镣丑。” “怎么了娱两?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵莺匠,是天一觀的道長。 經(jīng)常有香客問我十兢,道長趣竣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任旱物,我火速辦了婚禮遥缕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宵呛。我一直安慰自己单匣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布宝穗。 她就那樣靜靜地躺著户秤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逮矛。 梳的紋絲不亂的頭發(fā)上鸡号,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音须鼎,去河邊找鬼鲸伴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晋控,可吹牛的內(nèi)容都是我干的挑围。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼糖荒,長吁一口氣:“原來是場噩夢啊……” “哼杉辙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捶朵,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蜘矢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后综看,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體品腹,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年红碑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舞吭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泡垃。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖羡鸥,靈堂內(nèi)的尸體忽然破棺而出蔑穴,到底是詐尸還是另有隱情,我是刑警寧澤惧浴,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布存和,位于F島的核電站,受9級(jí)特大地震影響衷旅,放射性物質(zhì)發(fā)生泄漏捐腿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一柿顶、第九天 我趴在偏房一處隱蔽的房頂上張望茄袖。 院中可真熱鬧,春花似錦嘁锯、人聲如沸绞佩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽品山。三九已至,卻和暖如春烤低,著一層夾襖步出監(jiān)牢的瞬間肘交,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國打工扑馁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涯呻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓腻要,卻偏偏與公主長得像复罐,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子雄家,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容