Developer's Blog

iOS11 – DepthAPI と OpenGL ES を組み合わせて被写界深度エフェクトをかけてみた

こんにちは。アプリケーション共同開発部エンジニアの浪花です。

iOS11 が提供されて5ヶ月が経過しようとしていますが、個人のアプリや日々の業務で新機能を使った開発が出来ていますでしょうか。

今回 iOS11 で提供されたものの中で Depth API に興味があり OpenGL ES と組み合わせて被写界深度エフェクトをかける簡単なアプリを作成したので紹介したいと思います。

Depth API

iOS11 で追加された Depth API とはカメラから近い物体は白く、遠い物体は黒くといったように空間の深さ(深度情報)を可視化し、画像として出力できる機能になります。

現時点では2つのカメラを搭載した端末でしか試せることが出来ませんが、この機能は今までのカメラでは平面的なデータでしかフィルター処理などできませんでしたが、深度の情報が追加されたことにより、別のアプローチで様々な事ができそうです。

 

被写界深度エフェクトとは

被写界深度は、写真の焦点が合っている範囲のことを言います。

iPhone のカメラアプリでピントを合わせたい被写体をタップすると、周囲にボケが発生し被写体が鮮明に表示されます。

また、ポートレートモードで撮影する場合もこの処理が適用されています。

今回は Depth API で取得した深度情報を使って近くのものにピントを合わせ、遠くのものはボケさせる。といった被写界深度エフェクトの実装例を紹介します。

 

深度情報をみる

それではカメラを使って深度の情報を見ていきます、今回は被写界深度エフェクトの実装方法がメインとなるため、基本的な深度情報の取得には Apple が提供しているサンプルプログラム(AVCamPhotoFilter)を使用し、そちらを改良して実装します。

 

深度情報の取得を有効にする。



    func depthEnable(enable: Bool) {
        var depthEnabled = enable
        depthSmoothingSwitch.isHidden = !depthEnabled
        depthSmoothingLabel.isHidden = !depthEnabled
        mixFactorSlider.isHidden = !depthEnabled
        
        sessionQueue.async {
            self.session.beginConfiguration()
            
            if self.photoOutput.isDepthDataDeliverySupported {
                self.photoOutput.isDepthDataDeliveryEnabled = depthEnabled
            } else {
                depthEnabled = false
            }
            
            self.depthDataOutput.connection(with: .depthData)!.isEnabled = depthEnabled
            
            if depthEnabled {
                // Use an AVCaptureDataOutputSynchronizer to synchronize the video data and depth data outputs.
                // The first output in the dataOutputs array, in this case the AVCaptureVideoDataOutput, is the "master" output.
                self.outputSynchronizer = AVCaptureDataOutputSynchronizer(dataOutputs: [self.videoDataOutput, self.depthDataOutput])
                self.outputSynchronizer!.setDelegate(self, queue: self.dataOutputQueue)
            } else {
                self.outputSynchronizer = nil
            }
            
            self.session.commitConfiguration()
            
            self.dataOutputQueue.async {
                if !depthEnabled {
                    self.videoDepthConverter.reset()
                    self.videoDepthMixer.reset()
                    self.currentDepthPixelBuffer = nil
                }
                self.depthVisualizationEnabled = depthEnabled
            }
            
            self.processingQueue.async {
                if !depthEnabled {
                    self.photoDepthMixer.reset()
                    self.photoDepthConverter.reset()
                }
            }
        }
    }


深度情報の有効のみでは、精度の悪いデータが表示されてしまいます。

 

綺麗にグラデーションのかかった深度情報を今回は使用したいため、バッファに対して smoothing 処理を施します。



    sessionQueue.async {
        self.depthDataOutput.isFilteringEnabled = smoothingEnabled
    }


このように設定をすると、比較的精度の高い深度情報が取得できるようになり、被写界深度処理の際にも綺麗なエフェクトを施せるようになります。

 

GLSL を使って被写界深度エフェクトをかけてみる

深度情報の取得ができるようになったので、被写界深度エフェクトを実装します。

描画には OpenGL ES と GLSL を使用します。

今回 OpenGL ES の準備には GPUImage2 を使用しました。

使用する画像は カメラプレビュー画像 (albedo texture) と深度情報 (depth texture) です。

この2枚の画像を OpenGL ES に渡します、渡した画像は GLSL の Fragment Shader でフィルター処理を実装します。

