AVDepthData+Core Imageで深度マップを使った写真加工をする

iOS11から、写真の深度マップを扱うAPIが追加されました。
この深度マップは、iOS標準のカメラアプリにある「ポートレート撮影」などに使用されており、応用次第で様々な写真効果が実現できる可能性があります。

追加されたAPIにより、大別すると以下の2つができるようになりました。

* AVFoundation: 深度マップ付きの写真撮影
* Core Image: 写真から深度マップの抽出、これを用いたフィルタ機能

これらについては、WWDC17のセッションに詳細な内容が紹介されています。

* [Image Editing with Depth](https://developer.apple.com/videos/play/wwdc2017/508/)
    - 深度マップをCore Imageで扱う方法
* [Advances in Core Image: Filters, Metal, Vision, and More](https://developer.apple.com/videos/play/wwdc2017/510/)
    - 深度マップ関連で追加されたCIFilter
    - 上記の使用例
* [Capturing Depth in iPhone Photography](https://developer.apple.com/videos/play/wwdc2017/507/)
    - 深度マップ生成の仕組み
    - 深度マップの解像度について

それぞれのセッションに重要な情報があるものの、これらをまとめた記事やサンプルコードが無かったので作成してみました。

[imk2o/MediaCatalog](https://github.com/imk2o/MediaCatalog)

## AVFoundationで深度マップ付きの写真を撮影する

深度マップ付きの写真を撮影するには、以下の設定を追加します。

* `AVCaptureDevice.DeviceType.builtInTrueDepthCamera` に対応したビデオデバイスを取得
* `AVCapturePhotoOutput#isDepthDataDeliveryEnabled` に `true` を設定
* `AVCapturePhotoSettings#isDepthDataDeliveryEnabled` に `true` を設定

写真撮影後、以下のいずれかの方法により深度マップを取得することができます。

* `AVCapturePhoto#depthData` を参照する
* `AVCapturePhoto#fileDataRepresentation()`で保存した深度マップ付きの写真を、Core Imageで読み込む(後述)

### 深度マップが取得できるビデオデバイスの取得

深度マップが取得できるビデオデバイスを取得するには、`AVCaptureDevice.DeviceType`が `.builtInTrueDepthCamera` に合致するものを探す必要があります。

```
func depthCapturableVideoDevice() -> AVCaptureDevice? {
    if #available(iOS 11.1, *) {
        return AVCaptureDevice.default(.builtInTrueDepthCamera, for: .video, position: .back)
    } else {
        return nil
    }
}
```

執筆時点では、デュアルカメラを搭載した下記のデバイスのみ対応しているようです。

* iPhone X
* iPhone 8 Plus
* iPhone 7 Plus

### ビデオデバイスの出力設定

`AVCaptureSession` に出力先として `AVCapturePhotoOutput` を追加した後、`AVCapturePhotoOutput#isDepthDataDeliveryEnabled` を `true` に設定します。 
上記で取得したビデオデバイスであれば、`AVCapturePhotoOutput#isDepthDataDeliverySupported` が `true` になるはずです。

```
self.captureSession = AVCaptureSession()
self.photoOutput = AVCapturePhotoOutput()

...

if self.captureSession.canAddOutput(self.photoOutput) {
    self.captureSession.addOutput(self.photoOutput)

    self.photoOutput.isDepthDataDeliveryEnabled = self.photoOutput.isDepthDataDeliverySupported
}
```

### 写真設定と撮影

撮影方法を `AVCapturePhotoSettings` に設定する際、 `AVCapturePhotoSettings #isDepthDataDeliveryEnabled` を `true` に設定した上で撮影を行います。

```
let photoSettings = AVCapturePhotoSettings()

...
            
if self.photoOutput.isDepthDataDeliverySupported {
    photoSettings.isDepthDataDeliveryEnabled = true
}

...

// 撮影
self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
```

撮影後は `AVCapturePhoto` から深度マップを参照することができますが、以下のようにしてローカルストレージに保存することもできます。

```
// photo: AVCapturePhoto
guard let photoData = photo.fileDataRepresentation() else {
    return // FIXME: Alert error
}

// Temporaryに保存
let storeURL = FileManager.default
    .temporaryDirectory
    .appendingPathComponent(UUID().uuidString)
do {
    let _ = try photoData.write(to: storeURL)
} catch {
    return // FIXME: Alert error
}
```

## Core Imageで写真から深度マップを抽出する

まず深度マップは、以下のいずれかから参照することを考えます。

* `AVDepthData`
* 深度マップ付きの写真(HEIF)

### `AVDepthData`から`CIImage`を生成する

`AVDepthData#depthDataMap` から深度マップを取得することができますので、これを `CIImage` のイニシャライザに与えます。

```
extension CIImage {
    convenience init(depthData: AVDepthData) {
        self.init(cvPixelBuffer: depthData.depthDataMap)
    }
}
```

### 深度マップ付きの写真を`CIImage`で読み込む

`CIImage` のイニシャライザオプション `kCIImageAuxiliaryDepth` に `true` を指定すると、深度マップをグレイスケールイメージに変換した画像を取得することができます。

```
func depthImage(for url: URL) -> CIImage? {
    return CIImage(contentsOf: url, options: [
        kCIImageAuxiliaryDepth: true,
        kCIImageApplyOrientationProperty: true
    ])
}

```

## 深度マップを使った写真のフィルタリング

深度マップをグレイスケール・イメージに変換できれば、マスクとして使用したり、フィルタ適用の強弱に用いることができますね！

### 視差マップへの変換について

深度マップは実際のところ、2つレンズで撮影した画像で求められた **視差** から作られています。視差と深度は反比例の関係にあるため、遠いものほど0(黒)に近づくデータになります。`CIFilter` の "CIDepthToDisparity" を適用することで、視差マップに変換することができます。

```
func disparityImage(for url: URL) -> CIImage? {
    return self.depthImage(for: url)?.applyingFilter("CIDepthToDisparity")
}
```

深度マップと視差マップのどちらを参照するかは用途に依るかと思いますが、例えばポートレート写真風に加工する `CIDepthBlurEffect` に用いる場合は、視差マップを利用するほうがより現実の効果に近づくのではないかと思います。

なおこのフィルタで変換すると、結果がR成分にしか含まれていないので注意しましょう。

### 合成

試しに視差マップをマスクとして、画像に単色を合成してみます。
このとき注意が必要なのは、 **カラー部分の写真解像度と、深度マップの解像度は異なる**ことです。深度(視差)マップはカラーデータの解像度より低いものとなっているので、合成する前に解像度を合わせる必要があります。

```
func composedImage(for url: URL) -> CIImage? {
    guard
        let colorImage = self.colorImage(for: url),
        let disparityImage = self.disparityImage(for: url)
    else {
        return nil
    }

    // R -> Alpha
    let transform = CGAffineTransform(
        scaleX: colorImage.extent.width / disparityImage.extent.width,
        y: colorImage.extent.height / disparityImage.extent.height
    )
    let alphaMaskImage = disparityImage.applyingFilter(
        "CIColorMatrix",
        parameters: [
            "inputAVector": CIVector(x: 1, y: 0, z: 0, w: 0)
        ]
    )
    .applyingFilter("CIColorClamp")
    .transformed(by: transform)    // colorImage()にサイズを合わせる

    // 合成する画像(青単色)
    let backgroundImage = CIImage(color: CIColor(red: 0, green: 0, blue: 1, alpha: 1)).cropped(to: colorImage.extent)

    return colorImage.applyingFilter(
        "CIBlendWithAlphaMask",
        parameters: [
            kCIInputBackgroundImageKey: backgroundImage,
            kCIInputMaskImageKey: scaledAlphaMaskImage
        ]
    )
}
```

## むすび

深度マップを活用することで、スマートフォンならではの、よりフォトジェニック(言ってみたかった)な写真が作れるのではないでしょうか。現時点で対応するデバイスは限られていますが、いずれ多くのユーザが使ってみたくなる機能になることでしょう！

## 参考記事

[Image Depth Maps Tutorial for iOS: Getting Started](https://www.raywenderlich.com/168312/image-depth-maps-tutorial-ios-getting-started)