デプス（深度）をiOSで扱う方法を網羅したサンプルコード集「iOS-Depth-Sampler」

原始のiPhoneからカメラは搭載されていましたが、デプス（深度）が取れるデュアルカメラやTrueDepthカメラが搭載されたのは比較的最近のことです。従来のカメラやGPSが、デジタルの世界と我々が生きる現実世界を繋ぐ重要な役割を担い、アプリ開発者に多くの創造性を与えてくれたのと同様に、**「奥行き」がわかるようになったというのはアプリ開発の次元がひとつ増えたようなもの。そのわりに、深度関連APIにどのようなものがあるか、何ができるか、どう実装するのか、という情報は日本語はもちろん、英語でもあまり多くはありません**。

そこで、iOSにおけるデプス（深度）関連APIの実装方法を示すサンプル集「iOS-Depth-Sampler」をオープンソースで公開しました。

- https://github.com/shu223/iOS-Depth-Sampler

ソースコードは GitHub に置いてあるので、ご自由にご活用ください。Swift 4.2です。

今のところ6つのサンプル（後述）が入っています。本記事のタイトル「網羅した」は少し大げさですが、**撮影済みの写真からデプスを取得する方法から、リアルタイムに取得する方法、ARKitで取得する方法、フロント／リアカメラ、Disparity / Depth / Portrait Matteといったタイプと相互変換、デプスを利用した背景合成や3D空間へのマッピング**といったサンプルも備えています。

