经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » iOS » 查看文章
利用 iOS 14 Vision 的手势估测功能 实作无接触即可滑动的 Tinder App
来源:cnblogs  作者:Julday  时间:2021/6/28 9:30:18  对本文有异议

Vision 框架在 2017 年推出,目的是为了让行动 App 开发者轻松利用电脑视觉演算法。具体来说,Vision 框架中包含了许多预先训练好的深度学习模型,同时也能充当包裹器 (wrapper) 来快速执行你客制化的 Core ML 模型。

Apple 在 iOS 13 推出了文字辨识 (Text Recognition) 和 VisionKit 来增强 OCR 之后,现在将重点转向了 iOS 14 Vision 框架中的运动与动作分类上。

在之前的文章中,我们说过 Vision 框架可以做轮廓侦测 (Contour Detection)、光流请求 (Optical Flow Request),并提供一系列离线影片处理 (offline video processing) 的工具。不过更重要的是,我们现在可以进行手部与身体姿势估测 (Hand and Body Pose Estimation) ,这无疑为扩增实境 (augmented reality) 与电脑视觉带来了更多可能性。

在这篇文章中,我们会以手势估测功能来建构一个 iOS App,在无接触 (touchless) 的情况下 ,App 也能够感应手势。

我之前已经发表过一篇文章,展示如何使用 ML Kit 的脸部侦测 API,来建构无接触滑动的 iOS App。我觉得这个雏型 (prototype) 非常好用,可以整合到像是 Tinder 或 Bumble 等这种约会 App 中。不过,这种方式可能会因为持续眨眼和转动头部,而造成眼睛疲劳或头痛。

因此,我们简单地扩展这个范例,透过手势代替触摸,来往左或往右滑动。毕竟近年来说,使用手机来生活得更懒惰、或是练习社交距离也是合理的。在我们深入研究之前,先来看看如何在 iOS 14 中创建一个视觉手势请求。

视觉手势估测

这个新的 VNDetectHumanHandPoseRequest,是一个基于影像的视觉请求,用来侦测一个人的手势。在型别为 VNHumanHandPoseObservation 的实例当中,这个请求会在每隻手上回传 21 个标记点 (Landmark Point)。我们可以设定 maximumHandCount 数值,来控制在视觉处理过程之中,每张帧最多可以侦测的数量。

我们可以简单地在实例中如此使用列举 (enum),来获得每隻手指的标记点阵列 (array):

  1. try observation.recognizedPoints(.thumb)
  2. try observation.recognizedPoints(.indexFinger)
  3. try observation.recognizedPoints(.middleFinger)
  4. try observation.recognizedPoints(.ringFinger)
  5. try observation.recognizedPoints(.littleFinger)

这裡也有一个手腕的标记点,位置就在手腕的中心点位置。它并不属于上述的任何群组,而是在 all群组之中。你可以透过下列方式获得它:

let wristPoints = try observation.recognizedPoints(.all)

我们拿到上述的标记点阵列后,就可以这样将每个点独立抽取出来:

  1. guard let thumbTipPoint = thumbPoints[.thumbTip],
  2. let indexTipPoint = indexFingerPoints[.indexTip],
  3. let middleTipPoint = middleFingerPoints[.middleTip],
  4. let ringTipPoint = ringFingerPoints[.ringTip],
  5. let littleTipPoint = littleFingerPoints[.littleTip],
  6. let wristPoint = wristPoints[.wrist]else {return}

thumbIPthumbMPthumbCMC 是可以在 thumb 群组中获取的其他标记点,这也适用于其他手指。

hand-landmarks

每个独立的标记点物件,都包含了它们在 AVFoundation 座标系统中的位置及 confidence 阀值 (threshold)。

接著,我们可以在点跟点之间找到距离或角度的资讯,来创建手势处理器。举例来说,在 Apple 的范例 App 中,他们计算拇指与食指指尖的距离,来创建一个捏 (pinch) 的手势。

开始动工

现在我们已经了解视觉手势请求的基础知识,可以开始深入研究如何实作了!

开启 Xcode 并创建一个新的 UIKit App,请确认你有将开发目标设定为 iOS 14,并在 Info.plist 设置 NSCameraUsageDescription 字串。

xcode-setting

我在前一篇文章介绍过如何建立一个带有动画的 Tinder 样式卡片,现在可以直接参考当时的最终程式码

同样地,你可以在这裡参考 StackContainerView.swift 类别的程式码,这个类别是用来储存多个 Tinder 卡片的。

利用 AVFoundation 设置相机

接下来,让我们利用 Apple 的 AVFoundation 框架来建立一个客制化相机。

