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

# 今日この頃
新型コロナウイルスの影響で、リモート会議やリモート飲み
などが盛ん（？）に行われていると思います。

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

<img width="500" alt="スクリーンショット.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/5a059146-e25b-2f2f-a302-5212cc62722a.png">

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

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

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

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

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

<img width="300" alt="スクリーンショット 2020-04-27 15.21.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/f93bfba3-fd51-4624-e2ce-81f965931930.png">
# 環境
Unity2019.3.7f1
iPad Pro 11 インチ (第2世代)

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

Versionは4.0.0を使用しました。
<img width="694" alt="スクリーンショット 2020-04-27 15.29.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/222e2b33-0647-244a-822e-fe5be56de3a8.png">

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

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

表示したい背景用に`Image（BackgroundImage）`と
`RawImage（MaskRawImage）`を配置しておきます
<img width="351" alt="スクリーンショット 2020-04-27 15.42.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/bc75a33b-cb1f-45f2-b1ee-e04d97b07b78.png">
<img width="677" alt="スクリーンショット 2020-04-27 16.19.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/0edabab6-afd9-9f56-7f1e-b0f0246d3679.png">
<img width="676" alt="スクリーンショット 2020-04-27 16.19.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/48acad24-0d48-6083-35a1-2e6889b171ab.png">


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

マスク処理は`Shader`で行うので適応した`Material`を
`RawImage（MaskRawImage）`に設定します
<img width="678" alt="スクリーンショット 2020-04-27 16.21.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/4cc98ab1-b982-ea68-de97-2b27ae160f6f.png">

```言語: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
}
```

# 結果
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114276/5dee30cc-e53d-9469-3383-7880b66e11c4.gif)

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

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