WWDC Quick Look 💓 By SwiftGGTeam
Detect Body and Hand Pose with Vision

Detect Body and Hand Pose with Vision

元の動画を見る

ハイライト

Vision は、2020 年に手のポーズと人間のポーズの検出を追加しました。リクエストは確実にキー ポイントを返すため、アプリはカメラ フレームを使用してピンチ ジェスチャを認識し、手の軌跡を描画し、連続的なボディ ポーズを Create ML アクション分類子に渡すことができます。

主要内容

以前は、アプリが写真内の人物を理解したい場合、最も一般的なエントリ ポイントは顔でした。 Vision にはすでに Face Detection (顔検出)、Face Landmarks (顔のキーポイント)、Human Torso Detection (人間の胴体検出) があります。これらの能力は、「顔が写っていますか?」には答えることができますが、「指が挟まれていますか?」には答えることが困難です。 「その人は投げていますか?」 「ジャンプの最高点はどのフレームですか?」

この WWDC 2020 セッションは、Vision の「People」テーマをさらに一歩進め、新しいものにします。VNDetectHumanHandPoseRequestそしてVNDetectHumanBodyPoseRequest。前者は片手で 21 個のランドマーク (キー ポイント) を返し、後者は人体の顔、腕、胴体、脚のキー ポイントを返します。どちらのタイプの結果も合格ですVNRecognizedPointsObservation各点には正規化された座標と信頼性が含まれています。

これにより、インタラクション方法が「ボタンをクリックする」から「アクションを見る」に変わります。手の姿勢の例では、アプリはフレームごとに親指の先端と人差し指の先端を読み取り、距離が十分に近づき、連続 3 フレームにわたって安定した後、線の描画を開始します。体の姿勢の例では、アプリは投げる前後の姿勢ウィンドウを保存し、それを次のように変換します。MLMultiArrayを作成し、それを Core ML モデルに渡して、スローのタイプを決定します。

セッションは境界線も与えます。手を画面の端に近づけたり、手のひらをカメラの方向と平行に向けたり、手袋を着用したり、足を手と誤認識したりすることはすべて、手の姿勢に影響を与えます。逆立ちしたり、腰をかがめたり、衣服を妨げたり、複数の人が互いにブロックしたり、画面の端に近づいたりした場合にも、体の姿勢は低下します。リアルタイム推論を行う場合は、古い機器の遅延にも注意する必要があります。フレームごとに体の姿勢をサンプリングするのが最善ですが、分類推論により頻度を減らし、カメラのバッファーの占有を避けることができます。

詳細

手のポーズ: 親指と人差し指をカメラのフレームから外す

(07:07) ハンド ジェスチャーのリアルタイム パスは簡単です。カメラコールバックを取得しますCMSampleBufferその後、コードが作成されますVNImageRequestHandler、埋め込むVNDetectHumanHandPoseRequest、最初から始めますVNRecognizedPointsObservation親指と人​​差し指の 2 セットのポイントを用意します。

extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        var thumbTip: CGPoint?
        var indexTip: CGPoint?

        defer {
            DispatchQueue.main.sync {
                self.processPoints(thumbTip: thumbTip, indexTip: indexTip)
            }
        }

        let handler = VNImageRequestHandler(cmSampleBuffer: sampleBuffer, orientation: .up, options: [:])
        do {
            // Perform VNDetectHumanHandPoseRequest
            try handler.perform([handPoseRequest])
            // Continue only when a hand was detected in the frame.
            // Since we set the maximumHandCount property of the request to 1, there will be at most one observation.
            guard let observation = handPoseRequest.results?.first as? VNRecognizedPointsObservation else {
                return
            }
            // Get points for thumb and index finger.
            let thumbPoints = try observation.recognizedPoints(forGroupKey: .handLandmarkRegionKeyThumb)
            let indexFingerPoints = try observation.recognizedPoints(forGroupKey: .handLandmarkRegionKeyIndexFinger)
            // Look for tip points.
            guard let thumbTipPoint = thumbPoints[.handLandmarkKeyThumbTIP], let indexTipPoint = indexFingerPoints[.handLandmarkKeyIndexTIP] else {
                return
            }
            // Ignore low confidence points.
            guard thumbTipPoint.confidence > 0.3 && indexTipPoint.confidence > 0.3 else {
                return
            }
            // Convert points from Vision coordinates to AVFoundation coordinates.
            thumbTip = CGPoint(x: thumbTipPoint.location.x, y: 1 - thumbTipPoint.location.y)
            indexTip = CGPoint(x: indexTipPoint.location.x, y: 1 - indexTipPoint.location.y)
        } catch {
            cameraFeedSession?.stopRunning()
            let error = AppError.visionError(error: error)
            DispatchQueue.main.async {
                error.displayInViewController(self)
            }
        }
    }
}