以下是 ViewController.swift 档案的程式码:

  1. class ViewController: UIViewController, HandSwiperDelegate{
  2. //MARK: - Properties
  3. var modelData = [DataModel(bgColor: .systemYellow),
  4. DataModel(bgColor: .systemBlue),
  5. DataModel(bgColor: .systemRed),
  6. DataModel(bgColor: .systemTeal),
  7. DataModel(bgColor: .systemOrange),
  8. DataModel(bgColor: .brown)]
  9. var stackContainer : StackContainerView!
  10. var buttonStackView: UIStackView!
  11. var leftButton : UIButton!, rightButton : UIButton!
  12. var cameraView : CameraView!
  13. //MARK: - Init
  14. override func loadView() {
  15. view = UIView()
  16. stackContainer = StackContainerView()
  17. view.addSubview(stackContainer)
  18. configureStackContainer()
  19. stackContainer.translatesAutoresizingMaskIntoConstraints = false
  20. addButtons()
  21. configureNavigationBarButtonItem()
  22. addCameraView()
  23. }
  24. override func viewDidLoad() {
  25. super.viewDidLoad()
  26. title = "HandPoseSwipe"
  27. stackContainer.dataSource = self
  28. }
  29. private let videoDataOutputQueue = DispatchQueue(label: "CameraFeedDataOutput", qos: .userInteractive)
  30. private var cameraFeedSession: AVCaptureSession?
  31. private var handPoseRequest = VNDetectHumanHandPoseRequest()
  32. let message = UILabel()
  33. var handDelegate : HandSwiperDelegate?
  34. func addCameraView()
  35. {
  36. cameraView = CameraView()
  37. self.handDelegate = self
  38. view.addSubview(cameraView)
  39. cameraView.translatesAutoresizingMaskIntoConstraints = false
  40. cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  41. cameraView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  42. cameraView.widthAnchor.constraint(equalToConstant: 150).isActive = true
  43. cameraView.heightAnchor.constraint(equalToConstant: 150).isActive = true
  44. }
  45. //MARK: - Configurations
  46. func configureStackContainer() {
  47. stackContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  48. stackContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -60).isActive = true
  49. stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
  50. stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
  51. }
  52. func addButtons()
  53. {
  54. //full source of UI setup at the end of this article
  55. }
  56. @objc func onButtonPress(sender: UIButton){
  57. UIView.animate(withDuration: 2.0,
  58. delay: 0,
  59. usingSpringWithDamping: CGFloat(0.20),
  60. initialSpringVelocity: CGFloat(6.0),
  61. options: UIView.AnimationOptions.allowUserInteraction,
  62. animations: {
  63. sender.transform = CGAffineTransform.identity
  64. },
  65. completion: { Void in() })
  66. if let firstView = stackContainer.subviews.last as? TinderCardView{
  67. if sender.tag == 0{
  68. firstView.leftSwipeClicked(stackContainerView: stackContainer)
  69. }
  70. else{
  71. firstView.rightSwipeClicked(stackContainerView: stackContainer)
  72. }
  73. }
  74. }
  75. func configureNavigationBarButtonItem() {
  76. navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetTapped))
  77. }
  78. @objc func resetTapped() {
  79. stackContainer.reloadData()
  80. }
  81. override func viewDidAppear(_ animated: Bool) {
  82. super.viewDidAppear(animated)
  83. do {
  84. if cameraFeedSession == nil {
  85. cameraView.previewLayer.videoGravity = .resizeAspectFill
  86. try setupAVSession()
  87. cameraView.previewLayer.session = cameraFeedSession
  88. }
  89. cameraFeedSession?.startRunning()
  90. } catch {
  91. AppError.display(error, inViewController: self)
  92. }
  93. }
  94. override func viewWillDisappear(_ animated: Bool) {
  95. cameraFeedSession?.stopRunning()
  96. super.viewWillDisappear(animated)
  97. }
  98. func setupAVSession() throws {
  99. // Select a front facing camera, make an input.
  100. guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
  101. throw AppError.captureSessionSetup(reason: "Could not find a front facing camera.")
  102. }
  103. guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
  104. throw AppError.captureSessionSetup(reason: "Could not create video device input.")
  105. }
  106. let session = AVCaptureSession()
  107. session.beginConfiguration()
  108. session.sessionPreset = AVCaptureSession.Preset.high
  109. // Add a video input.
  110. guard session.canAddInput(deviceInput) else {
  111. throw AppError.captureSessionSetup(reason: "Could not add video device input to the session")
  112. }
  113. session.addInput(deviceInput)
  114. let dataOutput = AVCaptureVideoDataOutput()
  115. if session.canAddOutput(dataOutput) {
  116. session.addOutput(dataOutput)
  117. // Add a video data output.
  118. dataOutput.alwaysDiscardsLateVideoFrames = true
  119. dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
  120. dataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
  121. } else {
  122. throw AppError.captureSessionSetup(reason: "Could not add video data output to the session")
  123. }
  124. session.commitConfiguration()
  125. cameraFeedSession = session
  126. }
  127. }

