WWDC2020のセッションから見るVision FrameworkのHand pose

2020.09.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Visionフレームワークの主なテーマの一つに映像データの中で人を把握する手助けをするということがあります。今回のVision Frameworkのアップデートでそれまで提供されていたFace Detection、Face landmarks、Human detectorに加えてHand PoseとHuman body poseが提供されました。

ARKitから始まり関連するフレームワークについても1から学習を始めていて今回はVision Frameworkのセッションを観たのでレポート記事を書きます。また、セッションで紹介されたサンプルアプリの実装からHand poseのAPIを把握します。

セッション動画は以下です。

Detect Body and Hand Pose with Vision - WWDC 2020 - Videos - Apple Developer

Hand pose

デモではウクレレを弾く子供の手をトラッキング出来ることが示されています。

また、ハンドポーズの推測が可能になりました。このAPIを使ってどのようなアプリケーションを公開するかは開発者に委ねられていますが、セッションでは具体的なアプリをデモしつつAPIの利便性をアピールしていました。

  • ハンドポーズで自撮り棒の代替

  • 手を使った絵文字入力

どのようにHand poseを使うのか

アルゴリズムは他と同じパターンをリクエストする。ImageRequestHandlerというリクエストハンドラを使います。次にRequestを生成します。ここではVNDetectHumanHandPoseRequestというリクエストを生成します。VNDetectHumanHandPoseRequestのperformRequestsでハンドラにリクエストを提供します。完了後にobservation(この場合VNREcognizedPointsObservation)がrequests,resultsというプロパティに届きます。

landmarkがどのように表されるかについてはobservationの2Dのポイントを表すVNPointというクラスで表現されます。VNPointはlocationというCGPoint型のプロパティを持っていて、位置のxとy座標に直接アクセスが出来ます。

座標は他のVisionアルゴリズムと同じくlower left originを使って画像のpixel dimensionsに対して正規化された座標で返却されます。observationが位置だけでなくconfidenceも組み込まれている場合はVNDetectedPointオブジェクトも取得できます。VNPointにラベリングするシステムならVNRecognizedPointオブジェクトも取得可能です。

ハンドポーズでは最後のVNRecognizedPointオブジェクトがobservationを通じて返却されます。

Landmarkの辞書をリクエストするためrecognizedPoints(forGroupKey:)を呼び出します。引数には、今回全てのポイントが欲しいのでVRRecognizedPointGroupKeyAllを指定します。

let allPoints = handPoseObservation.recognizedPoints(forGroupKey: VRRecognizedPointGroupKeyAll)
// 人差し指の一部のポイントのみの場合は以下
let indexFingerPoints = handPoseObservation.recognizedPoints(forGroupKey: VRRecognizedPointGroupKey.handLandmarkRegionKeyIndexFinger)
// 人差し指の先端
llet indexTipPoint = indexFingerPoints[VRRecognizedPointGroupKey.handLandmarkKeyIndexTip)

Hand poseのLandmarkについてはスライドで説明がありました。指それぞれに4つと手首に1つの合計21個です。先程のコードで記載したVRRecognizedPointGroupKeyでこれらのLandmarkの内どれを検出するか指定します。

ここまでで紹介した実装でジェスチャを起点に絵を描くアプリが作れるようになります。

サンプルアプリケーションの実装

captureOutput(_:didOutput:from:)の実装

引数で受け取ったCMSampleBufferを使ってリクエストハンドラを生成します。ここでのリクエストハンドラの型はVNImageRequestHandlerです。

次にリクエストの実装です。リクエストはVNDetectHumanHandPoseRequestです。リクエストの結果が必要になるのでhandPoseRequestという変数で束縛されてます。手が検出されるとobservationが返却されます。これはVNDetectHumanHandPoseRequestのresultsから確認できます。

let thumbPoints = try observation.recognizedPoints(.thumb)
let indexFingerPoints = try observation.recognizedPoints(.indexFinger)