キーポイント:

  • VNImageRequestHandler(cmSampleBuffer:orientation:options:)カメラ フレームを最初に画像オブジェクトに変換せずに直接処理します。
  • handler.perform([handPoseRequest])ハンドジェスチャー要求を実行します。例maximumHandCount1 に設定すると、最大 1 つの観測値が処理されます。
  • recognizedPoints(forGroupKey:)指をグループ化することでポイントを獲得できます。ここでは親指と人差し指を取り上げます。
  • 例のみを使用します.handLandmarkKeyThumbTIPそして.handLandmarkKeyIndexTIP指先が2本。
  • confidence > 0.3信頼度の低い点をフィルタリングします。
  • ビジョン座標の原点は左下隅にあります。この例では、y を AVFoundation 座標に変換します。1 - y

(10:39) 画面内に複数の手が存在する可能性がある場合は、maximumHandCount結果の数と待ち時間に影響します。デフォルト値は 2 です。値が大きいほど、Vision はより多くの手のポーズを計算する必要があります。最も近いハンドまたは最大のハンドのみが必要な場合は、このパラメータを下げる方が適切です。

ピンチ ジェスチャー: 連続フレーム証拠でジッターを排除

(08:29) この例では、単一フレームの距離だけを調べているわけではありません。親指の先端と人差し指の先端の間の距離をしきい値と比較し、連続フレーム数を使用して状態を判断します。しきい値は 40 で、状態のトリガーには 3 フレームの証拠が必要です。

init(pinchMaxDistance: CGFloat = 40, evidenceCounterStateTrigger: Int = 3) {
        self.pinchMaxDistance = pinchMaxDistance
        self.evidenceCounterStateTrigger = evidenceCounterStateTrigger
    }

    func reset() {
        state = .unknown
        pinchEvidenceCounter = 0
        apartEvidenceCounter = 0
    }

    func processPointsPair(_ pointsPair: PointsPair) {
        lastProcessedPointsPair = pointsPair
        let distance = pointsPair.indexTip.distance(from: pointsPair.thumbTip)
        if distance < pinchMaxDistance {
            // Keep accumulating evidence for pinch state.
            pinchEvidenceCounter += 1
            apartEvidenceCounter = 0
            // Set new state based on evidence amount.
            state = (pinchEvidenceCounter >= evidenceCounterStateTrigger) ? .pinched : .possiblePinch
        } else {
            // Keep accumulating evidence for apart state.
            apartEvidenceCounter += 1
            pinchEvidenceCounter = 0
            // Set new state based on evidence amount.
            state = (apartEvidenceCounter >= evidenceCounterStateTrigger) ? .apart : .possibleApart
        }
    }

キーポイント:

  • pinchMaxDistanceはピンチしきい値で、例のデフォルトは 40 です。
  • pointsPair.indexTip.distance(from: pointsPair.thumbTip)これは、ジェスチャ判定のための唯一の幾何学的入力です。
  • 距離がしきい値未満の場合に蓄積されますpinchEvidenceCounter、同時にクリアapartEvidenceCounter
  • 距離がしきい値に達するか超えると逆累積apartEvidenceCounter
  • evidenceCounterStateTrigger単一フレームの誤検出による線画ステータスのジャンプを避けるために、連続 3 フレームが必要です。