在上面的程式码中包含了许多步骤,让我们一一来分析:

  • CameraView 是一个客制化的 UIView 类别,用来在画面上呈现相机的内容。之后我们会进一步讲解这个类别。
  • 我们会在 setupAVSession() 设置前置相机镜头,并将它设置为 AVCaptureSession 的输入。
  • 接著,我们在 AVCaptureVideoDataOutput 上呼叫 setSampleBufferDelegate

ViewController 类别要遵循 HandSwiperDelegate 协定:

  1. protocol HandSwiperDelegate {
  2. func thumbsDown()
  3. func thumbsUp()
  4. }

当侦测到手势后,我们将会触发相对应的方法。现在,让我们来看看要如何在捕捉到的影像中执行视觉请求。

在捕捉到的影像中执行视觉手势请求

在以下程式码中,我们为上述的 ViewController 创建了一个扩展 (extension),而这个扩展遵循 AVCaptureVideoDataOutputSampleBufferDelegate 协定:

  1. extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
  2. public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  3. var thumbTip: CGPoint?
  4. var wrist: CGPoint?
  5. let handler = VNImageRequestHandler(cmSampleBuffer: sampleBuffer, orientation: .up, options: [:])
  6. do {
  7. // Perform VNDetectHumanHandPoseRequest
  8. try handler.perform([handPoseRequest])
  9. guard let observation = handPoseRequest.results?.first else {
  10. cameraView.showPoints([])
  11. return
  12. }
  13. // Get points for all fingers
  14. let thumbPoints = try observation.recognizedPoints(.thumb)
  15. let wristPoints = try observation.recognizedPoints(.all)
  16. let indexFingerPoints = try observation.recognizedPoints(.indexFinger)
  17. let middleFingerPoints = try observation.recognizedPoints(.middleFinger)
  18. let ringFingerPoints = try observation.recognizedPoints(.ringFinger)
  19. let littleFingerPoints = try observation.recognizedPoints(.littleFinger)
  20. // Extract individual points from Point groups.
  21. guard let thumbTipPoint = thumbPoints[.thumbTip],
  22. let indexTipPoint = indexFingerPoints[.indexTip],
  23. let middleTipPoint = middleFingerPoints[.middleTip],
  24. let ringTipPoint = ringFingerPoints[.ringTip],
  25. let littleTipPoint = littleFingerPoints[.littleTip],
  26. let wristPoint = wristPoints[.wrist]
  27. else {
  28. cameraView.showPoints([])
  29. return
  30. }
  31. let confidenceThreshold: Float = 0.3
  32. guard thumbTipPoint.confidence > confidenceThreshold &&
  33. indexTipPoint.confidence > confidenceThreshold &&
  34. middleTipPoint.confidence > confidenceThreshold &&
  35. ringTipPoint.confidence > confidenceThreshold &&
  36. littleTipPoint.confidence > confidenceThreshold &&
  37. wristPoint.confidence > confidenceThreshold
  38. else {
  39. cameraView.showPoints([])
  40. return
  41. }
  42. // Convert points from Vision coordinates to AVFoundation coordinates.
  43. thumbTip = CGPoint(x: thumbTipPoint.location.x, y: 1 - thumbTipPoint.location.y)
  44. wrist = CGPoint(x: wristPoint.location.x, y: 1 - wristPoint.location.y)
  45. DispatchQueue.main.async {
  46. self.processPoints([thumbTip, wrist])
  47. }
  48. } catch {
  49. cameraFeedSession?.stopRunning()
  50. let error = AppError.visionError(error: error)
  51. DispatchQueue.main.async {
  52. error.displayInViewController(self)
  53. }
  54. }
  55. }
  56. }

值得注意的是,VNObservation 所回传的标记点是属于 Vision 座标系统的。我们必须将它们转换成 UIKit 座标,才能将它们绘制在萤幕上。

因此,我们透过以下方式将它们转换为 AVFoundation 座标:

wrist = CGPoint(x: wristPoint.location.x, y: 1 - wristPoint.location.y)

