ARKitで姿勢検出
モーションキャプチャ
ARKit 3(iOS 13)から、モーションキャプチャ機能が追加され、現実の人物の全身の動きを取得できるようになりました。これにより、人体の特定の部位に仮想オブジェクトやエフェクトを表示したり、現実の人物の動きと同じように3Dモデルをアニメーションさせたりといったことが可能になります。

他にもモーション認識に使用したり、スポーツにおける動きの分析、バーチャルオブジェクトとのインタラクションといった多様なユースケースが考えられます。
本機能はA12以降(iPhone XS・XR移行)のデバイスで利用可能です。
2D Body Detection
ARKitにおけるモーションキャプチャの実現方法のひとつが、本項で解説する「2D Body Detection」です。人体を2次元的に検出します。

2D Body Detectionの実装方法
2D Body Detectionの実装は非常に簡単です。ARConfigurationクラスにframeSemanticsというARConfiguration.FrameSemantics型のプロパティがiOS 13で追加されたので、
var frameSemantics: ARConfiguration.FrameSemantics
ここに、bodyDetectionを指定します。
configuration.frameSemantics = .bodyDetection
これだけの追加実装で、人体を検出できるようになります。
detectedBody, ARBody2D
検出した人体の情報は、ARFrameのdetectedBodyプロパティよりARBody2D型で取得できます。
var detectedBody: ARBody2D? { get }
ARBody2Dは、2Dで人体を表すためのクラスです。ARSkeleton2D型のskeletonプロパティを持ちます。
var skeleton: ARSkeleton2D { get }
ARSkeleton2D
ARSkeleton2Dは、2Dで人体の骨格(スケルトン)を表すクラスです。ARSkeletonクラス[1]を継承しています。
人体の骨格を形成する17個のジョイントのスクリーン空間における位置を表す2次元座標を、jointLandmarksプロパティにsimd_float2型の配列で保持しています。座標は原点を左上として、(0.0, 0.0)〜(1.0, 1.0)に正規化されています。
@nonobjc var jointLandmarks: [simd_float2] { get }

ARSkeleton2Dのジョイントランドマーク
また、landmark(for:)メソッドにARSkeleton.JointName型(次項で解説します)でジョイント名を指定することで、そのジョイントの2次元座標を取得することもできます。
@nonobjc func landmark(for jointName: ARSkeleton.JointName) -> simd_float2?
ARSkeleton.JointName
ARSkeleton.JointNameは骨格のジョイント名を表す構造体です。本構造体はARSkeleton2Dだけでなく、後述するARSkeleton3Dでも共通で使用します。
以下の型プロパティが定義されています。
static let head: ARSkeleton.JointName
static let leftFoot: ARSkeleton.JointName
static let leftHand: ARSkeleton.JointName
static let leftShoulder: ARSkeleton.JointName
static let rightFoot: ARSkeleton.JointName
static let rightHand: ARSkeleton.JointName
static let rightShoulder: ARSkeleton.JointName
static let root: ARSkeleton.JointName
2Dの場合は合計17個のジョイントが検出されますが、全てのジョイント名がARSkeleton.JointNameで定義されているわけではないことがわかります。

ARSkeleton2Dにおいて各JointNameが示すジョイントの位置
ARSkeletonDefinition
人体の骨格を可視化する際に、ジョイントの接続関係を知りたい場合があります。ジョイントの接続関係を調べるには、ARSkeletonクラス(ARSkeleton2D,ARSkeleton3Dの親クラス)に定義されているdefinitionプロパティから得られるARSkeletonDefinitionオブジェクトを使用します。
var definition: ARSkeletonDefinition { get }
ARSkeletonDefinitionクラスは次のように各ジョイントの「親」となるジョイントのインデックスを格納したparentIndicesプロパティを持っており、
@nonobjc public var parentIndices: [Int] { get }
次のように、ジョイントの接続関係をたどることができます。
let definition = skeleton.definition
let joints = skeleton.jointLandmarks
joints.enumerated().forEach { jointIndex, jointPosition in
// 親ジョイントのインデックスを取得
let parentIndex = definition.parentIndices[jointIndex]
// Rootの親ジョイントは存在しないため、インデックスとして-1が返ってくる
guard parentIndex != -1 else { return }
// 親ジョイントの位置を取得
let parentJoint = joints[parentIndex]
}

