ARKitとデプス


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)利用時にはARFramecapturedDepthDataプロパティは常にnilになります。

毎フレーム更新されるわけではない

デプスカメラのフレームレートはカラーカメラのフレームレートよりも遅いので、毎フレーム更新されるわけではありません。当該フレームにおいてデプスデータが得られない場合にも同プロパティはnilになる可能性があります。

デプスを描画してみる

ARKitで取得したデプスをMTKViewに描画してみます。

上述したとおりAVDepthDataCVPixelBuffer型でデプスマップのピクセルデータを保持しているのでいかようにも処理できるのですが、今回は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()
}

CIContextrender(_: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におけるデプスについての解説がある書籍

宣伝になってしまいますが、「iOS 12 Programming」という書籍で、iOSにおけるデプスの取り扱いについて30ページに渡って解説しています。

- 5.1 はじめに
- 5.2 デプスとは?
- 5.3 Disparity(視差)とDepth(深度)
- 5.4 AVDepthData
- 5.5 iOSにおけるデプス取得方法
- 5.6 デプス取得方法1:撮影済み写真から取得
  - 5.6.1 CGImageSourceオブジェクトを作成する
  - デプスデータをもつ PHAsset だけを取得する
  - 5.6.2 CGImageSource から Auxiliary データを取得する
  - 5.6.3 Auxiliary データから AVDepthData を初期化する
  - デプスマップをCIImageとして取得する
- 5.7 デプス取得方法2:カメラからリアルタイムに取得
  - 5.7.1 デプスが取れるタイプの AVCaptureDevice を使用する
  - 5.7.2 デプスが取れるフォーマットを指定する
  - 5.7.3 セッションの出力に AVCaptureDepthDataOutput を追加する
  - 5.7.4 AVCaptureDepthDataOutputDelegate を実装し、AVDepthData を取得する
  - 5.7.5 AVCaptureDataOutputSynchronizer で出力を同期させる
- 5.8 デプス取得方法3:ARKitから取得
- 5.9 デプス応用1:背景合成
  - 5.9.1 背景合成とは
  - 5.9.2 CIBlendWithMask
  - 5.9.3 デプスデータの加工
- 5.10 デプス応用2:2D写真を3D空間に描画する
- 5.11 PortraitEffectsMatte
  - 5.11.1 AVPortraitEffectsMatte
  - 5.11.2 PortraitEffectsMatteの取得方法
  - 5.11.3 PortraitEffectsMatteの取得条件/制約

ARKitだけではなく、AVFoundationを用いてリアルタイムにカメラからデプスを取得する方法、撮影済み写真から取得する方法、とフレームワークを横断してiOSにおけるデプスの取り扱いについて網羅的に解説しています。"iOS 12"とタイトルにありますが、iOS 13が出ても14が出ても役立つ内容になっているので、デプスに興味のある方はぜひぜひご検討ください。Amazonや書店では販売しておらず、以下のPEAKS社のサイトより購入できます。


  1. ARKitで取得したデプスをMetalシェーダに食わせてエフェクトをつくる、という案件は実際にやったことがあるのですが残念ながら非公開