![3d.png](https://qiita-image-store.s3.amazonaws.com/0/3180/29c0b867-73f5-bc82-4c0b-c5db34fb3c38.png)

<span style="font-size: 80%">（2次元の写真をデプスを利用して3D空間へマッピング）</span>

今後iOSにデプス関連の機能が追加されるたびに、サンプルも追加していく予定です。


## 利用可能なデバイス

<b><span style="color: #ff0000">デュアルカメラもしくはTrueDepthカメラを持つデバイスを使用してください</span></b>。

2018年9月現在、7 Plus, 8 Plus, X, XS, XRが対象機種です。お持ちの方はぜひビルドして遊んでみてください。リア（Dual Camera）とフロント（TrueDepth Camera）でそれぞれサポートしている機能が全然違うので、X or XSが理想です。


## 利用方法

git cloneしてXcode 10でビルドしてiOS 12端末 [^ios12] にインストール

以上です。Metalを使用しているのでシミュレータでは動作しない点、ご注意ください。


[^ios12]: **ほとんどのデプスAPIはiOS 11から使える**のですが、インパクトのある「Portrait Matte」がiOS 12〜なので、本サンプルはiOS 12以上にしてしまいました。


## サンプル一覧

### Real-time Depth

リアルタイムにカメラから深度情報を取得し、可視化するサンプルです。

![depth_1.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/f9824bcb-9d91-341c-9be8-7fd1d2f85f38.gif)

### Real-time Dpeth Mask

リアルタイムにカメラから取得した深度情報をマスクとして使用し、背景合成を行うサンプルです。

![blend.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/2130d133-ccbb-c694-ee16-e125b9fdb851.gif)

### Depth from Camera Roll

カメラロールにある撮影済みの写真から深度情報を取り出し、可視化するサンプルです。標準のカメラアプリのポートレートモードで撮影した写真を利用できます。

![depth_baby_histoeq.jpg](https://qiita-image-store.s3.amazonaws.com/0/3180/d8fd8791-6919-92c4-dafe-cedc0d456804.jpeg)


### Portrait Matte

Portrait Matte (Portrait Effect Matte)を使用し、背景を除去するサンプルです。ポートレートモードで「人間を撮影」した写真を利用できます。要iOS 12。

![portraitmatte.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/deffb418-157e-8d1b-1a06-fba3cf11be8c.gif)

カメラはフロント・リアどちらもOKです。

関連記事: [iOS 12のPortrait Matteがすごい - その後のその後](http://shu223.hatenablog.com/entry/2018/08/22/200226)

### ARKit Depth

ARKitで深度情報を取得し、可視化するサンプルです。

![arkit-depth.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/fbd5326d-2ef3-6311-5b47-3b8f50168fed.gif)

現状、ARKitでは`ARFaceTrackingConfiguration`使用時のみ（つまりフェイストラッキングARのときだけ）この方法で（※後で貼るスライド内で説明しています）深度情報を取得できます。


### 2D image in 3D space

写真（2D）を深度をz値として3D空間にマッピングするサンプルです。

![3d.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/16ff2c31-2793-1ded-9354-305827efd4e5.gif)


### AR occlusion

[WIP] An occlusion sample on ARKit using depth.



## そもそもデプスって？

「デプスって何？」「何が嬉しいの？」「どういう仕組みで計測してるの？」といったところは拙スライドもご参照ください。

- [Depth in Depth #iOSDC - Speaker Deck](https://speakerdeck.com/shu223/depth-in-depth-number-iosdc)
- [iOSDCで「深度（デプス）」について話しました #iosdc - その後のその後](http://shu223.hatenablog.com/entry/2018/09/06/095452)


## 付録: iOSで深度データにアクセスする方法のまとめ（書きかけ）

途中になってるので、ここに一旦おいておきます。

### PHAssetから深度マップ画像を生成

```swift
requestContentEditingInput(with: nil) { (contentEditingInput, info) in
    guard let url = contentEditingInput?.fullSizeImageURL else { fatalError() }
    let depthImage = CIImage(contentsOf: url, options: [CIImageOption.auxiliaryDisparity : true])!
    ...
}
```

### 深度データを持つPHAssetだけを取得する

```swift
let resultCollections = PHAssetCollection.fetchAssetCollections(
    with: .smartAlbum,
    subtype: .smartAlbumDepthEffect,
    options: nil)
var assets: [PHAsset] = []
resultCollections.enumerateObjects({ collection, index, stop in
    let result = PHAsset.fetchAssets(in: collection, options: nil)
    result.enumerateObjects({ asset, index, stop in
        assets.append(asset)
    })
})
```

### PHAssetが持つ深度データからAVDepthData生成

アセットのURLを指定して、`CGImageSource`を作成する。

```swift
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { fatalError() }
```

`CGImageSourceCopyAuxiliaryDataInfoAtIndex`を使用する

```swift
let info = CGImageSourceCopyAuxiliaryDataInfoAtIndex(source, 0, kCGImageAuxiliaryDataTypeDisparity) as? [String : AnyObject]
```

```swift
let info = CGImageSourceCopyAuxiliaryDataInfoAtIndex(source, 0, kCGImageAuxiliaryDataTypeDepth) as? [String : AnyObject]
```

ここで得られる辞書型データは次のような情報を持っている。

- the depth data (CFDataRef) - (kCGImageAuxiliaryDataInfoData),
- the depth data description (CFDictionary) - (kCGImageAuxiliaryDataInfoDataDescription)
- metadata (CGImageMetadataRef) - (kCGImageAuxiliaryDataInfoMetadata)

この辞書型データをそのまま`AVDepthData`のイニシャライザに渡せる。

```swift
let depthData = try! AVDepthData(fromDictionaryRepresentation: info)
```

### `CGImageSourceCopyProperties`から深度に関する情報を取得する

以下のように辿って深度に関する情報を得ることもできる。

```swift
guard let sourceProperties = CGImageSourceCopyProperties(source, nil) as? [String: AnyObject] else { fatalError() }
guard let fileContentsProperties = sourceProperties[String(kCGImagePropertyFileContentsDictionary)] as? [String : AnyObject] else { fatalError() }
guard let images = fileContentsProperties[String(kCGImagePropertyImages)] as? [AnyObject] else { return nil }
for imageProperties in images {
    guard let auxiliaryDataProperties = imageProperties[String(kCGImagePropertyAuxiliaryData)] as? [[String : AnyObject]] else { continue }
    ...
}
```

こういう情報が取れる。

> ["Width": 576, "AuxiliaryDataType": kCGImageAuxiliaryDataTypeDisparity, "Height": 768]

とはいえ深度マップのデータそのものは入っていない。


### AVCaptureSynchronizedDataCollectionからデータを取り出す

`AVCaptureDataOutputSynchronizerDelegate`のdidOutputメソッドの第２引数から`AVCaptureSynchronizedDataCollection`オブジェクトが得られる。

```swift
func dataOutputSynchronizer(_ synchronizer: AVCaptureDataOutputSynchronizer, didOutput synchronizedDataCollection: AVCaptureSynchronizedDataCollection) {

    if let syncedDepthData = synchronizedDataCollection.synchronizedData(for: depthDataOutput) as? AVCaptureSynchronizedDepthData, !syncedDepthData.depthDataWasDropped {
        let depthData = syncedDepthData.depthData
        ...
    }
    
    if let syncedVideoData = synchronizedDataCollection.synchronizedData(for: videoDataOutput) as? AVCaptureSynchronizedSampleBufferData, !syncedVideoData.sampleBufferWasDropped {
        let videoSampleBuffer = syncedVideoData.sampleBuffer
        ...
    }
}
```


## iOS 12のPortrait Matte

```swift
let info = CGImageSourceCopyAuxiliaryDataInfoAtIndex(self, 0, kCGImageAuxiliaryDataTypePortraitEffectsMatte) as? [String : AnyObject]
```

```swift
let matte = try! AVPortraitEffectsMatte(fromDictionaryRepresentation: info)
```