[iOS] WWDC2020で紹介されたVision frameworkのbody pose検出を使ってみる

Vision frameworkを使ってbody poseを検出する。
2020.12.06

ビジュアルデータの中に存在している人間を識別し理解するためのツールを提供するVision フレームワークを利用することで、body poseを検出する機能をアプリケーションで活用できます。

iOS 14とmacOS 11から、Vision frameworkにhand pose detectionに加えてbody pose detectionという機能が追加されました。下の図に示すように、最大19のユニークなボディポイントを検出します。

hand pose detectionについては過去に記事を書いています。

VisionのBody Pose Detectionに関するセッション

WWDC2020でDetect Body and Hand Pose with Vision - WWDC 2020 - Videos - Apple Developerというセッションが公開されました。

このセッションではVision frameworkで新たに提供された新機能が紹介されました。Hand poseと合わせてBody poseの検出が出来るようになったこと、それによりどのようなアプリが提供できるか、APIの簡単な使い方などが紹介されました。

複数人のボディーポーズの検出も可能になりました。

CreateMLのAction ClassificationにもBody poseを利用できます。

この機能についてはここでは扱いませんが以下のセッションが参考になります。

Explore the Action & Vision app - WWDC 2020 - Videos - Apple Developer

基本的なAPIの使い方

APIはHand pose detectionと似通っています。

VNImageRequestHandlerの作成、VNDetectHumanBodyPoseRequestを作成しリクエストハンドラで実行するだけで、ハンドポーズとの違いはhandがbodyに命名を変更しているだけです。

let allPoints = bodyPoseObservation.recongnizedPoints(forGroupKey: VNRecognizedPointGroupKeyAll)
let leftArmPoints = bodyPoseObservation.recognizedPoints(forGroupKey: VNRecognizedPointGroupKey.bodyLandmarkRegionKeyLeftArm) // 特定のランドマークの入手(左手)
let leftWristPoint = leftArmPoints[VNRecognizedPointKey.bodyLandmarkKeyLeftWrist] // 左手首のランドマークの入手

ランドマークについて

セッションでは検出できるランドマークについても説明がありました。

VNRecognizedPointGroupKey.bodyLandmarkRegionKeyFace(顔付近)のVNRecognizedPointKeyは3つです。

  • 鼻(bodyLandmarkKeyNose)
  • 両目(bodyLandmarkKeyLeftEye, bodyLandmarkKeyRightEye)
  • 両耳(bodyLandmarkKeyLeftEar, bodyLandmarkKeyRightEar)

VNRecognizedPointGroupKey.bodyLandmarkRegionKeyRightArm(右腕)のVNRecognizedPointKeyも3つです。

  • 肩(bodyLandmarkKeyRightShoulder)
  • 肘(bodyLandmarkKeyRightElbow)
  • 手首(bodyLandmarkKeyRightWrist)

VNRecognizedPointGroupKey.bodyLandmarkRegionKeyTorso(上半身の)VNRecognizedPointKeyは6つです。

  • 右肩(bodyLandmarkKeyLeftShoulder)
  • 左肩(bodyLandmarkKeyRightShoulder)
  • 肩の間、首元(bodyLandmarkKeyNeck)
  • 左股関節(bodyLandmarkKeyLeftHip)
  • 右股関節(bodyLandmarkKeyRightHip)
  • 股関節の間(bodyLandmarkKeyRoot)

VNRecognizedPointGroupKey.bodyLandmarkRegionKeyRightLeg(右足)のVNRecognizedPointKeyは3つです。

  • 右股関節(bodyLandmarkKeyRightHip)
  • 右ひざ(bodyLandmarkKeyRightKnee)
  • 右足首(bodyLandmarkKeyRightAnkle)

セッションで紹介された以上のランドマークを見ると複数のVNRecognizedPointGroupKeyに所属するVNRecognizedPointKeyがあることがわかります。

また、かがんでいる、逆さまの場合や、服装、 画面端付近などで精度の低下が考えられること、重なって隠れた場合にアルゴリズムが混乱する可能性があること(People occluding)などBody poseの制限事項についても説明がありました。

ARKitのBody poseとの比較

ARKitのARSessionでbody poseが検出できます。しかしVisionはポイントごとの信頼値(confidence プロパティ)を提供でき、更にVisionは静止画や動画でも使用できます。そのため、Visionはオフラインでの画像分析が可能です。ARKitのbody pose検出はライブモーションキャプチャに使用することが想定されているとセッションで話がされていました。Vtuber向けのアプリケーションにも利用例があります。

また、ARKitは背面カメラでしか使えません。

ARKitでしか利用できなかったbody pose検出の機能がVisionによってそれ以外のフレームワークを使用したアプリケーションでも利用できるようになることが大きなメリットであると紹介されていました。

最小限の実装をやってみる

