ハイライト
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 をサポートする初期化メソッドが追加され、CVPixelBuffer と CMSampleBuffer に対応します。
画像ファイル自体に深度データ(ポートレートモード写真など)が含まれている場合、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 リクエストと完全に同じですresultsはVNHumanBodyPose3DObservationの配列を返し、現在のバージョンでは最も目立つ人物の結果を 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(_:)はグループ名から関節グループを取得し、辞書を返しますpositionはsimd_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)")
}
キーポイント:
cameraOriginMatrixはsimd_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 で骨格をレンダリングし、視点切り替えをサポートします
関連セッション
- Detect animal poses in Vision — 人体姿勢検出を補完する Vision の動物姿勢検出
- Lift subjects from images in your app — 画像から被写体を切り抜く、person segmentation 関連機能
- Discover advancements in iOS camera capture — 深度データ取得を含む iOS カメラキャプチャの高度な機能
- Build spatial experiences with RealityKit — 3D 人体姿勢データと RealityKit を組み合わせて空間体験を作る
コメント
GitHub Issues · utterances