(09:25) 状態に入りますpinched最後に、この例ではキャッシュ ポイントを描画パスに書き込みます。入力apartまたはunknown、キャッシュは破棄され、最後のパスが描画されます。

private func handleGestureStateChange(state: HandGestureProcessor.State) {
        let pointsPair = gestureProcessor.lastProcessedPointsPair
        var tipsColor: UIColor
        switch state {
        case .possiblePinch, .possibleApart:
            // We are in one of the "possible": states, meaning there is not enough evidence yet to determine
            // if we want to draw or not. For now, collect points in the evidence buffer, so we can add them
            // to a drawing path when required.
            evidenceBuffer.append(pointsPair)
            tipsColor = .orange
        case .pinched:
            // We have enough evidence to draw. Draw the points collected in the evidence buffer, if any.
            for bufferedPoints in evidenceBuffer {
                updatePath(with: bufferedPoints, isLastPointsPair: false)
            }
            // Clear the evidence buffer.
            evidenceBuffer.removeAll()
            // Finally, draw current point
            updatePath(with: pointsPair, isLastPointsPair: false)
            tipsColor = .green
        case .apart, .unknown:
            // We have enough evidence to not draw. Discard any evidence buffer points.
            evidenceBuffer.removeAll()
            // And draw the last segment of our draw path.
            updatePath(with: pointsPair, isLastPointsPair: true)
            tipsColor = .red
        }
        cameraView.showPoints([pointsPair.thumbTip, pointsPair.indexTip], color: tipsColor)
    }

キーポイント:

  • .possiblePinchそして.possibleApartこれは単なる過渡状態です。「詳細設定」をクリックします。evidenceBuffer
  • .pinchedキャッシュポイントとカレントポイントが一緒にパスに書き込まれ、着実に指が近づいてきました。
  • .apartそして.unknown誤って判断された軌跡が画面上に描画されるのを防ぐために、キャッシュがクリアされます。
  • tipsColorステータスを UI にフィードバックします (オレンジ色は待機中、緑色は描画中、赤色は停止中)。

体の姿勢: 同じビジョン モードで複数の人物を分析する

(14:26) 体の姿勢リクエストのモードは手の姿勢と一致しています: リクエストハンドラの作成、作成VNDetectHumanBodyPoseRequest、リクエストを実行してから開始します。VNRecognizedPointsObservation重要なポイントを読んでください。違いは、人体姿勢は写真内の複数の人物を分析し、顔、左腕、右腕、胴体、左脚、右脚などのグループキーに従ってランドマークを整理できることです。

(17:08) セッションでは、特に Vision と ARKit を比較します。どちらも同じ人間のランドマークのセットを返します。 Vision はポイントごとに信頼性があり、静止画像、カメラ フィード、オフライン ギャラリー分析を処理でき、AR セッションを必要としません。 ARKit は、背面カメラとサポート デバイスに依存するライブ モーション キャプチャ (リアルタイム モーション キャプチャ) により適しています。

(20:48) アクションとビジョンの例では、体の姿勢と軌道の検出を同じカメラ コールバックに入れています。スローイングトラッキング状態では、コードは軌道リクエストを実行します。同時に、実行し続けます。detectPlayerRequest、プレーヤーのフレームとジョイント オーバーレイをボディ ポーズの結果で更新します。

