iOS11から、写真の深度マップを扱うAPIが追加されました。
この深度マップは、iOS標準のカメラアプリにある「ポートレート撮影」などに使用されており、応用次第で様々な写真効果が実現できる可能性があります。
追加されたAPIにより、大別すると以下の2つができるようになりました。
- AVFoundation: 深度マップ付きの写真撮影
- Core Image: 写真から深度マップの抽出、これを用いたフィルタ機能
これらについては、WWDC17のセッションに詳細な内容が紹介されています。
-
Image Editing with Depth
- 深度マップをCore Imageで扱う方法
-
Advances in Core Image: Filters, Metal, Vision, and More
- 深度マップ関連で追加されたCIFilter
- 上記の使用例
-
Capturing Depth in iPhone Photography
- 深度マップ生成の仕組み
- 深度マップの解像度について
それぞれのセッションに重要な情報があるものの、これらをまとめた記事やサンプルコードが無かったので作成してみました。
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
]
)
}
むすび
深度マップを活用することで、スマートフォンならではの、よりフォトジェニック(言ってみたかった)な写真が作れるのではないでしょうか。現時点で対応するデバイスは限られていますが、いずれ多くのユーザが使ってみたくなる機能になることでしょう!