WWDC Quick Look 💓 By SwiftGGTeam
Explore 3D body pose and person segmentation in Vision

Explore 3D body pose and person segmentation in Vision

元の動画を見る

ハイライト

Vision フレームワークには 3D 人体姿勢検出(VNDetectHumanBodyPose3DRequest)が追加され、ARKit なしで通常の写真から 17 個の関節の 3D 位置(メートル単位)を抽出できます。深度データ入力と最大 4 人までのインスタンス分割にも対応し、人体理解を 2D 平面から実空間へ広げます。

主な内容

2D から 3D へ: 人体姿勢検出の進化

Vision フレームワークがこれまで提供していたのは 2D 人体姿勢検出で、返されるのは正規化されたピクセル座標でした。WWDC23 では VNDetectHumanBodyPose3DRequest が導入され、画像から 3D 関節位置を直接抽出できます。

3D 姿勢は 17 個の関節を返し、グループごとに整理されています。

  • 頭部: 頭頂、頭部中心
  • 胴体: 左肩、右肩、脊柱、腰の中心(root joint)、左股関節、右股関節
  • 左腕: 左手首、左肩、左肘
  • 右腕: 右手首、右肩、右肘
  • 左脚: 左股関節、左膝、左足首
  • 右脚: 右股関節、右膝、右足首

左右は検出対象者から見た左右であり、画像上の左右ではありません。

00:51

2D 検出と異なり、3D 関節位置はメートル単位で返され、原点は腰の中心(root joint)にあります。これにより、人の手首が地面からどれくらい高いかなど、実世界の距離を直接測定できます。

深度データ: 3D をより正確にする

Vision は深度データを入力として受け取れるようになりました。VNImageRequestHandler には AVDepthData をサポートする初期化メソッドが追加され、CVPixelBufferCMSampleBuffer に対応します。

画像ファイル自体に深度データ(ポートレートモード写真など)が含まれている場合、Vision は既存 API を変更しなくても自動で抽出します。

深度データには次が含まれます。

  • 深度マップ: 視差(Disparity)または深度(Depth)形式で保存されます
  • カメラキャリブレーションデータ: 内部パラメータ、外部パラメータ、レンズ歪みパラメータ

深度データがある場合、bodyHeight は実測の身長を返します。ない場合は参照値の 1.8 メートルを返します。どちらの方式が使われたかは heightEstimation プロパティで判定できます。

10:41

複数人のインスタンス分割: 「全員」から「一人ひとり」へ

従来の GeneratePersonSegmentation リクエストは、すべての人物を含む単一の mask を返していました。新しく導入された person instance mask リクエストは、最大 4 人までの独立した人物 mask を出力でき、それぞれに信頼度スコアが付きます。

画像内の特定の人物だけを選択し、抽出できます。背景だけが必要な場合は instance 0 を選びます。

4 人を超えるシーンでは、まず Vision の顔検出 API で人数を数え、4 人を超える場合は従来の単一人物分割リクエストへフォールバックすることが推奨されます。

12:20

詳細

3D 人体姿勢検出のワークフロー

VNDetectHumanBodyPose3DRequest を使う流れは 2D リクエストと同じです。

import Vision
import UIKit

func detect3DBodyPose(in image: UIImage) {
    // 1. 3D 人体姿勢検出リクエストを作成
    let request = VNDetectHumanBodyPose3DRequest()
    
    // 2. 画像リクエストハンドラを初期化
    guard let cgImage = image.cgImage else { return }
    let handler = VNImageRequestHandler(cgImage: cgImage)
    
    // 3. リクエストを実行
    do {
        try handler.perform([request])
        
        // 4. 結果を取得
        if let observation = request.results?.first as? VNHumanBodyPose3DObservation {
            process3DPose(observation)
        }
    } catch {
        print("3D pose detection failed: \(error)")
    }
}

キーポイント:

  • VNDetectHumanBodyPose3DRequest は新しい 3D 姿勢検出リクエストクラスです
  • VNImageRequestHandler の使い方は 2D リクエストと完全に同じです
  • resultsVNHumanBodyPose3DObservation の配列を返し、現在のバージョンでは最も目立つ人物の結果を 1 つだけ返します
  • エラー処理には標準の do-catch パターンを使います

関節位置データを取得する

import Vision
import simd

