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


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

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

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

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

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

imk2o/MediaCatalog

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

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

  • AVCaptureDevice.DeviceType.builtInTrueDepthCamera に対応したビデオデバイスを取得
  • AVCapturePhotoOutput#isDepthDataDeliveryEnabledtrue を設定
  • AVCapturePhotoSettings#isDepthDataDeliveryEnabledtrue を設定

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

  • 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#isDepthDataDeliveryEnabledtrue に設定します。
上記で取得したビデオデバイスであれば、AVCapturePhotoOutput#isDepthDataDeliverySupportedtrue になるはずです。

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 #isDepthDataDeliveryEnabledtrue に設定した上で撮影を行います。

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 のイニシャライザオプション kCIImageAuxiliaryDepthtrue を指定すると、深度マップをグレイスケールイメージに変換した画像を取得することができます。

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