Body poseはHand poseと違ってサンプルコードなどは提供されていませんでした。今回は最低限の知識で済むよう検出と検出した座標への点の描画のみをやってみました。

AVFoundationのAVCaptureVideoDataOutputを使ってカメラ入力のプレビューと1フレームごとの処理

ビデオデータ出力からサンプルバッファを受信し、ビデオデータ出力の状態を監視できるAVCaptureVideoDataOutputSampleBufferDelegateを使用します。

必要なのは新しいビデオフレームが書き込まれたことをデリゲートに通知するcaptureOutput(_:didOutput:from:)です。

extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
  func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    guard let delegate = delegate else { return }
    guard let pixelBuffer = sampleBuffer.imageBuffer else { return }

    // ピクセルバッファのベースアドレスをロック
    guard CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess
    else { return }
    var image: CGImage?

    // 指定されたピクセルバッファを使用して、Core Graphicsの画像作成
    VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &image)

    // ピクセルバッファのベースアドレスをアンロック
    CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)

    DispatchQueue.main.sync {
        delegate.videoCapture(self, didCaptureFrame: image)
    }
}

利用者側でのカメラのセットアップです。

// MARK: Private methods
override func viewDidLoad() {
    super.viewDidLoad()
    videoCapture.setUpAVCapture(completion: handleSetUpCompletion(_:))
}

private extension ViewController {
    func handleSetUpCompletion(_ error: Error?) {
        guard let error = error else {
            videoCapture.delegate = self
            videoCapture.startCapturing()
            return
        }
        print("Error詳細: \(error)")
    }
}

フレームごとの処理をVisionで利用するコードです。

// MARK: Conform VideoCaptureDelegate
extension ViewController: VideoCaptureDelegate {
    func videoCapture(_ videoCapture: VideoCapture, didCaptureFrame image: CGImage?) {
        guard let image = image else {
            print("Can not captured")
            return
        }
        currentFrame = image
        estimate(from: image)
    }
}

ユーザー定義のestimate(from:)でVisionによる画像の分析を行っています。Visionのコードはこの中に閉じていて他では利用していません。以下のコードで各フレームごとに受け取ったCGImageを元にしたリクエストハンドラを作成した後リクエストの作成。perform(_:)メソッドでリクエストの実行を行っています。

private var imageSize = CGSize.zero

func estimate(from cgImage: CGImage) {
    imageSize = CGSize(width: cgImage.width, height: cgImage.height)
    let requestHandler = VNImageRequestHandler(cgImage: cgImage)
    let request = VNDetectHumanBodyPoseRequest(completionHandler: handleBodyPoseDetection)

    do {
        try requestHandler.perform([request])
    } catch {
        print("Request cannot perform because \(error)")
    }
}

VNDetectHumanBodyPoseRequestに渡しているcompletionHandlerの実装は以下です。VNRecognizedPointsObservationが検出できていた時にprocessObservation(_:)で処理します。

ここでは検出したランドマークのポイントごとの信頼値を表すconfidenceを見て信頼出来るものについては正規化された座標空間から画像座標に点を投影するVNImagePointForNormalizedPointを使ってCGPointの配列を取り出します。

このCGPointの配列を使って点を描画すれば一つのフレームで検出したランドマークに点を描画できます。

func handleBodyPoseDetection(request: VNRequest, error: Error?) {
    guard let observations = request.results as? [VNRecognizedPointsObservation] else { return }

    if observations.count == 0 {
        guard let currentFrame = currentFrame else { return }
        let image = UIImage(cgImage: currentFrame)
        DispatchQueue.main.async {
            self.previewImageView.image = image
        }
    } else {
        observations.forEach { processObservation($0) }
    }
}

func processObservation(_ observation: VNRecognizedPointsObservation) {
    guard let recognizedPoints =
            try? observation.recognizedPoints(forGroupKey: .all) else {
        return
    }

    let imagePoints: [CGPoint] = recognizedPoints.values.compactMap {
        guard $0.confidence > 0 else { return nil}
        return VNImagePointForNormalizedPoint($0.location, Int(imageSize.width), Int(imageSize.height))
    }

    let image = currentFrame?.drawPoints(points: imagePoints)
    DispatchQueue.main.async {
        self.previewImageView.image = image
    }
}

iPhoneでアプリをビルドしてYoutubeの手近な動画を使ってbody poseを検出してみました。

まとめ

body poseを検出して体が重ならないフレームを探し描画するStromotion shotsなど面白そうなアプリが作れそうなVision frameworkの奥深さをセッションで知り、実際の実装では開発者がcomputer visionの知識を有していなくてもアプリで利用できるようにAPIが設計されていることを実感しました。

ただ、アプリで利用するためにはCoreGraphicsとAVFoundationについてももっと習熟しないと自由に扱うのは大変そうに感じたので、インプットとアウトプットを継続して行いたいです。

参考にしたリンク