ARKitでバーチャル背景つくってみた


今日この頃

新型コロナウイルスの影響で、リモート会議やリモート飲み
などが盛ん(?)に行われていると思います。

Zoomなどのサービスを使うと、カメラを使って顔出しをする際に
周りの背景などを写したくない人の為に、人物部分だけを抜き出して
好きな背景を設定できる「バーチャル背景」機能があります。
(いろいろな会社さんがバーチャル背景用の画像をツイートしていたりします)

使ってみると結構綺麗に抜けます。
おそらく機械学習でデータを作成して利用しているのでしょう。

今回の騒動の影響で「リモートで〇〇する」というのは需要が伸びそうな気がしていて、
配信系のアプリを作る際も「バーチャル背景」機能は必須となるかもしれません。

ですが、調べてみると案外簡単に利用できる技術やサービスがありません
(機会学習のデータは作成に手間が掛かっているので簡単には流れてくることはないでしょう)

なので

ARKit3から実装されたPeople Occlusionの機能を使って
似たような機能をつくってみました。

ARKitの機能で人物とそれ以外で分けられた画像が取得できます。
その画像がまさにMask画像なのでそのままガッツリ利用して、楽に実装していきます。

環境

Unity2019.3.7f1
iPad Pro 11 インチ (第2世代)

実装

・前提
UnityでARKitを利用する為にAR Foundationを使います。
インストールや設定などは、他にも詳しく解説しているサイトが多くあるので省きます。

Versionは4.0.0を使用しました。

・シーン作成
AR Session OriginAR Session配置し
AROcclusionManager.csのスクリプトを追加します。

AR Cameraに追加されている
ARCameraBackground.csのスクリプトは
使用しないので非アクティブにします。

表示したい背景用にImage(BackgroundImage)
RawImage(MaskRawImage)を配置しておきます


・画像の取得と割り当て
カメラ映像とマスク用の画像を取得してそれぞれを割り当てます。

マスク処理はShaderで行うので適応したMaterial
RawImage(MaskRawImage)に設定します

Manager.cs
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class Manager : MonoBehaviour
{
    public ARCameraManager cameraManager;
    public AROcclusionManager occlusionManager;

    private Texture2D camaraImage;

    public RawImage maskRawImage;
    public Material maskMaterial;

    void OnEnable()
    {
        cameraManager.frameReceived += OnCameraFrameReceived;
    }

    void OnDisable()
    {
        cameraManager.frameReceived -= OnCameraFrameReceived;
    }

    unsafe void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
    {
        XRCameraImage image;
        if (!cameraManager.TryGetLatestImage(out image))
        {
            return;
        }

        var format = TextureFormat.BGRA32;

        if (camaraImage == null || camaraImage.width != image.width || camaraImage.height != image.height)
        {
            camaraImage = new Texture2D(image.width, image.height, format, false);
        }

        var conversionParams = new XRCameraImageConversionParams(image, format, CameraImageTransformation.MirrorY);

        var rawTextureData = camaraImage.GetRawTextureData<byte>();
        try
        {
            image.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length);
        }
        finally
        {
            image.Dispose();
        }

        camaraImage.Apply();

        Texture2D humanStencil = occlusionManager.humanStencilTexture;

        maskRawImage.texture = camaraImage;
        maskMaterial.SetTexture("_MaskTex", humanStencil);

    }
}
SpriteWithMask
Shader "Custom/SpriteWithMask" {
    Properties {
        _MainTex ("Base", 2D) = "white" {}
        _MaskTex ("Mask", 2D) = "white" {}
        _Color ("Color", Color) = (0.5, 0.5, 0.5, 0.5)
    }
    SubShader {

    Cull Off

        Pass {
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

sampler2D _MainTex;
sampler2D _MaskTex;
float4 _Color;

struct v2f {
    float4 pos : SV_POSITION;
    float2 uv1 : TEXCOORD0;
    float2 uv2 : TEXCOORD1;
};

float4 _MainTex_ST;
float4 _MaskTex_ST;

v2f vert (appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos (v.vertex);
    o.uv1 = TRANSFORM_TEX (v.texcoord, _MainTex);
    o.uv2 = TRANSFORM_TEX (v.texcoord, _MaskTex);

    o.uv2.x = 1.0 - o.uv2.x;

    return o;
}

half4 frag (v2f i) : COLOR
{
    half4 base = tex2D (_MainTex, i.uv1);
    half4 mask = tex2D (_MaskTex, i.uv2);
    base.w = mask.x * mask.x * mask.x;

    return base;
}
            ENDCG
        }
    } 
    FallBack Off
}

結果

現状と今後

なかなか良い精度のものが作れましたが、ARKit限定かつ、対応機種が
まだ少ない状態なので、もう少し普及率が高まらないと採用できないのかな
と思います。

こういう時ARKitとARCoreが並走してくれないのが、もどかしいですね・・・
(どうしようもないことですが・・・)