extension GameViewController: CameraViewControllerOutputDelegate {
    func cameraViewController(_ controller: CameraViewController, didReceiveBuffer buffer: CMSampleBuffer, orientation: CGImagePropertyOrientation) {
        let visionHandler = VNImageRequestHandler(cmSampleBuffer: buffer, orientation: orientation, options: [:])
        if self.gameManager.stateMachine.currentState is GameManager.TrackThrowsState {
            DispatchQueue.main.async {
                // Get the frame of rendered view
                let normalizedFrame = CGRect(x: 0, y: 0, width: 1, height: 1)
                self.jointSegmentView.frame = controller.viewRectForVisionRect(normalizedFrame)
                self.trajectoryView.frame = controller.viewRectForVisionRect(normalizedFrame)
            }
            // Perform the trajectory request in a separate dispatch queue
            trajectoryQueue.async {
                self.setUpDetectTrajectoriesRequest()
                do {
                    if let trajectoryRequest = self.detectTrajectoryRequest {
                        try visionHandler.perform([trajectoryRequest])
                    }
                } catch {
                    AppError.display(error, inViewController: self)
                }
            }
        }
        // Run bodypose request for additional GameConstants.maxPostReleasePoseObservations frames after the first trajectory observation is detected
        if !(self.trajectoryView.inFlight && self.trajectoryInFlightPoseObservations >= GameConstants.maxTrajectoryInFlightPoseObservations) {
            do {
                try visionHandler.perform([detectPlayerRequest])
                if let result = detectPlayerRequest.results?.first as? VNRecognizedPointsObservation {
                    let box = humanBoundingBox(for: result)
                    let boxView = playerBoundingBox
                    DispatchQueue.main.async {
                        let horizontalInset = CGFloat(-20.0)
                        let verticalInset = CGFloat(-20.0)
                        let viewRect = controller.viewRectForVisionRect(box).insetBy(dx: horizontalInset, dy: verticalInset)
                        self.updateBoundingBox(boxView, withRect: viewRect)
                        if !self.playerDetected && !boxView.isHidden {
                            self.gameStatusLabel.alpha = 0
                            self.resetTrajectoryRegions()
                            self.gameManager.stateMachine.enter(GameManager.DetectedPlayerState.self)
                        }
                    }
                }
            } catch {
                AppError.display(error, inViewController: self)
            }
        } else {
            // Hide player bounding box
            DispatchQueue.main.async {
                if !self.playerBoundingBox.isHidden {
                    self.playerBoundingBox.isHidden = true
                    self.jointSegmentView.resetView()
                }
            }
        }
    }
}

キーポイント:

  • VNImageRequestHandler(cmSampleBuffer:orientation:options:)ビジョン分析用にカメラ バッファを再利用します。
  • trajectoryQueue.asyncUI の更新による混雑を避けるために、軌跡リクエストを別のキューに入れます。
  • detectPlayerRequest体位リクエスト、リターンですVNRecognizedPointsObservation
  • humanBoundingBox(for:)プレイヤーの位置は姿勢キーポイントに基づいて計算され、ビュー座標に変換されます。
  • トラックエントリーinFlight最後に、この例では、限られた数のリリース後のポーズ フレームのみを保存し続け、アクション分類ウィンドウを制御します。

(21:19) 人体フレームは個別に検出されません。観察から得られた例.allキーポイントをグループ化し、信頼性の低いポイントをフィルタリングして、これらのポイントの位置を正規化された境界ボックスに結合します。

func humanBoundingBox(for observation: VNRecognizedPointsObservation) -> CGRect {
        var box = CGRect.zero
        // Process body points only if the confidence is high
        guard observation.confidence > 0.6 else {
            return box
        }
        var normalizedBoundingBox = CGRect.null
        guard let points = try? observation.recognizedPoints(forGroupKey: .all) else {
            return box
        }
        for (_, point) in points {
            // Only use point if human pose joint was detected reliably
            guard point.confidence > 0.1 else { continue }
            normalizedBoundingBox = normalizedBoundingBox.union(CGRect(origin: point.location, size: .zero))
        }
        if !normalizedBoundingBox.isNull {
            box = normalizedBoundingBox
        }
        // Fetch body joints from the observation and overlay them on the player
        DispatchQueue.main.async {
            let joints = getBodyJointsFor(observation: observation)
            self.jointSegmentView.joints = joints
        }
        // Store the body pose observation in playerStats when the game is in TrackThrowsState
        // We will use these observations for action classification once the throw is complete
        if gameManager.stateMachine.currentState is GameManager.TrackThrowsState {
            playerStats.storeObservation(observation)
            if trajectoryView.inFlight {
                trajectoryInFlightPoseObservations += 1
            }
        }
        return box
    }