observationから以上のようなコードで親指のPointと人差し指のPointを取得します。さらにそこからそれぞれの指のTipPointを取得します。

guard let thumbTipPoint = thumbPoints[.thumbTip], let indexTipPoint = indexFingerPoints[.indexTip] else { return ]

検出されたポイントの精度を示す信頼度スコアを表しているconfidenceプロパティで一定の精度を下回るポイントは無視するように実装します。

guard thmbTipPoint.confidence > 0.3 && indexTipPoint.confidence > 0.3 else { return }

guard文で弾かれい程度の精度を持つ場合はVision座標からAVFoundationの座標に変換します。

thumbTip = CGPoint(x: thumbTipPoint.location.x, y: 1 - thumbTipPoint.location.y)
indexTip = CGPoint(x: indexTipPoint.location.x, y: 1 - indexTipPoint.location.y)

do-catchのエラーハンドリング実装でエラーの場合はエラーを通知するビューを表示するようになっていますが、このAPIの本筋に関わるところではないので省きます。

また、このメソッドはdeferでメインスレッドでprocessPoints(thumbTip:indexTip:)というメソッドを呼びだしています。

processPoints(thumbTip:indexTip:)

引数に渡された二つのPointをチェックします。2 秒以上観測がなかった場合は、内部的に実装しているGestureProcessorと命名されたステートマシンをリセットします。

その後の処理ではAVFoundationの座標からUIKitの座標にポイントを変換します。

let previewLayer = cameraView.previewLayer
let thumbPointConverted = previewLayer.layerPointConverted(fromCaptureDevicePoint: thumbPoint)
let indexPointConverted = previewLayer.layerPointConverted(fromCaptureDevicePoint: indexPoint)
     
// Process new points
gestureProcessor.processPointsPair((thumbPointConverted, indexPointConverted))

ステートマシンGestureProcessorのprocessPointsPair(_:)の実装

人差し指と親指の指先間の距離を見ます。

let distance = pointsPair.indexTip.distance(from: pointsPair.thumbTip)

ステートマシンが内部で持っているしきい値より低ければつまみ状態を表わすpincedか見込みつまみ状態を表すpossiblbePinchかを集めたエビデンスから決めます。

状態変化を扱うhandleGestureStateChange(state:)

gestureProcessorのlastProcessedPointsPairからどのケースに当たるか確認します。possiblePinch(先述の通り見込みつまみ状態を表す)かpossibleApart(見込み離れの状態を表す)の場合は検出したポイントを追跡しておきます。

evidenceBuffer.append(pointsPair)

ケースがつまみ状態を表すpincehdの場合はポイントを描画します。

for bufferedPoints in evidenceBuffer { 
  updatePath(with: bufferdPoints, islastPointsPair: false)
}
evidenceBuffer.removeAll()
updatePath(with: pointsPair, isLastPointsPair: true)

実機で動かしてみた

Vision APIで提供しているHand poseに関わるその他のAPI

  • maximumHandCount

maximumHandCountがVNDetectHumanHandPoseRequestから提供されていて検出する手の数を指定できます。デフォルトは2でそれ以上も可能ではありますが、その分パフォーマンスが低下します。

  • VNTrackObjectRequest

手のポーズより位置を追跡したいだけの場合ハンドポーズリクエストで手を検出して以降はVNTrackObjectRequestを使って動きが把握できます。

Hand poseをアプリ活用する際の注意点

以下のような場合、Hand poseの精度が悪くなる可能性があると話がされていました。

  • 画面端付近の手
  • カメラの撮影方向と平行な手
  • 手袋をした手
  • 足を手として検出してしまう場合

まとめ

今回のセッションでHand poseを扱うためのAPIについてセッションとコードリーディングをしながらいくつかのことを学んだので次はセッションで活用例として紹介されていたHand Emojiアプリを作ってみた記事を書こうと思います。