ARKitとデプス

## ARKitにおけるデプス取得方法

ARセッション中に毎フレーム得られる`ARFrame`に、`capturedDepthData`というプロパティがあり、ここから`AVDepthData`オブジェクトが得られます。

```swift
var capturedDepthData: AVDepthData? { get }
```

`AVDepthData`はデプスデータを表すクラスで、ARKitに限らずAVFoundationやImage I/O等々、iOSにおいてどういうフレームワークでデプスを取得するにせよ、（ほとんどの場合は）最終的にデプスデータをこの型で得ることになります。

というわけでこのプロパティにアクセスすればデプスが得られます。

```swift
guard let frame = sceneView.session.currentFrame else { return }
let depthData = frame.capturedDepthData
```

以上！非常にシンプルですね。

## AVDepthData

上述したとおり、`AVDepthData`はデプスデータを表すクラスで、iOS 11以降で利用可能です。ARKitのクラスではなく、AVFoundationフレームワークに属します。

`AVDepthData`は多くのプロパティやメソッドを持ちますが、
最も重要なのは`depthDataMap`プロパティです。

```swift
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`オブジェクトを引数に取るメソッドを実装します。

```swift
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を用意しておきます。

```swift
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`オブジェクトを作成しつつ、描画サイズにリサイズしつつ、回転を補正します。

```swift
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入門](https://booth.pm/ja/items/826318)」の第１２章に書いてあります）

`ARFrame`から`capturedDepthData`が取得できたら上のメソッドを使用して`CIImage`に変換しておき、

```swift
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`オブジェクトをレンダリングメソッドに渡します。

```swift
func draw(in view: MTKView) {
    if let image = depthImage {
        renderer.update(with: image)
    }
}
```


![arkit-depth.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/186ea9e9-4301-8807-7b32-d59cb07898c9.gif)

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

- [デプス（深度）をiOSで扱う方法を網羅したサンプルコード集「iOS-Depth-Sampler」](https://qiita.com/shu223/items/a26573b6838f68e82b41)


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

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

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

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

<img width="521" alt="Screen Shot 2018-12-18 at 9.05.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/76cd7f7d-d5c5-4cb8-f84c-69571a24af34.png">

https://speakerdeck.com/algal/augmented-reality-beyond-arkit?slide=34

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

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

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

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">で、拙作iOS-Depth-Samplerでデプスデータを可視化しつつ赤外線カメラを指で塞いだり指を離したりしてわかったのは、<br>・赤外線カメラを塞ぐとデプスの供給が止まる<br>・塞いでいてもフェイストラッキングは動作する<br>デプスが取れれば使用し、取れなくてもそれなりに動作するようになってるっぽい</p>&mdash; Shuichi Tsutsumi (@shu223) <a href="https://twitter.com/shu223/status/1044529454993666049?ref_src=twsrc%5Etfw">2018年9月25日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



## iOSにおけるデプスについての解説がある書籍

「[Depth in Depth - iOSデプス詳解](https://booth.pm/ja/items/1313752)」という書籍で、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におけるデプスの取り扱いについて網羅的に解説しています。デプスに興味のある方はぜひぜひご検討ください。

https://booth.pm/ja/items/1313752