キーポイント:

  • observation.confidence > 0.6まず人体全体の観察をフィルタリングします。
  • recognizedPoints(forGroupKey: .all)体の詳細をすべて削除します。
  • 単一点の信頼度が 0.1 以下の場合はスキップします。
  • normalizedBoundingBox.union(...)残りのキーポイントを使用して人体のフレームを構築します。
  • ゲームエントリーTrackThrowsStateその後、この例では観察結果を次のように保存します。playerStats、これらのジェスチャは後続のアクションの分類に使用されます。

アクション分類: 姿勢ウィンドウを Core ML 入力に変換する

(18:23) 体の姿勢をCreate MLのアクション分類と照合できます。セッションは、いくつかのトレーニングと推論に関する提案を行いました。トレーニング ビデオには対象者を 1 人だけにするのが最善です。ビデオを使用せずに Vision を直接使用することもできます。keypointsMultiArrayML MultiArray バッファを取得します。トレーニングにはビジョン ポーズを使用し、推論にはビジョン ポーズを使用します。 ARKit ポーズを Vision ポーズでトレーニングされたモデルにフィードすることはできません。

(21:58) この例では、リング バッファーを使用して、最新の身体姿勢の観察を保存します。いっぱいになったら、最も古いフレームを削除し、新しい観測を追加します。

mutating func storeObservation(_ observation: VNRecognizedPointsObservation) {
        if poseObservations.count >= GameConstants.maxPoseObservations {
            poseObservations.removeFirst()
        }
        poseObservations.append(observation)
    }

キーポイント:

  • poseObservations継続的な体のポーズを維持します。
  • GameConstants.maxPoseObservationsウィンドウのサイズを制限します。
  • removeFirst()最も古い観察結果を破棄します。
  • 新しい観測値は常にウィンドウの最後に追加されます。

(22:42) 分類する前に、コードは最大 60 フレームの観察を次の形式に変換する必要があります。MLMultiArray。フレーム数が 60 未満の場合は、0 が追加されます。

func prepareInputWithObservations(_ observations: [VNRecognizedPointsObservation]) -> MLMultiArray? {
    let numAvailableFrames = observations.count
    let observationsNeeded = 60
    var multiArrayBuffer = [MLMultiArray]()

    // swiftlint:disable identifier_name
    for f in 0 ..< min(numAvailableFrames, observationsNeeded) {
        let pose = observations[f]
        do {
            let oneFrameMultiArray = try pose.keypointsMultiArray()
            multiArrayBuffer.append(oneFrameMultiArray)
        } catch {
            continue
        }
    }

    // If poseWindow does not have enough frames (60) yet, we need to pad 0s
    if numAvailableFrames < observationsNeeded {
        for _ in 0 ..< (observationsNeeded - numAvailableFrames) {
            do {
                let oneFrameMultiArray = try MLMultiArray(shape: [1, 3, 18], dataType: .double)
                try resetMultiArray(oneFrameMultiArray)
                multiArrayBuffer.append(oneFrameMultiArray)
            } catch {
                continue
            }
        }
    }
    return MLMultiArray(concatenating: [MLMultiArray](multiArrayBuffer), axis: 0, dataType: MLMultiArrayDataType.double)
}

キーポイント:

  • observationsNeededが 60 の場合、モデルは固定長のポーズ ウィンドウを想定します。
  • pose.keypointsMultiArray()ボディポーズの​​単一フレームを、Core ML が受信できる配列に変換します。
  • フレーム数が足りない場合、サンプル作成形状は[1, 3, 18]MLMultiArray完了。
  • 最後の行は、軸 0 に沿ったすべてのフレームを 1 つの入力に連結します。

