【ARKit3】ARMatteGenerator 使ってみた!


iOS ベータ機能 (2019/7月現在) でありARの可能性を拡げてくれそうな、ARKit3 のARMatteGeneratorを触ってみたので機能や内容について共有します。

はじめに

この記事では以下について紹介します。

  • ARMatteGeneratorって?
  • ARKit3 からカメラ画像を取り出す
  • alphaおよびdepth画像の取得方法とMetal
  • 人物の背景除去

以下では、Apple が公開しているサンプルコード (リンク) を活用してコードの中身の簡単な解説と、ちょっとした応用として人物の背景を除去する方法について紹介します。

使用機材

  • iPhone XS
    : iOS 13 Beta 版をインストールしたもの

  • MacBook Pro (13”, 2018)
    : XCode 11 Beta2 版をインストールしたもの

ARMatteGenerator って?

カメラ映像中の人物の "距離" と "マスク" を画像オブジェクトとして生成するためのものです。以下のような雰囲気です。

(Apple公式から引用、動画3分ごろ)

空間に配置したARオブジェクトと検出された人物の前後関係を把握し、適切にマスキングしています。上記リンクの動画によるとARKit3では、機械学習により人物のみですが距離を推定してARと人がインタラクトする表現が可能になったとのこと!

ARKit で画像データを取り出す

【備考】 Metalについて

ARMatteGeneratorを使う計算は、Swift の GPU 描画ライブラリ “Metal” を使うことで最適なパフォーマンスを発揮できます。Metalの説明は非常に複雑 (筆者も深くは理解できてない😅) なので、ここでは要点だけご紹介出来ればと思います。

後日、別記事にてMetalを使った描画の全体像をイラストを使って分かりやすくまとめてみたいと思います。乞うご期待!

CurrentFrame

RGB画像については、従来のARKitから存在するARSessionクラスのcurrentFrameメソッドを使ってpixelbuffer形式で取得できます。

guard let currentFrame = session.currentFrame else {return}
var pixelBuffer = currentFrame.capturedImage

なおsessionは、ARSessionオブジェクトであり、ARKitのどのフレームワークにおいても必須のものです。

ARMatteGenerator

距離 (depth) 画像とマスク (alpha) 画像については、ARKit3 で追加された “ARMatteGenerator” (リンク) クラスをつかって、MTLTextureオブジェクトとして取得することができます。

alphaTexture = matteGenerator.generateMatte(from: currentFrame, commandBuffer: commandBuffer)
dilatedDepthTexture = matteGenerator.generateDilatedDepth(from: currentFrame, commandBuffer: commandBuffer)

(commandBuffer については別記事で解説予定です。)

matteGeneratorは、ARMatteGeneratorオブジェクトであり、予めinitializeしてあります (本記事では記載なし)。

MTLTextureは、例えば以下のように簡単にCIImageに変換することができます。

// .portrait の向きに合うように変換した画像を .transform で回転
alphaImage = CIImage(mtlTexture: dilatedDepthTexture!, options: nil)?
            .transformed(by: CGAffineTransform(scaleX: 1, y: -1)
                .translatedBy(x: 0, y: CGFloat(alphaTexture!.height)))
            .transformed(by: CGAffineTransform(rotationAngle: angle))
// UIImage に変換したい場合は以下
let ciContext = CIContext()
let cgImage = ciContext.createCGImage(alphaImage, from: alphaImage.extent)
let uiImage = UIImage(cgImage: cgImage!)

変換後のマスク画像 (alpha) を、CIImage -> CGImage -> UIImage の順に変換して可視化してみると以下のような感じです。

(写真)

(備考)

ここで紹介している方法は基本的にMetalを用いることが想定されており、記述が複雑になります。実は単純に背景除去や顔の occlusion (ARエフェクト) をやるだけであれば上記のARMatteGenerator は不要で、より簡単な方法があります。

替わりに frameSemantics を使用できます。公式ドキュメント (リンク) 等をご覧ください。

Mattingの方法

Matting by Metal

Metalでは数種類のshaderという関数を用いて画像の描画内容を定義することで高速なGPU計算を実行します。

VertexShader

ここで RGB、マスク、ARオブジェクトなどの頂点位置を決めます。2次元の画像であればピクセルの位置を、3次元オブジェクトであればメッシュの位置を決めるようなイメージです。

公式のサンプルコードでは背景、ARオブジェクト、前景(検出された人)とを分けてshaderで描画しています。ここでは代表して、前景の描画を実施するshaderの例を示しています。

vertex CompositeColorInOut compositeImageVertexTransform(const device CompositeVertex* cameraVertices
                                                            [[ buffer(0) ]],
                                                         unsigned int vid [[ vertex_id ]]) {
    CompositeColorInOut out;

    const device CompositeVertex& cv = cameraVertices[vid];

    out.position = float4(cv.position, 0.0, 1.0);
    out.texCoordCamera = cv.texCoord;

    return out;
}

cameraVertices は画像の頂点bufferデータであり、mesh 頂点のXY位置と mesh を埋めるテクスチャー (色) の順番とを持っています。これら頂点情報はここでは示していませんが iPhone の画面サイズに合うよう別途調整されています。

実行時に (修飾子 [[vertex_id]] を指定していることで) 自動で割り当てられる インデックス vid を指定して各個のピクセルにアクセスできます。

FragmentShader

VertexShaderで決めたそれぞれのピクセル (=頂点、CompositeColorInOut in に入る) に対し、色情報 = texture を計算します。

fragment half4 compositeImageFragmentShader(CompositeColorInOut in [[ stage_in ]],
                                    texture2d<float, access::sample> capturedImageTextureY [[ texture(0) ]],
                                    texture2d<float, access::sample> capturedImageTextureCbCr [[ texture(1) ]],
                                    texture2d<float, access::sample> alphaTexture [[ texture(2) ]])
{
    constexpr sampler s(address::clamp_to_edge, filter::linear);

    float2 cameraTexCoord = in.texCoordCamera;
    // rgb ピクセル値をfloat4型 (RGBA) でセット
    float4 rgb = ycbcrToRGBTransform(capturedImageTextureY.sample(s, cameraTexCoord), capturedImageTextureCbCr.sample(s, cameraTexCoord));

    // mask (alpha) 画像のピクセル値をhalf型 (1チャネル) でセット
    half alpha = half(alphaTexture.sample(s, cameraTexCoord).r);

    half4 whiteColor = half4(1.0, 1.0, 1.0, 1.0);
    // mix(a, b, x) は、 a*x + b*(1-x) を返す。即ち Alpha Blending を実行。
    half4 mattingResult = mix(whiteColor, cameraColor, alpha);

    return mattingResult

ycbcrToRGBTransform はYCbCr to RGBの変換関数で、サンプルコード中に記載がありますので割愛します。

各Metal Textureを引数に取り、出力のピクセル値を決定しています。

背景除去の結果

こちらが結果です!

(写真)

結構良い精度でマスキングできてます!👍

CIFilter と組み合わせる

単なる背景除去だとイマイチですが、フィルターをちょこっと加えるだけで面白くなります。CIFilterの既成フィルターである CIComicEffect を使うと、、、

(写真)

これだと輪郭の曖昧さが意外と気にならずいい感じです! ARKit3なら、ここにARオブジェクトを色々貼ってインタラクティブな表現が可能なので色んな表現ができそうです!

おわりに

いかがでしたでしょうか?

ご参考になれば幸いです!
改善方法やご意見などあれば、どしどしコメント下さい!