接著,我们将会把这些标记点传递给 processPoints 函式。为了精简流程,这裡我们只用了拇指指尖与手腕两个标记点来侦测手势。

以下是 processPoints 函式的程式码:

  1. func processPoints(_ points: [CGPoint?]) {
  2. let previewLayer = cameraView.previewLayer
  3. var pointsConverted: [CGPoint] = []
  4. for point in points {
  5. pointsConverted.append(previewLayer.layerPointConverted(fromCaptureDevicePoint: point!))
  6. }
  7. let thumbTip = pointsConverted[0]
  8. let wrist = pointsConverted[pointsConverted.count - 1]
  9. let yDistance = thumbTip.y - wrist.y
  10. if(yDistance > 50){
  11. if self.restingHand{
  12. self.restingHand = false
  13. self.handDelegate?.thumbsDown()
  14. }
  15. }else if(yDistance < -50){
  16. if self.restingHand{
  17. self.restingHand = false
  18. self.handDelegate?.thumbsUp()
  19. }
  20. }
  21. else{
  22. self.restingHand = true
  23. }
  24. cameraView.showPoints(pointsConverted)
  25. }

我们可以利用以下这行程式码,将 AVFoundation 座标转换为 UIKit 座标:

  1. previewLayer.layerPointConverted(fromCaptureDevicePoint: point!)

最后,我们会依据两个标记点之间的绝对阈值距离,触发对推叠卡片往左或往右滑动的动作。

我们利用 cameraView.showPoints(pointsConverted),在 CameraView 子图层上绘制一条连接两个标记点的直线。

以下是 CameraView 类别的完整程式码:

  1. import UIKit
  2. import AVFoundation
  3. class CameraView: UIView {
  4. private var overlayThumbLayer = CAShapeLayer()
  5. var previewLayer: AVCaptureVideoPreviewLayer {
  6. return layer as! AVCaptureVideoPreviewLayer
  7. }
  8. override class var layerClass: AnyClass {
  9. return AVCaptureVideoPreviewLayer.self
  10. }
  11. override init(frame: CGRect) {
  12. super.init(frame: frame)
  13. setupOverlay()
  14. }
  15. required init?(coder: NSCoder) {
  16. super.init(coder: coder)
  17. setupOverlay()
  18. }
  19. override func layoutSublayers(of layer: CALayer) {
  20. super.layoutSublayers(of: layer)
  21. if layer == previewLayer {
  22. overlayThumbLayer.frame = layer.bounds
  23. }
  24. }
  25. private func setupOverlay() {
  26. previewLayer.addSublayer(overlayThumbLayer)
  27. }
  28. func showPoints(_ points: [CGPoint]) {
  29. guard let wrist: CGPoint = points.last else {
  30. // Clear all CALayers
  31. clearLayers()
  32. return
  33. }
  34. let thumbColor = UIColor.green
  35. drawFinger(overlayThumbLayer, Array(points[0...1]), thumbColor, wrist)
  36. }
  37. func drawFinger(_ layer: CAShapeLayer, _ points: [CGPoint], _ color: UIColor, _ wrist: CGPoint) {
  38. let fingerPath = UIBezierPath()
  39. for point in points {
  40. fingerPath.move(to: point)
  41. fingerPath.addArc(withCenter: point, radius: 5, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
  42. }
  43. fingerPath.move(to: points[0])
  44. fingerPath.addLine(to: points[points.count - 1])
  45. layer.fillColor = color.cgColor
  46. layer.strokeColor = color.cgColor
  47. layer.lineWidth = 5.0
  48. layer.lineCap = .round
  49. CATransaction.begin()
  50. CATransaction.setDisableActions(true)
  51. layer.path = fingerPath.cgPath
  52. CATransaction.commit()
  53. }
  54. func clearLayers() {
  55. let emptyPath = UIBezierPath()
  56. CATransaction.begin()
  57. CATransaction.setDisableActions(true)
  58. overlayThumbLayer.path = emptyPath.cgPath
  59. CATransaction.commit()
  60. }
  61. }

最终成果

最终 App 的成果会是这样:

vision-framework-hand-pose-estimation-demo

结论

我们可以在许多情况下用到 Vision 新的手势估测请求,包括利用手势来进行自拍、绘制签名,甚至是辨识川普在演讲当中不同的手势。

你也可以将视觉请求与身体姿势请求串接在一起,用来建构更複杂的姿态。

你可以在 Github 储存库 参考这个专案的完整程式码。

这篇文章到此为止,感谢你的阅读!

文末推荐:iOS热门文集

原文链接:http://www.cnblogs.com/Julday/p/14918685.html

 友情链接:直通硅谷  点职佳

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号