【ARDK】Semantic Confidenceでセグメントの境界線をぼかす

## はじめに
以前、ARDKのSemantic Segmentationを利用して巨大なオブジェクトにオクルージョン処理をかける記事を書きました。

【参考リンク】：[【ARDK】Semantic Segmentationで巨大なオブジェクトにオクルージョン処理を行う際の注意点](https://qiita.com/OKsaiyowa/items/884b318fd776c332a548)

その発展として、新機能であるSemantic Confidenceを利用したデモを作成しました。experimentalということもあり、かなり遠回りな実装が必要だったのでメモします。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ARDKの新機能調査中です👍<br><br>セグメンテーションの境界線をぼやけさせる処理を試しました😆<a href="https://twitter.com/hashtag/ARDK?src=hash&amp;ref_src=twsrc%5Etfw">#ARDK</a> <a href="https://t.co/JUG9fgi4Dq">pic.twitter.com/JUG9fgi4Dq</a></p>&mdash; KENTO⚽️XRエンジニア😎Shader100記事マラソン挑戦中82/100 (@kento_xr) <a href="https://twitter.com/kento_xr/status/1596002740274008065?ref_src=twsrc%5Etfw">November 25, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## バージョン情報
ARDK 2.3.0

## ARDK
[ARDK](https://lightship.dev/)はNianticが提供するARライブラリです。

リアルワールドメタバースを目指しているだけあって、
ARの実装時に活躍する様々な機能が用意されています。

## Semantic Confidence
ARDKにExperimentalとして追加された新機能です。Semantic Segmentationの拡張機能という立ち位置で、文字通りピクセルの認識における信頼度を利用可能になります。今まで0か1かで識別していたセグメントを「おそらくこの領域は80パーセントの確立で『空』であろう」といった具合で取得できるということです。

もっと簡潔にいうとセグメントをグラデーションで取得可能になるということです。公式ドキュメント記載の以下GIFがわかりやすいです。中央が従来のSemantic segmentationで、右側がSemantic Confidenceを利用した結果です。境界がぼやけているのが見て取れます。
![semantic_confidence.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/9889d9c3-5c55-2e82-250f-5e69e1ad58bc.gif)

【引用元】：[Semantic Confidence](https://lightship.dev/docs/ardk/experimental/semantic_confidence.html#semantic-confidence)

## 実装手法
Semantic Confidenceでできることはわかりましたが、それをどうオクルージョン処理に落とし込むかについてはわかりませんでした。
そこで公式フォーラムに`I'm Stuck`として質問を投げた結果以下のような回答をいただきました。

>At the moment Semantic confidence is used for shader effects only as it is experimental.
>The way the giant yeti in the sky video was made was to render it to a texture and blend that into the scene with the confidence mask.
>e.g.
Add the objects you want to draw to a layer (sky_layer),
Hide it from the main camera.
Add a second camera that renders to texture and set it to only render that layer.
>Then in your blit pass you can send in the texture with your cg, and the confidence mask.
In the shader you can then composite the items together.

【引用元】：[How can I use semantic confidence for occlusion?](https://community.lightship.dev/t/how-can-i-use-semantic-confidence-for-occlusion/2770/3?u=kento)

簡単にまとめると以下です。
- ARオブジェクト専用のカメラとそれ以外を描画するカメラ(MainCameraとする)を用意する。
-  ARオブジェクト専用カメラのレンダリング結果をShaderに渡してSemantic ConfidenceのTextureをかけ合わせる。
- その結果をMainCameraのレンダリング結果にBlitする。

少しわかりずらいので処理を1枚ずつ画像にわけて解説していきます。UnityEditor上でのシミュレート結果を用いて説明するのでわかりにくいですが、Humanoidのオブジェクト以外は現実空間の物体である想定で話を進めていきます。

上記想定での最終的な描画結果は以下画像の通りです。木がHumanoidのオブジェクトより全面に表示されているのがわかります。この"木でオクルージョンされた状態"をつくっていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/b061727c-6763-1fc6-9e37-d85e3e7304a9.png)

まず、MainCameraの描画は以下です。Humanoidのオブジェクト以外を描画するように設定してあります。
![SC1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/936a91df-b652-ef80-2c16-f1f97712ef10.png)


次にSemantic Confidenceの実行結果のTexture取得です。

実際にShaderに流し込んで白黒表示した画像が以下です。今回は空である可能性が高いピクセルほど0に近い値を返すようにして適用しています。
(以下画像について、正確にはSemantic Segmentationのシミュレート結果です。Semantic ConfidenceをUnityEditor上でシミュレートする機能はないのでSemantic Confidenceをシミュレートした結果であると仮定して話を進めます)
![SC2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/61b73bd0-9228-9e4c-3f6d-f0f41d92d7cd.png)

残すはARオブジェクト専用のカメラの描画結果です。この描画結果はRenderTextureとして利用します。
![SC3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/cf588a71-6273-2397-c82e-a8f880343606.png)

ここまでで、必要な描画処理はそろいました。あとはこれらをかけあわせていきます。
Semantic Confidenceの実行結果のTextureとARオブジェクト専用のカメラの描画結果をShaderでかけ合わせものが以下になります。赤い丸で囲った部分に注目します。木の描画領域がきれいに切り抜かれています。
![SC4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/ca6d38ce-f935-ecd2-0146-567d4cabc9ba.png)

あとはこの描画結果をMainCameraにBlitしてあげれば完成です。
![SC5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/f53ceee1-572a-be9d-43db-2b986b79f35d.png)


前回のSemantic Segmentationと異なる点として、DepthによるMask処理は行っていないことが挙げられます。

## コード

ARDKのサンプルに含まれるSemanticSegmentationExampleManager.csに追記します。
変更点としては`//追記`とコメントした3か所のみです。

```CS

using Niantic.ARDK.AR;
using Niantic.ARDK.AR.Awareness;
using Niantic.ARDK.AR.Awareness.Semantics;
using Niantic.ARDK.Extensions;
using Niantic.ARDK.Rendering;
using UnityEngine;
using UnityEngine.UI;

namespace Niantic.ARDKExamples
{
    public class SemanticSegmentationExampleManager :
        MonoBehaviour
    {
        [SerializeField] private ARSemanticSegmentationManager _semanticSegmentationManager;

        [SerializeField] private Material _overlayMaterial;
        
        // 追記
        // "ARオブジェクトだけ描画したカメラに設定したRenderTexture"を利用する
        [SerializeField] private RenderTexture _renderTexture;

        [Header("UI")] [SerializeField] private GameObject _togglesParent;

        [SerializeField] private Text _toggleFeaturesButtonText;

        [SerializeField] private Text _toggleInterpolationText;

        [SerializeField] private Text _channelNameText;

        [SerializeField] private Text _selectedModeText;

        private Texture2D _semanticTexture;
        private bool _useThresholdedSemantics = true;
        private bool _semanticsUpdated = false;

        // All channels available in the model will be stored here. 
        private string[] _channels;

        // The index of the currently displayed semantics channel in the array
        private int _featureChannel;

        private void Start()
        {
            // 追記
            // カメラの描画サイズに合わせる
            _renderTexture.Release();
            _renderTexture.width = Screen.width;
            _renderTexture.height = Screen.height; 
            
            // Disable the UI while contextual awareness is initializing
            if (_togglesParent != null)
                _togglesParent.SetActive(false);

            // Enable the UI when the semantic segmentation stream starts
            _semanticSegmentationManager.SemanticBufferInitialized += EnableUserInterface;
            _semanticSegmentationManager.SemanticBufferUpdated += OnSemanticBufferUpdated;
        }

        private void OnSemanticBufferUpdated(ContextAwarenessStreamUpdatedArgs<ISemanticBuffer> args)
        {
            if (!args.IsKeyFrame)
            {
                return;
            }

            // Get the name of the observed channel
            var currentChannelName = _channels[_featureChannel];

            if (_useThresholdedSemantics)
            {
                // Update the texture using thresholded semantics
                _semanticsUpdated = _semanticSegmentationManager.GetThresholdedARGB32
                (
                    ref _semanticTexture,
                    currentChannelName
                );
            }
            else
            {
                // Update the texture using confidence values
                _semanticsUpdated = _semanticSegmentationManager.GetConfidencesARGB32
                (
                    ref _semanticTexture,
                    currentChannelName
                );
            }
        }

        // We use this callback to overlay semantics on the rendered background
        private void OnRenderImage(RenderTexture src, RenderTexture dest)
        {
            // 追記
            Graphics.Blit(src, dest);
            
            if (!_semanticSegmentationManager.enabled || !_semanticsUpdated)
            {
                Graphics.Blit(src, dest);
                return;
            }

            // Get the transformation to correctly map the texture to the viewport
            var sampler = _semanticSegmentationManager.SemanticBufferProcessor.SamplerTransform;

            // Update the transform
            _overlayMaterial.SetMatrix(PropertyBindings.SemanticsTransform, sampler);
            
            // Update the texture
            _overlayMaterial.SetTexture(PropertyBindings.SemanticChannel, _semanticTexture);

            // Display semantics
            Graphics.Blit(src, dest, _overlayMaterial);
        }

        private void OnDestroy()
        {
            // Release semantic overlay texture
            if (_semanticTexture != null)
                Destroy(_semanticTexture);

            _semanticSegmentationManager.SemanticBufferUpdated -= OnSemanticBufferUpdated;
        }

        private void EnableUserInterface(ContextAwarenessArgs<ISemanticBuffer> args)
        {
            _channels = _semanticSegmentationManager.SemanticBufferProcessor.Channels;

            _semanticSegmentationManager.SemanticBufferInitialized -= EnableUserInterface;

            if (_togglesParent != null)
                _togglesParent.SetActive(true);

            _channelNameText.text = _channels[_featureChannel];

            // Tell the manager for which channels the semantic confidence is needed.
            _semanticSegmentationManager.SetConfidenceChannels(_channels[_featureChannel]);
        }

        public void ChangeFeatureChannel()
        {
            // Increment the channel count with wraparound.
            _featureChannel += 1;
            if (_featureChannel == _channels.Length)
                _featureChannel = 0;

            // Tell the manager for which channels the semantic confidence is needed.
            _semanticSegmentationManager.SetConfidenceChannels(_channels[_featureChannel]);
            _channelNameText.text = _channels[_featureChannel];
        }

        public void ToggleSessionSemanticFeatures()
        {
            var newEnabledState = !_semanticSegmentationManager.enabled;
            _toggleFeaturesButtonText.text = newEnabledState ? "Disable Features" : "Enable Features";
            _semanticSegmentationManager.enabled = newEnabledState;
        }

        public void ToggleInterpolation()
        {
            var provider = _semanticSegmentationManager.SemanticBufferProcessor;
            var current = provider.InterpolationMode;
            provider.InterpolationMode = current == InterpolationMode.None
                ? InterpolationMode.Smooth
                : InterpolationMode.None;

            _toggleInterpolationText.text =
                provider.InterpolationMode != InterpolationMode.None
                    ? "Disable Interpolation"
                    : "Enable Interpolation";
        }

        public void ToggleBinaryAndConfidence()
        {
            _useThresholdedSemantics = !_useThresholdedSemantics;

            _selectedModeText.text = _useThresholdedSemantics
                ? "Mode:\nThresholded"
                : "Mode:\nConfidence";
        }
    }
}
```

Scriptで特筆すべきポイントは特にありません。ほとんどがサンプル内ですでに用意されています。
OnRenderImage内で`Graphics.Blit(src, dest);`を呼んでいる理由は一度レンダリング結果を更新しておかないと前フレームの描画結果を取りまわしてしまうからです。記述しない場合、以下GIFのような挙動になります。
![BlitBug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/545075b7-d09a-dab1-5309-85174c6870e1.gif)

Shader側は以下です。

```glsl
Shader "Custom/SemanticOverlay"
{
    Properties
    {
        // "ARオブジェクトだけ描画したカメラに設定したRenderTexture"を利用する
        _OverlayTex ("Overlay Texture", 2D) = "" {}
    }

    SubShader
    {
        Tags
        {
            "RenderType"="Transparent"
        }

        Lighting Off
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
        ZTest Always
        Fog{ Mode Off }

        Pass
        {

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest

            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 color_uv : TEXCOORD0;
                float3 semantic_uv : TEXCOORD1;
            };
            
            sampler2D _OverlayTex;
            sampler2D _textureSemantic;

            float4x4 _semanticsTransform;

            v2f vert(appdata_t v)
            {
                v2f o;

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.color_uv = v.uv;
                o.semantic_uv = mul(_semanticsTransform, float4(v.uv, 1.0f, 1.0f)).xyz;

                return o;
            }

            float4 frag(v2f i) : COLOR
            {
                //RenderTextureサンプリング
                float4 overlayColor = tex2D(_OverlayTex, i.color_uv);

                //UV計算
                float2 semanticUV = float2(i.semantic_uv.x / i.semantic_uv.z, i.semantic_uv.y / i.semantic_uv.z);

                //SemanticTexture取得                 
                float3 semColor = tex2D(_textureSemantic, semanticUV).rgb;

                //Semantic Confidenceの値とRenderTextureをかけあわせる
                float interpolation = saturate(semColor.r);
                float4 texColor = overlayColor * interpolation;

                return texColor;
            }
            ENDCG
        }
    }

    Fallback off
}

```

`_textureSemantic`には`SemanticSegmentationExampleManager.cs`のOnRenderImage内で渡したSemantic ConfidenceのTextureが入ります。あとはその描画結果のうち、r成分だけ取り出してRenderTextureのサンプリング結果とかけあわせています。

先ほどの例でいうと以下画像がこのShaderの結果で得られます。
![SC4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/fe4be466-1256-b052-61b4-2fcfe49e5736.png)

r成分だけ利用するという観点では、[GetConfidencesRFloat](https://lightship.dev/docs/ardk/api-documentation/class_Niantic_ARDK_Extensions_ARSemanticSegmentationManager.html#doxid-ardk-class-niantic-1-1-a-r-d-k-1-1-extensions-1-1-a-r-semantic-segmentation-manager-a091227f569a0ac33024b6c7e5b1accf3-1a091227f569a0ac33024b6c7e5b1accf3)という関数もあるようです。
(今回のサンプルで適用したところ、従来のSemantic Segmentationと交互に機能を切り替えての検証時にエラーを吐いて切り替えた先の機能が動作しなかったため使用しませんでした)

## おわりに
まだExperimentalな機能ですが、本実装として提供されるのが楽しみな機能でした。