本クラスも、2Dだけではなく、後述する3Dボディトラッキングでも使用します。
利用可能なコンフィギュレーション
前述の通りbodyDetectionはARConfigurationのframeSemanticsプロパティにセットするわけですが、どのコンフィギュレーションクラスに対しても適用できるわけではありません。サポートしていないARConfiguration.FrameSemanticsの型プロパティをセットすると、実行時エラーとなりアプリが停止します。
2D Body Detectionを利用可能なコンフィギュレーションは、以下の4つです[2]。
ARWorldTrackingConfigurationAROrientationTrackingConfigurationARImageTrackingConfigurationARBodyTrackingConfiguration
bodyDetection と personSegmentation は併用可能か?
personSegmentationあるいは〜WithDepthを使用するとカメラフレーム画像の各々のピクセルが人物を表す一部であるかそうでないか、という情報は得ることができますが、画像の中で頭はどこにあるか、手はどこにあるか、といった情報は得られません。そこでbodyDetectionを併用したい、というケースがあるかもしれません。幸いARConfiguration.FrameSemanticsはOptionSetに準拠しているので、次のように両方を指定してもビルドは通ります。
configuration.frameSemantics = [.personSegmentation, .bodyDetection]
しかし、残念ながら実行時に次のようなエラーで停止してしまいます。結論として、bodyDetectionとpersonSegmentationの併用はできません。
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'This set of frame semantics is not supported on this configuration'
3D Body Tracking
ARKitにおいてモーションキャプチャの実現するもうひとつの方法が、本項で解説する「3D Body Tracking」です。3次元的に人体を検出し、トラッキングします。

2D Body Detectionではワールドトラッキング等のコンフィギュレーションにframe semanticとして機能を付加する形で利用しましたが、こちらはARBodyTrackingConfigurationという専用のコンフィギュレーションを使用します。
ARBodyTrackingConfiguration
ARBodyTrackingConfigurationは、3D空間で人体の動きをトラッキングするコンフィギュレーションです。
初期化し、ARセッションに渡してrunすればボディトラッキングが開始します。
let configuration = ARBodyTrackingConfiguration()
sceneView.session.run(configuration)
なお、frameSemanticsプロパティにはデフォルトでbodyDetectionがセットされています。
平面検出と画像検出の利用
ARBodyTrackingConfigurationでは平面検出と画像検出も利用可能です。トラッキング中の人体の動きに合わせて仮想キャラクターを動かす場合に、検出した平面や画像の上にキャラクターを設置するといった使い方が可能です。
平面検出にはplaneDetectionプロパティを利用します。使用方法はARWorldTrackingConfigurationの同名プロパティと同様[3]です。
var planeDetection: ARWorldTrackingConfiguration.PlaneDetection
また画像検出/トラッキングには以下のプロパティが用意されています。こちらも使い方はARWorldTrackingConfigurationの同名プロパティと同様[4]です。
var detectionImages: Set<ARReferenceImage>
var maximumNumberOfTrackedImages: Int
ボディトラッキング時にでPeople Occlusionは利用可能か?
人体の3D骨格情報を利用して何か仮想オブジェクトを表示する際に、人体の輪郭に合わせてオクルージョンしたいケースがあるかもしれません。しかしこちらで示した通り、personSegmentation及びpersonSegmentationWithDepthはARBodyTrackingConfigurationでは使用できません。
ARBodyAnchor
ボディトラッキングを開始し、人体が検出されると、ARBodyAnchorオブジェクトがアンカーのリストに追加されます。
このARBodyAnchorは、他のアンカークラスと同様にARAnchorのサブクラスですので、検出されたアンカーオブジェクトにアクセスする方法も同様です。ボディトラッキングが動作することをシンプルに確認するために、検出したボディアンカーの位置に球体ノードを設置してみましょう。
func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode, for anchor: ARAnchor) {
if let bodyAnchor = anchor as? ARBodyAnchor {
let sphereNode = SCNNode.sphereNode(color: UIColor.blue)
node.addChildNode(sphereNode)
}
}

