ハンドトラッキングから指の関節を取得する
はじめに
visionOSのハンドトラッキングでは手首の部分に原点があることを説明しました。
今回は、指の関節の位置を取得する方法について説明します。visionOSでは、片手だけで27箇所のポイントを取得することができます。本記事では話をシンプルに保つため、両手の人差し指の指先のみをトラッキングする方法に焦点を当てます。
実装
具体的なコードは下記のサンプルコードを参照してください。
https://github.com/tochi/HandGestureExample/tree/fingertracking_origin
指の関節の位置を取得する場合、まずはハンドトラッキングで手の原点を取得する必要があります。詳しくは「ハンドトラッキングを読み解く」を参考にして実装を進めてください。ここでは、ハンドトラッキングが行えている前提で話を進めます。
# HandGestureModel.swift
@Published var latestHandTracking: HandsUpdates = .init(left: nil, right: nil)
var rightHandIndexFingerTipAnchorFromJointTransform: simd_float4x4? {
guard let rightHandIndexFingerTip = latestHandTracking.right?.handSkeleton?.joint(.indexFingerTip),
rightHandIndexFingerTip.isTracked else { return nil }
return rightHandIndexFingerTip.anchorFromJointTransform
}
var leftHandIndexFingerTipAnchorFromJointTransform: simd_float4x4? {
guard let leftHandIndexFingerTip = latestHandTracking.left?.handSkeleton?.joint(.indexFingerTip),
leftHandIndexFingerTip.isTracked else { return nil }
return leftHandIndexFingerTip.anchorFromJointTransform
}
struct HandsUpdates {
var left: HandAnchor?
var right: HandAnchor?
}
指の関節の位置を取得するには、ハンドトラッキングのHandAnchorから.handSkeleton?.jointを使用します。例えば、latestHandTracking.right?.handSkeleton?.joint(.indexFingerTip)のように記述します。引数では、取得したいポイントを指定できます。今回は人差し指の指先を取得するため、.indexFingerTipを指定しています。後は、rightHandIndexFingerTip.anchorFromJointTransformを使用することで、指のポイントの位置をsimd_float4x4型で取得できます。
# ContentView.swift
@ObservedObject var gestureModel: HandGestureModel
var body: some View {
VStack() {
...
HStack() {
fingerAnchorLogView(title: "Left Index", fingerAnchorFromJointTransform: gestureModel.leftHandIndexFingerTipAnchorFromJointTransform)
.padding()
fingerAnchorLogView(title: "Right Index", fingerAnchorFromJointTransform: gestureModel.rightHandIndexFingerTipAnchorFromJointTransform)
}
...
}
}
private func fingerAnchorLogView(title: String, fingerAnchorFromJointTransform: simd_float4x4?) -> some View {
VStack(alignment: .leading) {
Text("\(title) Finger Tip Anchor")
.font(.title)
if let fingerAnchorFromJointTransform = fingerAnchorFromJointTransform {
Text("x: \(String(describing: roundUp(fingerAnchorFromJointTransform.columns.3.x)))")
Text("y: \(String(describing: roundUp(fingerAnchorFromJointTransform.columns.3.y)))")
Text("z: \(String(describing: roundUp(fingerAnchorFromJointTransform.columns.3.z)))")
} else {
Text("x:")
Text("y:")
Text("z:")
}
}
}
ContentView.swiftでは、ハンドトラッキングの場合と同様に、座標をViewに表示します。この際、指の関節の座標だけでなく、ハンドトラッキングの原点となる手首の座標も併せて表示して見比べてみます。
右手(手首) X:0.102、Y:1.264、Z:-0.429
左手(手首) X:-0.148、Y:1.253、Z:-0.413
右手(人差し指) X:-0.162、Y:-0.047、Z:0.035
左手(人差し指) X:0.164、Y:0.046、Z:-0.036
以前、ハンドトラッキング座標はImmersive Spaceの原点からの距離であると説明しました。しかし、人差し指の座標を表示すると、手首の座標と大きく異なることが分かります。なぜこのような違いが生じるのでしょうか。
指の関節はハンドトラッキングの原点からの相対座標
指の座標は、Immersive Spaceの原点からの絶対座標ではなく、手首の原点からの相対座標で表されます。また、右手と左手では座標系が反転していることに注意してください。
右手(人差し指) X:-0.162、Y:-0.047、Z:-0.035
左手(人差し指) X:0.164、Y:0.046、Z:-0.036
これを踏まえて先ほどの人差し指の座標を見ると、右手の人差し指は手首から以下の位置にあることがわかります。
X軸:手首から16.2cm離れている
Y軸:手首から4cm上
Z軸:手首から3cm右
しかし、この相対座標だけでは実用上の問題があります。例えば、人差し指に球体を表示したい場合、Immersive Space上での人差し指の絶対座標が必要になります。つまり、Immersive Spaceの原点から見た人差し指の座標が必要となります。
Immersive Spaceの原点からの指の座標を取得
ではどうすればよいのでしょうか。解決方法は次のように考えられます。
手首の座標は、Immersive Spaceの原点からの絶対座標を表している
手首の座標に、手首から指までの相対座標を組み合わせる
この方法で、Immersive Space内での指の正確な座標を求めることができます。そして、これを簡単に計算する為のmatrix_multiplyというメソッドが準備されています。
# HandGestureModel.swift
var originFromRightHandIndexFingerTipTransform: simd_float4x4? {
guard let rightHandAnchorOriginFromAnchorTransfor = rightHandAnchorOriginFromAnchorTransform,
let rightHandIndexFingerTipAnchorFromJointTransform = rightHandIndexFingerTipAnchorFromJointTransform else { return nil }
return matrix_multiply(rightHandAnchorOriginFromAnchorTransfor, rightHandIndexFingerTipAnchorFromJointTransform)
}
var originFromLeftHandIndexFingerTipTransform: simd_float4x4? {
guard let leftHandAnchorOriginFromAnchorTransfor = leftHandAnchorOriginFromAnchorTransform,
let leftHandIndexFingerTipAnchorFromJointTransform = leftHandIndexFingerTipAnchorFromJointTransform else { return nil }
return matrix_multiply(leftHandAnchorOriginFromAnchorTransfor, leftHandIndexFingerTipAnchorFromJointTransform)
}
手首の座標系であるrightHandAnchorOriginFromAnchorTransforと、人差し指の座標系であるrightHandIndexFingerTipAnchorFromJointTransformをmatrix_multiply(rightHandAnchorOriginFromAnchorTransfor, rightHandIndexFingerTipAnchorFromJointTransform)として計算します。これで、Immersive Spaceの原点からの人差し指の絶対座標が得られます。
右手(手首) X:0.14、Y:1.362、Z:-0.433
左手(手首) X:-0.132、Y:1.359、Z:-0.428
右手(人差し指:手首から) X:-0.164、Y:-0.048、Z:0.028
右手(人差し指:原点から) X:0.139、Y:1.513、Z:-0.515
左手(人差し指:手首から) X:0.165、Y:0.04、Z:-0.034
左手(人差し指:原点から) X:-0.132、Y:1.519、Z:-0.494
この結果から、右手の人差し指はImmersive Space上の下記の位置にあることが分かります。
X軸:体の中心から13.9cm右側
Y軸:地面から1.5mの高さ
Z軸:体の手前側51.5cmの位置
まとめ
指の関節に対して処理をしたい場合には座標系の変換に注意して取り扱う必要があります。ただ、matrix_multiplyのように簡単に計算する方法が準備されているので実際にはそれほど手間はかかりません。