LoginSignup
30
20

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)
    }
}

arkit-depth.gif

このサンプルは「iOS-Depth-Sampler」としてオープンソースにしているのでそちらもご参照ください。

ここではシンプルに可視化しただけですが、このデプスをオクルージョンに使うもよし、エフェクトに使うもよしで、色々と活用してみてください。 1

フェイストラッキングではデプスデータを使用しているのか?

ARKitのフェイストラッキングは赤外線カメラを塞いでも動作します。なので、赤外線カメラ(すなわちそこから得られるデプス)を使用してないのでは、という説があります。

Screen Shot 2018-12-18 at 9.05.01.png

それだとフェイストラッキング時にデプスデータを取得できることと矛盾する気がしたので、実際に試してみました。挙動としては

  • 赤外線カメラを塞ぐとデプスの供給が止まる
  • 塞いでいてもフェイストラッキングは動作する

という感じになりました。顔のトラッキング自体はデプスなしで動作するようになっていて、開発者がオクルージョン等で使用できるようにデプスデータも提供してくれてるのでしょうか。でもARFaceTrackingConfigurationをTrueDepthカメラあり端末に限定してるからにはやはり内部でも何かしらの用途で使用しているのでしょうか。WWDCに参加できたらラボで聞いてみたいものです。

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におけるデプスの取り扱いについて網羅的に解説しています。デプスに興味のある方はぜひぜひご検討ください。

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

30
20
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
20