func process3DPose(_ observation: VNHumanBodyPose3DObservation) {
    // 特定の関節の 3D 位置を取得
    do {
        let leftWrist = try observation.recognizedPoint(.leftWrist)
        print("Left wrist position: \(leftWrist.position)")
        
        // position は simd_float4x4 行列で、4 列目に平行移動値が含まれる
        let translation = leftWrist.position.columns.3
        print("Translation - x: \(translation.x), y: \(translation.y), z: \(translation.z)")
    } catch {
        print("Failed to get left wrist point: \(error)")
    }
    
    // 関節グループを取得
    do {
        let torsoPoints = try observation.recognizedPoints(.torso)
        for (jointName, point) in torsoPoints {
            print("\(jointName): \(point.position)")
        }
    } catch {
        print("Failed to get torso points: \(error)")
    }
    
    // 推定身長を取得
    print("Estimated body height: \(observation.bodyHeight) meters")
    print("Height estimation method: \(observation.heightEstimation)")
}

キーポイント:

  • recognizedPoint(_:) は関節名から単一の関節を取得し、VNHumanBodyRecognizedPoint3D を返します
  • recognizedPoints(_:) はグループ名から関節グループを取得し、辞書を返します
  • positionsimd_float4x4 行列で、ARKit と互換性があり、4 列目(columns.3)に x/y/z の平行移動値が含まれます
  • bodyHeight は推定身長(メートル)を返します
  • heightEstimation は身長が実測値か参照値かを示します

3D ジオメトリクラス階層を理解する

Vision には新しい 3D ジオメトリ基底クラスが導入されています。

  • VNPoint3D: 基底クラスで、3D 位置を保存する simd_float4x4 行列を定義します
  • VNRecognizedPoint3D: 位置情報を継承し、関節名などの識別子を追加します
  • VNHumanBodyRecognizedPoint3D: 局所位置(localPosition)と親関節への参照を追加します
func analyzeJointHierarchy(_ observation: VNHumanBodyPose3DObservation) {
    do {
        let leftWrist = try observation.recognizedPoint(.leftWrist)
        
        // model position: root joint(腰の中心)に対するグローバル位置
        let globalPosition = leftWrist.position
        print("Global position relative to root: \(globalPosition)")
        
        // local position: 親関節(ここでは左肘)に対する局所位置
        let localPosition = leftWrist.localPosition
        print("Local position relative to parent: \(localPosition)")
        
        // local position から関節角度を計算
        let angle = calculateLocalAngleToParent(localPosition)
        print("Joint angle: \(angle)")
        
    } catch {
        print("Error: \(error)")
    }
}

func calculateLocalAngleToParent(_ localPosition: simd_float4x4) -> (pitch: Float, yaw: Float, roll: Float) {
    let pos = localPosition.columns.3
    let vectorLength = sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z)
    
    // pitch: 90 度回転して、ジオメトリを標準の下向き方向から骨格方向へ合わせる
    let pitch = Float.pi / 2
    
    // yaw: 逆余弦で計算
    let yaw = acos(pos.z / vectorLength)
    
    // roll: 逆正接で計算
    let roll = atan2(pos.y, pos.x)
    
    return (pitch, yaw, roll)
}

キーポイント:

  • position は常に skeleton の root joint(腰の中心)を基準にします
  • localPosition は親関節を基準にするため、身体の局所的な動きの分析に適しています
  • simd_float4x4 は ARKit と SceneKit の座標系と一致します
  • localPosition から関節間の角度(pitch/yaw/roll)を計算できます

3D 関節を 2D 画像へ投影し直す

func projectJointsToImage(_ observation: VNHumanBodyPose3DObservation, imageSize: CGSize) {
    do {
        // 2D 画像内での root joint の位置を取得
        let rootJointInImage = try observation.pointInImage(.root)
        print("Root joint in image: \(rootJointInImage)")
        
        // 2D 画像内での左肩の位置を取得
        let leftShoulderInImage = try observation.pointInImage(.leftShoulder)
        print("Left shoulder in image: \(leftShoulderInImage)")
        
        // 3D 骨格を元画像に合わせるには次が必要:
        // 1. スケール: 既知の関節の 3D と 2D の距離比に基づいて画像平面を拡大縮小
        // 2. 平行移動: root joint の 2D 位置を使ってオフセットを決定
        
    } catch {
        print("Projection failed: \(error)")
    }
}