ぼかし処理には Gaussian filter を使用します。

今回は Gaussian filter の処理に depth texture を使ってフィルターをかけます。以下が Fragment Shader です。

lowp vec3 singlePassGaussianBlur(sampler2D sampler, lowp vec2 texcoord, lowp vec2 scale) {
    lowp vec3 color = vec3(0.0);
    lowp float accum = 0.0;
    lowp float weight;
    lowp vec2 offset;
    
    for(int x = -samples/2; x < samples/2; ++x) {
        for(int y = -samples/2; y < samples/2; ++y) {
            offset = vec2(x, y);
            weight = gaussian(offset);
            color += texture2D(sampler, texcoord + scale * offset).rgb * weight;
            accum += weight;
        }
    }
    
    return color / accum;
}

scale 情報にぼかしの拡大率を設定します、今回は depth texture の値を使用して拡大させます。

 

次に Fragment Shader メインの処理をみてみましょう。



    void main() {
        lowp vec4 albedo = texture2D(inputImageTexture, textureCoordinate);
        lowp vec4 depth = texture2D(inputImageTexture2, textureCoordinate2);
        lowp vec2 scale = ((1.0-depth.xy)/196.0);
        
        gl_FragColor.rgb = singlePassGaussianBlur(inputImageTexture, textureCoordinate, scale);
        gl_FragColor.a = albedo.a;
    }


depth には depth texture のテクセル情報が入っています、深度はカメラの近くは白く遠くは黒くなるような情報になっているので、このままでは先ほど紹介した関数に scale として渡してしまうと、手前がボケ、奥に行くほど鮮明が画像が出力されるようになってしまいます。

なので手前ほど鮮明に、奥に行くほどボケるように値を反転させ移動量を調整しています。

 

シーンを描画

GLSL の作成が完了したので、GPUImage2 を使いシーンを描画します。



    let shader = DepthOfFieldShader()
    
    let output = PictureOutput()
    output.encodedImageFormat = .jpeg
    output.imageAvailableCallback = {image in
        self.previewImageView.image = image
    }
    
    let images = [self.albedo, self.depth]
    for (index, image) in images.enumerated() {
        if let img = image {
            let texture = PictureInput(image: img)
            if index < (images.count-1) {
                texture --> shader
            } else {
                texture --> shader --> output
            }
            texture.processImage(synchronously: true)
        }
    }


shaderObject に albedo, depth texture を登録しフィルター処理を実行しています。

 

最終的な結果が描画されました、手前にあるものが鮮明に、奥に行くほどボケが強くなるよう表示されていると思います。

Fragment Shader の scale から細かなボケの調整が可能なので、色々な算出方法を試せば面白いエフェクトがかけれるかもしれません。

 

まとめ

今回は Depth API と OpenGL ES を組み合わせて被写界深度エフェクトを実装しました。

ぼかし処理を深度情報を元に計算してみましたが、ほかにも遠くほどネガティブなシーンにしてみたり。

深度情報から輪郭を抽出してみたり。

ただの深度の白黒画像と思う人もいるかもしれませんが Depth API を使用すれば世にない新しいアプリのアイデアが出てくると思っています。

ですので一度 Apple 公式のサンプルなどに目を通し、何かアプリのアイデアを考えてみても良いのではないでしょうか。

私も何か公開できるアプリを作ってみようとアイデアを練っています!

 

また OpenGL ES や WebGL などの 3D グラフィックスは、初期化やモデルの準備、描画処理など、単なる板ポリゴンを表示するだけでも手間がかかりなかなか手をつけられない人も多いと思います。

ですが、最近ではブラウザで編集しながら描画処理を確認できる Web サイトがたくさん出てきています。

なのでいきなりポリゴンの表示など難しい事にはトライせず、サクッとサンプルを編集して動きを楽しんでみてはどうでしょうか。

以下に WebGL を編集しながら結果を確認できる Web サイトを紹介します。

https://codepen.io/tag/threejs/

http://glslsandbox.com

 

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

 

 

フェンリル採用チームの Twitter アカウントです。応募前のお問い合わせや、ちょっとした相談ごとなどお気軽にどうぞ!

 

フェンリルの Facebook ページでは、最新トピックをお知らせしています。よろしければいいね!してください!

 

Sleipnir の Facebook ページでは、ユーザーの方たちとのコミュニケーションや最新情報の投稿などを行なっています。よろしければいいね!してください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.