(22:21) ウィンドウの準備ができたら、サンプルが作成されますPlayerActionClassifierInput、Core ML モデルを呼び出して予測し、確率配列から最も高い信頼レベルに対応するスロー タイプを取得します。

mutating func getLastThrowType() -> ThrowType {
        let actionClassifier = PlayerActionClassifier().model
        guard let poseMultiArray = prepareInputWithObservations(poseObservations) else {
            return ThrowType.none
        }
        let input = PlayerActionClassifierInput(input: poseMultiArray)
        guard let predictions = try? actionClassifier.prediction(from: input),
            let output = predictions.featureValue(for: "output")?.multiArrayValue,
                let outputBuffer = try? UnsafeBufferPointer<Float32>(output) else {
            return ThrowType.none
        }
        let probabilities = Array(outputBuffer)
        guard let maxConfidence = probabilities.prefix(3).max(), let maxIndex = probabilities.firstIndex(of: maxConfidence) else {
            return ThrowType.none
        }
        let throwTypes = ThrowType.allCases
        return throwTypes[maxIndex]
    }

キーポイント:

  • PlayerActionClassifier().modelこれは、Create ML でトレーニングした後にアプリに入力される分類モデルです。
  • prepareInputWithObservations(poseObservations)ポーズ ウィンドウをモデル入力に変えます。
  • prediction(from:)分類を実行します。
  • 出力経由"output"取得するMLMultiArray、そして次に変換されますFloat32確率の配列。
  • probabilities.prefix(3).max()最初の 3 つの投球カテゴリーで最も高い信頼性のみを探してください。

重要ポイント

  • やるべきこと: 空中描画またはホワイトボード注釈アプリを作成します。やる価値がある理由:安定した手の姿勢で親指と人差し指の先端を出し、3フレームつまんでから線を描き始め、放したら止まります。開始方法: 使用するVNDetectHumanHandPoseRequest親指の先端と人差し指の先端を取り出し、例の距離しきい値と証拠カウンターを再利用します。

  • やるべきこと: ボタンのないカメラのシャッターを作成します。実行する価値がある理由: セッションの冒頭で、写真撮影をトリガーするために特定のジェスチャーを使用することが言及されました。ハンドジェスチャーにより、ユーザーは画面がなくても操作できます。開始方法: 最初に低めに設定しますmaximumHandCount遅延を制御し、ターゲット ジェスチャ分類ロジックを写真アクションにバインドします。

  • やるべきこと: モーション アクション分類子を作成します。行う価値がある理由: 身体姿勢の観察は次のように行うことができます。keypointsMultiArray()に変換するMLMultiArrayを入力し、Create ML で学習させたアクション分類モデルを入力します。開始方法: まず、10043 Training Action Classifier を参照し、アプリに 60 フレームのポーズ ウィンドウを保存し、この例に従ってゼロを埋めます。

  • やるべきこと: 写真またはビデオに最適なアクション フレーム ピッカーを作成します。実行する価値がある理由: 身体姿勢により、静止画像、カメラ フィード、オフライン ギャラリーを分析できます。セッションでは、ジャンプの最高点、ストロモーションの合成、スポーツ姿勢の選択の例を示します。開始方法: ビデオ フレームをオフラインで走査して実行するVNDetectHumanBodyPoseRequest、関節の位置と信頼度によって候補フレームを選択します。

  • やるべきこと: 単純な人間のポーズ品質のヒント レイヤーを作成します。実行する価値がある理由: セッションでは、エッジ オクルージョン、複数の人が互いにオクルージョンすること、逆立ち、前かがみになること、緩んだ衣服が効果を軽減するものとして明確にリストされています。開始方法: 観測の信頼度と単一点の信頼度を確認します。値がしきい値より低い場合は、ビューファインダー内のユーザーに、後ずさりするか、位置を調整するか、全身を露出するように促します。

関連セッション

コメント

GitHub Issues · utterances