キーポイント:

  • pointInImage(_:) は 3D 関節座標を 2D 画像座標へ投影し直します
  • 返り値は左下原点の VNPoint 座標系を使います
  • レンダリング環境では、画像中心を原点とする座標系への変換が必要です
  • cameraOriginMatrix と組み合わせると、カメラ視点からシーンをレンダリングできます

カメラ原点行列を使う

func setupCameraPerspective(_ observation: VNHumanBodyPose3DObservation) {
    // cameraOriginMatrix には、検出対象者に対するカメラの位置と回転が含まれる
    let cameraMatrix = observation.cameraOriginMatrix
    print("Camera origin matrix: \(cameraMatrix)")
    
    // 回転情報を使って画像平面をカメラへ向ける
    // 回転部分(3x3 の部分行列)だけを使い、平行移動(最後の列)は無視する
    let rotationMatrix = simd_float3x3(
        columns: (
            simd_float3(cameraMatrix.columns.0.x, cameraMatrix.columns.0.y, cameraMatrix.columns.0.z),
            simd_float3(cameraMatrix.columns.1.x, cameraMatrix.columns.1.y, cameraMatrix.columns.1.z),
            simd_float3(cameraMatrix.columns.2.x, cameraMatrix.columns.2.y, cameraMatrix.columns.2.z)
        )
    )
    
    // 逆回転を計算し、画像平面をカメラに正対させる
    let inverseRotation = rotationMatrix.inverse
    print("Inverse rotation: \(inverseRotation)")
}

キーポイント:

  • cameraOriginMatrixsimd_float4x4 行列を返し、3D 空間内のカメラ位置と向きを表します
  • カメラは検出対象者に正対していない場合があり、この行列は相対位置の理解に役立ちます
  • カメラ視点からレンダリングするときは、行列の回転部分を使い、平行移動は無視します
  • 逆変換によって画像平面を正しくカメラへ向けます

深度データ付きリクエストの処理

import Vision
import AVFoundation

func detectPoseWithDepth(image: CGImage, depthData: AVDepthData) {
    // 深度データ付きの初期化メソッドを使う
    let handler = VNImageRequestHandler(
        cgImage: image,
        depthData: depthData,
        options: [:]
    )
    
    let request = VNDetectHumanBodyPose3DRequest()
    
    do {
        try handler.perform([request])
        if let observation = request.results?.first as? VNHumanBodyPose3DObservation {
            // 深度データがある場合、bodyHeight は実測値を返す
            print("Measured height: \(observation.bodyHeight)")
            print("Estimation: \(observation.heightEstimation)") // .measured
        }
    } catch {
        print("Detection failed: \(error)")
    }
}

// ポートレート写真から深度を自動抽出
func detectPoseFromPortraitPhoto(imageURL: URL) {
    // ファイルに深度データが含まれる場合、Vision が自動で抽出する
    let handler = VNImageRequestHandler(url: imageURL)
    let request = VNDetectHumanBodyPose3DRequest()
    
    do {
        try handler.perform([request])
        // 結果を処理...
    } catch {
        print("Detection failed: \(error)")
    }
}

キーポイント:

  • VNImageRequestHandler には AVDepthData パラメータを受け取るオーバーロードが追加されました
  • AVDepthData は Apple SDK における深度メタデータの統一コンテナです
  • ポートレートモード写真には深度データが自動的に含まれます(視差マップ形式で保存)
  • 深度データがある場合、heightEstimation.measured を返し、そうでない場合は .reference を返します
  • LiDAR デバイスはリアルタイムキャプチャ時に、より正確なシーン計測を提供できます

複数人のインスタンス分割

import Vision

