美文网首页
Vision框架详细解析(十) —— 基于Vision的Body

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

作者: 刀客传奇 | 来源:发表于2021-03-10 09:35 被阅读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 
      }
    }
    

    在这里,您:

    • 1) 由于CameraPreviewUIView的子类,因此请导入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。 在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 {
    }
    

    以下是代码细分:

    • 1) 在viewDidAppear(_ :)中,检查是否已经初始化了cameraFeedSession
    • 2) 您调用setupAVSession(),该函数目前为空,但很快就会实现。
    • 3) 然后,将会话设置为cameraViewPreviewLayer的会话,并设置视频的调整大小模式。
    • 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框架功能强大。 它可以检测到图像中的许多手。 由于最多十颗星落在任何一滴中,因此两只手和十根手指就足够了。

    现在,是时候设置处理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()
      }
    }
    

    以下是代码细分:

    • 1) 只要有采样,就会调用captureOutput(_:didOutput:from :)。在此方法中,您将创建一个handler,这是使用Vision所需的第二步。您将获得的样本缓冲区(sample buffer)作为输入参数传递,以对单个图像执行请求。

    • 2) 然后,您执行请求。如果有任何错误,此方法将引发错误,因此它位于do-catch块中。执行请求是同步操作。还记得您提供给代理回调的调度队列吗?这样可以确保您不会阻塞主队列。Vision完成了该后台队列上的检测过程。

    • 3) 您可以使用请求的results获得检测结果或观察结果。在这里,您可以获得前两项,并确保结果数组不为空。当您在创建请求时只要求两只手时,这是一种额外的预防措施,可确保您得到的结果项不超过两个。接下来,将结果打印到控制台。

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

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

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

    3. Anatomy to the Rescue!

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

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

    这些手指中的每个手指都在一个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有很多很棒的资源。 要更深入地探讨此主题,请尝试:

    后记

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

    相关文章

      网友评论

          本文标题:Vision框架详细解析(十) —— 基于Vision的Body

          本文链接:https://www.haomeiwen.com/subject/boqiqltx.html