ARKitにおけるデプス取得方法
ARセッション中に毎フレーム得られるARFrame
に、capturedDepthData
というプロパティがあり、ここからAVDepthData
オブジェクトが得られます。
var capturedDepthData: AVDepthData? { get }
AVDepthData
はデプスデータを表すクラスで、ARKitに限らずAVFoundationやImage I/O等々、iOSにおいてどういうフレームワークでデプスを取得するにせよ、(ほとんどの場合は)最終的にデプスデータをこの型で得ることになります。
というわけでこのプロパティにアクセスすればデプスが得られます。
guard let frame = sceneView.session.currentFrame else { return }
let depthData = frame.capturedDepthData
以上!非常にシンプルですね。
AVDepthData
上述したとおり、AVDepthData
はデプスデータを表すクラスで、iOS 11以降で利用可能です。ARKitのクラスではなく、AVFoundationフレームワークに属します。
AVDepthData
は多くのプロパティやメソッドを持ちますが、
最も重要なのはdepthDataMap
プロパティです。
var depthDataMap: CVPixelBuffer { get }
このプロパティはデプスマップのピクセルデータをCVPixelBuffer
型で保持します。
CVPixelBuffer
はiOS 4の頃から存在し、多くの画像を扱うフレームワークがこの型をサポートしているので、デプスデータをAVDepthData
として取得してしまえば、
あとはそのデプスマップをCore ImageでもMetalでも、従来の画像処理方法で好きなように処理可能です。
制約
フェイストラッキング時のみ利用可能
現状ではARFaceTrackingConfiguration
を利用してフェイストラッキングを行っている場合のみこのデプスデータが取得できます。
他のコンフィギュレーション(たとえば平面検出等を行うARWorldTrackingConfiguration
)利用時にはARFrame
のcapturedDepthData
プロパティは常にnil
になります。
毎フレーム更新されるわけではない
デプスカメラのフレームレートはカラーカメラのフレームレートよりも遅いので、毎フレーム更新されるわけではありません。当該フレームにおいてデプスデータが得られない場合にも同プロパティはnil
になる可能性があります。
デプスを描画してみる
ARKitで取得したデプスをMTKView
に描画してみます。
上述したとおりAVDepthData
はCVPixelBuffer
型でデプスマップのピクセルデータを保持しているのでいかようにも処理できるのですが、今回はCIImage
としてMetalでレンダリングすることにします。
というわけでレンダラにこんな感じのCIImage
オブジェクトを引数に取るメソッドを実装します。
func update(with ciImage: CIImage) {
let _ = inFlightSemaphore.wait(timeout: .distantFuture)
guard
let commandBuffer = commandQueue.makeCommandBuffer(),
let currentDrawable = renderDestination.currentDrawable
else {
inFlightSemaphore.signal()
return
}
commandBuffer.addCompletedHandler{ [weak self] commandBuffer in
if let strongSelf = self {
strongSelf.inFlightSemaphore.signal()
}
}
ciContext.render(ciImage, to: currentDrawable.texture, commandBuffer: commandBuffer, bounds: ciImage.extent, colorSpace: colorSpace)
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
CIContext
のrender(_:to:commandBuffer:bounds:colorSpace:)
メソッドで引数に渡されたCIImage
をレンダリングするコマンドをMetalのコマンドバッファにエンコードしているところがポイント。
AVDepthData
が持つCVPixelBuffer
型のデプスマップをCIImage
型にするために、次のようなextensionを用意しておきます。
extension CVPixelBuffer {
func transformedImage(targetSize: CGSize, rotationAngle: CGFloat) -> CIImage? {
let image = CIImage(cvPixelBuffer: self, options: [:])
let scaleFactor = Float(targetSize.width) / Float(image.extent.width)
return image.transformed(by: CGAffineTransform(rotationAngle: rotationAngle)).applyingFilter("CIBicubicScaleTransform", parameters: ["inputScale": scaleFactor])
}
}
上のメソッドを使って、CVPixelBuffer
からCIImage
オブジェクトを作成しつつ、描画サイズにリサイズしつつ、回転を補正します。
extension ARFrame {
func transformedDepthImage(targetSize: CGSize) -> CIImage? {
guard let depthData = capturedDepthData else { return nil }
return depthData.depthDataMap.transformedImage(targetSize: CGSize(width: targetSize.height, height: targetSize.width), rotationAngle: -CGFloat.pi/2)
}
}
なお、重要な点として、CIImage
のリサイズや回転処理はまだこの時点では行われず、Metalのコマンドバッファにレンダリングコマンドをエンコードし、コミットした後にGPU側で処理される、という点です。というわけでCPUとGPUを行ったり来たりせずリサイズ等の処理〜描画の一連の処理がGPU側でまとめて処理されることになります。(このあたりの話は拙著「Metal入門」の第12章に書いてあります)
ARFrame
からcapturedDepthData
が取得できたら上のメソッドを使用してCIImage
に変換しておき、
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard let frame = sceneView.session.currentFrame else { return }
if let depthImage = frame.transformedDepthImage(targetSize: currentDrawableSize) {
self.depthImage = depthImage
}
}
あとはMTKView
の描画のタイミングで、CIImage
オブジェクトをレンダリングメソッドに渡します。
func draw(in view: MTKView) {
if let image = depthImage {
renderer.update(with: image)
}
}
このサンプルは「iOS-Depth-Sampler」としてオープンソースにしているのでそちらもご参照ください。
ここではシンプルに可視化しただけですが、このデプスをオクルージョンに使うもよし、エフェクトに使うもよしで、色々と活用してみてください。 1
フェイストラッキングではデプスデータを使用しているのか?
ARKitのフェイストラッキングは赤外線カメラを塞いでも動作します。なので、赤外線カメラ(すなわちそこから得られるデプス)を使用してないのでは、という説があります。
それだとフェイストラッキング時にデプスデータを取得できることと矛盾する気がしたので、実際に試してみました。挙動としては
- 赤外線カメラを塞ぐとデプスの供給が止まる
- 塞いでいてもフェイストラッキングは動作する
という感じになりました。顔のトラッキング自体はデプスなしで動作するようになっていて、開発者がオクルージョン等で使用できるようにデプスデータも提供してくれてるのでしょうか。でもARFaceTrackingConfiguration
をTrueDepthカメラあり端末に限定してるからにはやはり内部でも何かしらの用途で使用しているのでしょうか。WWDCに参加できたらラボで聞いてみたいものです。
で、拙作iOS-Depth-Samplerでデプスデータを可視化しつつ赤外線カメラを指で塞いだり指を離したりしてわかったのは、
— Shuichi Tsutsumi (@shu223) 2018年9月25日
・赤外線カメラを塞ぐとデプスの供給が止まる
・塞いでいてもフェイストラッキングは動作する
デプスが取れれば使用し、取れなくてもそれなりに動作するようになってるっぽい
iOSにおけるデプスについての解説がある書籍
「Depth in Depth - iOSデプス詳解」という書籍で、iOSにおけるデプスの取り扱いについて72ページに渡って解説しています。
第1章 デプスの概要
- 1.1 デプスとは?
- 1.2 デプスの用途
- 1.3 Disparity(視差)とDepth(深度)
- 1.4 AVDepthData
第2章 iOSにおけるデプス取得方法
- 2.1 デプス取得方法1: 撮影済み写真から取得
- 2.2 デプス取得方法2: カメラからリアルタイムに取得
- 2.3 デプス取得方法3: ARKitから取得
第3章 デプス応用1: 背景合成
- 3.1 CIBlendWithMask
- 3.2 デプスデータをそのままマスクとして用いる
- 3.3 デプスマップを二値化する
- 3.4 マスクの平滑化
第4章 デプス応用2: 2D写真から3D点群を生成する
- 4.1 3D点群座標を求める計算式
- 4.2 Intrinsic Matrix
- 4.3 3D点群座標計算の実装
- Intrinsic Matrixを取得する
- Intrinsic Matrixをスケールする
- Intrinsic Matrixを用いてX, Yを計算する
第5章 Portrait Effects Matte
- 5.1 AVPortraitEffectsMatte
- 5.2 Portrait Effects Matteの取得方法
- 5.3 Portrait Effects Matteの取得条件/制約
第6章 Semantic Segmentation Matte
- 6.1 Semantic Segmentation Matteの取得方法
- 6.2 AVSemanticSegmentationMatte
- 6.3 SSMをCIImage経由で取得する
第7章 People Occlusion (ARKit)
- 7.1 PeopleOcclusionの実装方法
- 7.2 personSegmentationとpersonSegmentationWithDepth の違い
- 7.3 利用可能なコンフィギュレーション
- 7.4 segmentationBufferとestimatedDepthData
- 7.5 Metalカスタムレンダリング時のオクルージョン
- ARMatteGenerator
- オクルージョン処理のMetalシェーダ
第8章 デプス推定
- 8.1 FCRN-DepthPredictionモデル
- 8.2 デプス推定モデルを使用する
第9章 一般物体のセグメンテーション
- 9.1 iOSにおける他のセグメンテーション手段との違い
- 9.2 DeeplabV3を利用したリアルタイムセグメンテーションの実装
ARKitだけではなく、AVFoundationを用いてリアルタイムにカメラからデプスを取得する方法、撮影済み写真から取得する方法、とフレームワークを横断してiOSにおけるデプスの取り扱いについて網羅的に解説しています。デプスに興味のある方はぜひぜひご検討ください。
-
ARKitで取得したデプスをMetalシェーダに食わせてエフェクトをつくる、という案件は実際にやったことがあるのですが残念ながら非公開 ↩