func segmentMultiplePeople(in image: UIImage) {
    guard let cgImage = image.cgImage else { return }
    
    // 複数人のインスタンス分割リクエストを作成
    let request = VNGeneratePersonInstanceMaskRequest()
    
    // 任意: 特定のインスタンスだけを返すよう指定
    // request.instanceNumber = 1  // 1 人目だけを返す
    
    let handler = VNImageRequestHandler(cgImage: cgImage)
    
    do {
        try handler.perform([request])
        
        if let observation = request.results?.first as? VNGeneratePersonInstanceMaskObservation {
            // 利用可能なすべてのインスタンスを取得
            let instanceCount = observation.allInstances.count
            print("Detected \(instanceCount) people")
            
            // 各人物の mask を抽出
            for instanceIndex in observation.allInstances {
                let mask = try observation.generateMaskedImage(
                    ofInstances: [instanceIndex],
                    from: handler,
                    croppedToInstancesExtent: false
                )
                print("Instance \(instanceIndex) mask generated")
            }
            
            // 背景を抽出(instance 0)
            let backgroundMask = try observation.generateMaskedImage(
                ofInstances: [0],
                from: handler,
                croppedToInstancesExtent: false
            )
        }
    } catch {
        print("Instance segmentation failed: \(error)")
    }
}

// 4 人を超えるシーンを処理
func handleCrowdedScene(image: UIImage) {
    // まず顔検出で人数を数える
    let faceRequest = VNDetectFaceRectanglesRequest()
    let handler = VNImageRequestHandler(cgImage: image.cgImage!)
    
    do {
        try handler.perform([faceRequest])
        let faceCount = faceRequest.results?.count ?? 0
        
        if faceCount > 4 {
            // 従来の単一人物分割リクエストへフォールバック
            let segmentationRequest = VNGeneratePersonSegmentationRequest()
            try handler.perform([segmentationRequest])
            // 単一 mask を処理...
        } else {
            // インスタンス分割を使用
            let instanceRequest = VNGeneratePersonInstanceMaskRequest()
            try handler.perform([instanceRequest])
            // 独立した mask を処理...
        }
    } catch {
        print("Error: \(error)")
    }
}

キーポイント:

  • VNGeneratePersonInstanceMaskRequest は新しい複数人インスタンス分割リクエストです
  • 最大 4 人まで対応し、各人物に独立した mask があります
  • instanceNumber プロパティで特定インスタンスだけを返すよう指定できます(0 は背景)
  • allInstances は検出されたすべてのインスタンスインデックスを返します
  • generateMaskedImage(ofInstances:from:croppedToInstancesExtent:) は指定インスタンスの mask 画像を生成します
  • 4 人を超えるシーンでは、まず顔検出で人数を数え、どの分割戦略を使うか決めることが推奨されます

重要ポイント

  • 「姿勢補正」フィットネス App を作る

    • 何をするか: ユーザーがヨガやフィットネス動作を行うとき、3D 姿勢をリアルタイムで検出し、標準姿勢と比較して補正提案を出します
    • なぜ取り組む価値があるか: 3D 関節位置はメートル単位で返るため、関節角度を正確に測定し、動作が標準に合っているか判断できます
    • 始め方: VNDetectHumanBodyPose3DRequest で関節位置を取得し、localPosition から関節角度(pitch/yaw/roll)を計算して、事前設定した標準角度範囲と比較します
  • 「身長測定」ツールを作る

    • 何をするか: ポートレート写真から人物の身長を自動測定します
    • なぜ取り組む価値があるか: 深度データがある場合、Vision は実測身長を返すため、ARKit や専用ハードウェアは不要です
    • 始め方: ポートレートモード写真(深度データを自動で含む)を読み込み、VNDetectHumanBodyPose3DRequest を実行し、bodyHeight プロパティを読み取り、heightEstimation で実測値であることを確認します
  • 「集合写真の背景置き換え」App を作る

    • 何をするか: 集合写真から特定の人物だけを抽出し、背景を置き換えたり新しいシーンへ合成したりします
    • なぜ取り組む価値があるか: インスタンス分割 API は最大 4 人までを個別に抽出でき、各人物に独立した mask があります
    • 始め方: VNGeneratePersonInstanceMaskRequest で各インスタンスの mask を取得し、対象人物の instance index を選び、generateMaskedImage で抽出してからカスタム背景と合成します
  • 「3D 人体モデル」プレビューツールを作る

    • 何をするか: 通常の写真から 3D 骨格の可視化を生成し、回転して確認できるようにします
    • なぜ取り組む価値があるか: ARKit や深度カメラがなくても、通常の写真から 3D 関節位置を取得できます
    • 始め方: VNDetectHumanBodyPose3DRequest で 3D 関節を取得し、cameraOriginMatrix でカメラ位置を取得して、SceneKit または RealityKit で骨格をレンダリングし、視点切り替えをサポートします

関連セッション

コメント

GitHub Issues · utterances