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

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

作者: 刀客传奇 | 来源:发表于2021-03-12 22:26 被阅读0次

    版本记录

    版本号 时间
    V1.0 2021.03.12 星期五

    前言

    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扫描(二)
    10. Vision框架详细解析(十) —— 基于Vision的Body Detect和Hand Pose(一)

    源码

    1. Swift

    首先看下工程组织结构

    接着就是源码了。

    1. AppMain.swift
    
    import SwiftUI
    
    @main
    struct AppMain: App {
      var body: some Scene {
        WindowGroup {
          TabView {
            ContentView()
              .tabItem {
                Label("StarCount", systemImage: "line.horizontal.star.fill.line.horizontal")
              }
          }
        }
      }
    }
    
    2. ContentView.swift
    
    import SwiftUI
    
    struct ContentView: View {
      @State private var overlayPoints: [CGPoint] = []
      @StateObject private var gameLogicController = GameLogicController()
    
      var body: some View {
        ZStack {
          CameraView {
            overlayPoints = $0
            gameLogicController.checkStarsCount($0.count)
          }
          .overlay(
            FingersOverlay(with: overlayPoints)
              .foregroundColor(.orange)
          )
          .edgesIgnoringSafeArea(.all)
    
          StarAnimator(makeItRain: $gameLogicController.makeItRain) {
            gameLogicController.didRainStars(count: $0)
          }
        }
        .onAppear {
          gameLogicController.start()
        }
        .overlay(
          successBadge
            .animation(.default)
        )
      }
    
      @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()
        }
      }
    }
    
    struct ContentView_Previews: PreviewProvider {
      static var previews: some View {
        ContentView()
      }
    }
    
    3. FingersOverlay.swift
    
    import SwiftUI
    
    struct FingersOverlay: Shape {
      let points: [CGPoint]
      private let pointsPath = UIBezierPath()
    
      init(with points: [CGPoint]) {
        self.points = points
      }
    
      func path(in rect: CGRect) -> Path {
        for point in points {
          pointsPath.move(to: point)
          pointsPath.addArc(withCenter: point, radius: 5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        }
    
        return Path(pointsPath.cgPath)
      }
    }
    
    4. GameLogicController.swift
    
    import Combine
    import Foundation
    
    final class GameLogicController: ObservableObject {
      private var goalCount = 0
    
      @Published var makeItRain = false
      @Published private(set) var successBadge: Int?
      private var shouldEvaluateResult = true
    
      func start() {
        makeItRain = true
      }
    
      func didRainStars(count: Int) {
        goalCount = count
      }
    
      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
          }
        }
      }
    }
    
    5. CameraViewController.swift
    
    import UIKit
    import AVFoundation
    import Vision
    
    final class CameraViewController: UIViewController {
      // swiftlint:disable:next force_cast
      private var cameraView: CameraPreview { view as! CameraPreview }
    
      private let videoDataOutputQueue = DispatchQueue(
        label: "CameraFeedOutput",
        qos: .userInteractive
      )
      private var cameraFeedSession: AVCaptureSession?
      private let handPoseRequest: VNDetectHumanHandPoseRequest = {
        let request = VNDetectHumanHandPoseRequest()
        request.maximumHandCount = 2
        return request
      }()
    
      var pointsProcessorHandler: (([CGPoint]) -> Void)?
    
      override func loadView() {
        view = CameraPreview()
      }
    
      override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        do {
          if cameraFeedSession == nil {
            try setupAVSession()
            cameraView.previewLayer.session = cameraFeedSession
            cameraView.previewLayer.videoGravity = .resizeAspectFill
          }
          cameraFeedSession?.startRunning()
        } catch {
          print(error.localizedDescription)
        }
      }
    
      override func viewWillDisappear(_ animated: Bool) {
        cameraFeedSession?.stopRunning()
        super.viewWillDisappear(animated)
      }
    
      func setupAVSession() throws {
        // Select a front facing camera, make an input.
        guard let videoDevice = AVCaptureDevice.default(
          .builtInWideAngleCamera,
          for: .video,
          position: .front)
        else {
          throw AppError.captureSessionSetup(
            reason: "Could not find a front facing camera."
          )
        }
    
        guard let deviceInput = try? AVCaptureDeviceInput(
          device: videoDevice
        ) else {
          throw AppError.captureSessionSetup(
            reason: "Could not create video device input."
          )
        }
    
        let session = AVCaptureSession()
        session.beginConfiguration()
        session.sessionPreset = AVCaptureSession.Preset.high
    
        // Add a video input.
        guard session.canAddInput(deviceInput) else {
          throw AppError.captureSessionSetup(
            reason: "Could not add video device input to the session"
          )
        }
        session.addInput(deviceInput)
    
        let dataOutput = AVCaptureVideoDataOutput()
        if session.canAddOutput(dataOutput) {
          session.addOutput(dataOutput)
          // Add a video data output.
          dataOutput.alwaysDiscardsLateVideoFrames = true
          dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
        } else {
          throw AppError.captureSessionSetup(
            reason: "Could not add video data output to the session"
          )
        }
        session.commitConfiguration()
        cameraFeedSession = session
      }
    
      func processPoints(_ fingerTips: [CGPoint]) {
        // Convert points from AVFoundation coordinates to UIKit coordinates.
        let convertedPoints = fingerTips.map {
          cameraView.previewLayer.layerPointConverted(fromCaptureDevicePoint: $0)
        }
        pointsProcessorHandler?(convertedPoints)
      }
    }
    
    extension
    CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
      func captureOutput(
        _ output: AVCaptureOutput,
        didOutput sampleBuffer: CMSampleBuffer,
        from connection: AVCaptureConnection
      ) {
        var fingerTips: [CGPoint] = []
    
        defer {
          DispatchQueue.main.sync {
            self.processPoints(fingerTips)
          }
        }
    
        let handler = VNImageRequestHandler(
          cmSampleBuffer: sampleBuffer,
          orientation: .up,
          options: [:]
        )
        do {
          // Perform VNDetectHumanHandPoseRequest
          try handler.perform([handPoseRequest])
    
          // Continue only when at least a hand was detected in the frame. We're interested in maximum of two hands.
          guard
            let results = handPoseRequest.results?.prefix(2),
            !results.isEmpty
          else {
            return
          }
    
          var recognizedPoints: [VNRecognizedPoint] = []
    
          try results.forEach { observation in
            // Get points for all fingers.
            let fingers = try observation.recognizedPoints(.all)
    
            // Look for tip points.
            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)
            }
          }
    
          fingerTips = recognizedPoints.filter {
            // Ignore low confidence points.
            $0.confidence > 0.9
          }
          .map {
            // Convert points from Vision coordinates to AVFoundation coordinates.
            CGPoint(x: $0.location.x, y: 1 - $0.location.y)
          }
        } catch {
          cameraFeedSession?.stopRunning()
          print(error.localizedDescription)
        }
      }
    }
    
    6. CameraPreview.swift
    
    import UIKit
    import AVFoundation
    
    final class CameraPreview: UIView {
      var previewLayer: AVCaptureVideoPreviewLayer {
        // swiftlint:disable:next force_cast
        layer as! AVCaptureVideoPreviewLayer
      }
    
      override class var layerClass: AnyClass {
        AVCaptureVideoPreviewLayer.self
      }
    }
    
    7. CameraView.swift
    
    import SwiftUI
    
    struct CameraView: UIViewControllerRepresentable {
      var pointsProcessorHandler: (([CGPoint]) -> Void)?
    
      func makeUIViewController(context: Context) -> CameraViewController {
        let cvc = CameraViewController()
        cvc.pointsProcessorHandler = pointsProcessorHandler
        return cvc
      }
    
      func updateUIViewController(
        _ uiViewController: CameraViewController,
        context: Context
      ) {
      }
    }
    
    8. StarAnimator.swift
    
    import SwiftUI
    
    struct StarAnimator: UIViewRepresentable {
      final class Coordinator: NSObject, StarAnimatorDelegate {
        var parent: StarAnimator
    
        init(_ parent: StarAnimator) {
          self.parent = parent
        }
    
        func didStartRaining(count: Int) {
          parent.makeItRain = false
          parent.numberOfStarsHandler(count)
        }
      }
    
      @Binding var makeItRain: Bool
      var numberOfStarsHandler: (Int) -> Void
    
      func makeUIView(context: Context) -> StarAnimatorView {
        let uivc = StarAnimatorView()
        uivc.delegate = context.coordinator
        return uivc
      }
    
      func updateUIView(_ uiView: StarAnimatorView, context: Context) {
        if makeItRain {
          uiView.rain(shouldClearCanvas: true)
        }
      }
    
      func makeCoordinator() -> Coordinator {
        Coordinator(self)
      }
    }
    
    9. StarAnimatorView.swift
    
    import UIKit
    
    protocol StarAnimatorDelegate: class {
      func didStartRaining(count: Int)
    }
    
    final class StarAnimatorView: UIView {
      private enum Constants {
        static let animationDelay: TimeInterval = 0.1
        static let defaultAnimationDuration: TimeInterval = 0.5
        static let cleanupDelay: TimeInterval = 5
        static let starWidth: CGFloat = 16
        static let maxStarCount: Int = 10
      }
    
      weak var delegate: StarAnimatorDelegate?
    
      private var cleanupWorkItem: DispatchWorkItem?
      private lazy var animator: UIDynamicAnimator = {
        UIDynamicAnimator(referenceView: self)
      }()
    
      private lazy var collision: UICollisionBehavior = {
        let behavior = UICollisionBehavior(items: [])
        behavior.addBoundary(
          withIdentifier: "bottom" as NSCopying,
          from: CGPoint(x: 0, y: bounds.size.height),
          to: CGPoint(x: bounds.size.width, y: bounds.size.height)
        )
        behavior.addBoundary(
          withIdentifier: "right" as NSCopying,
          from: CGPoint(x: bounds.size.width, y: 0),
          to: CGPoint(x: bounds.size.width, y: bounds.size.height)
        )
        behavior.addBoundary(
          withIdentifier: "left" as NSCopying,
          from: CGPoint(x: 0, y: 0),
          to: CGPoint(x: 0, y: bounds.size.height)
        )
        animator.addBehavior(behavior)
        return behavior
      }()
    
      private lazy var gravity: UIGravityBehavior = {
        let behavior = UIGravityBehavior(items: [])
        behavior.magnitude = 0.4
        animator.addBehavior(behavior)
        return behavior
      }()
    
      private var rotationBehaviors: [UIDynamicItemBehavior] = []
    
      func rain(shouldClearCanvas: Bool) {
        if frame.size.width == 0 {
          return
        }
    
        if shouldClearCanvas {
          cleanup()
        }
    
        cleanupWorkItem?.cancel()
        cleanupWorkItem = DispatchWorkItem { [weak self] in
          self?.cleanup()
        }
        if let item = cleanupWorkItem {
          DispatchQueue.main.asyncAfter(deadline: .now() + Constants.cleanupDelay, execute: item)
        }
    
        let starsCount = Int.random(in: 1...Constants.maxStarCount)
    
        for i in 0..<starsCount {
          let star = RoundedCollisionImageView(
            frame: CGRect(
              x: CGFloat.random(in: 0 ..< frame.size.width - Constants.starWidth),
              y: -Constants.starWidth,
              width: Constants.starWidth,
              height: Constants.starWidth
            )
          )
          star.tintColor = .systemYellow
          star.alpha = 0
    
          star.image = UIImage(systemName: "star.fill")
          star.preferredSymbolConfiguration =
            .init(scale: [UIImage.SymbolScale.large, .medium, .small].randomElement() ?? .medium)
          star.sizeToFit()
    
          star.layer.shadowColor = UIColor.white.cgColor
          star.layer.shadowOpacity = 0.3
          star.layer.shadowOffset = .zero
    
          addSubview(star)
    
          gravity.addItem(star)
          collision.addItem(star)
    
          let dynamicBehavior = UIDynamicItemBehavior(items: [star])
          dynamicBehavior.elasticity = 0.4
          dynamicBehavior.addAngularVelocity(CGFloat.random(in: -10...10), for: star)
          rotationBehaviors.append(dynamicBehavior)
    
          animator.addBehavior(dynamicBehavior)
    
          UIView.animate(withDuration: Constants.defaultAnimationDuration, delay: Constants.animationDelay * Double(i)) {
            star.alpha = 1
          }
        }
    
        print("Dropped Stars: \(starsCount)")
    
        delegate?.didStartRaining(count: starsCount)
      }
    
      @objc private func cleanup() {
        subviews.forEach {
          $0.removeFromSuperview()
        }
    
        rotationBehaviors.forEach {
          animator.removeBehavior($0)
        }
        rotationBehaviors.removeAll()
    
        while !gravity.items.isEmpty {
          gravity.removeItem(gravity.items[0])
        }
        while !collision.items.isEmpty {
          collision.removeItem(collision.items[0])
        }
      }
    }
    
    final private class RoundedCollisionImageView: UIImageView {
      override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
        .ellipse
      }
    }
    
    10. AppError.swift
    
    enum AppError: Error {
      case captureSessionSetup(reason: String)
    }
    

    后记

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

    相关文章

      网友评论

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

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