ボディアンカーの位置にブルーの球体オブジェクトを設置
ARSkeleton3D
ARBodyAnchorはARSkeleton3D型のskeletonプロパティを持ち、ここから、トラッキング中の人体の3次元骨格情報が取得できます。
var skeleton: ARSkeleton3D { get }
ARSkeleton3Dは、3Dで人体の骨格(スケルトン)を表すクラスです。先に解説したARSkeleton2Dと同様にARSkeletonクラスを継承しています。
ARSkeleton2Dのジョイント数は17個でしたが、ARSkeleton3Dはなんと91個ものジョイントから構成されます。

ARSkeleton3Dの91個のジョイント
ARSkeleton3DはjointModelTransformsプロパティを持ち、各ジョイントのモデル空間[5]におけるTransform[6]をsimd_float4x4型の配列で保持しています。
@nonobjc public var jointModelTransforms: [simd_float4x4] { get }
またjointLocalTransformsプロパティには、各ジョイントのローカル空間[7]のTransformをsimd_float4x4型の配列で保持しています。
@nonobjc public var jointLocalTransforms: [simd_float4x4] { get }
modelTransform(for:)メソッド、あるいはlocalTransform(for:)メソッドにARSkeleton.JointName型(2Dの項で解説済み)でジョイント名を指定することで、そのジョイントのTransformを表す4x4行列を取得することもできます。
@nonobjc public func modelTransform(for jointName: ARSkeleton.JointName)
-> simd_float4x4?
@nonobjc public func localTransform(for jointName: ARSkeleton.JointName)
-> simd_float4x4?

ARSkeleton3Dにおいて各JointNameが示すジョイントの位置
ジョイントの接続関係も、2Dの場合と同様にskeletonプロパティから取得できるARSkeletonDefinitionオブジェクト(2Dの項で解説済み)を使用し、親子関係を利用してたどることができます。

Root以外のジョイントは親ジョイントを持つ
let definition = skeleton.definition
let joints = skeleton.jointModelTransforms
joints.enumerated().forEach { jointIndex, jointTransform in
// 親ジョイントのインデックスを取得
let parentIndex = definition.parentIndices[jointIndex]
// Rootの親ジョイントは存在しないため、インデックスとして-1が返ってくる
guard parentIndex != -1 else { return }
// 親ジョイントの位置を取得
let parentJoint = joints[parentIndex]
}

ARSkeleton3Dを可視化
「つくりながら学ぶ」ARKitの入門書
ARKitの本を書いて個人で出版しました。
はじめの一歩として3行で書ける最小実装のARから始めて、平面を検出する方法、その平面に仮想オブジェクトを設置する方法、そしてその仮想オブジェクトとインタラクションできるようにする方法・・・と、読み進めるにつれて「作りながら」引き出しが増えていき、最終的にはARKitを用いたメジャーや、空間に絵や文字を描くといった、ARKitならではのアプリケーションの実装ができるよう構成しています。
全146ページ。サンプルコードはGitHubよりダウンロード可能です。BOOTHにて販売中。
-
ARSkeletonクラスは、後述するARSkeleton3Dの親クラスでもあります。 ↩︎ -
どのコンフィギュレーションでどのframe semanticをサポートしているかを調べるには、
ARConfigurationのsupportsFrameSemantics(_:)メソッドを利用できます。本メソッドについては「ARKitのPeople Occlusion」で解説しています。 ↩︎ -
「実践ARKit」参照。 ↩︎
-
「実践ARKit」参照。 ↩︎
-
このモデル空間はRootジョイント(お尻)を原点とします。 ↩︎
-
位置・回転・スケール情報を保持する4x4行列。 ↩︎
-
このローカル空間は親ジョイントを原点とします。 ↩︎
Discussion