ARFoundationのImageTrackingにて画像検出時と外れる時 (Removed) のタイミングを取得する方法

こんにちは、ARエンジニアのイワケンです。
ARFoundationのImageTracking機能を使う際に詰まったところと解決方法を共有します。

# 環境

- Unity2020.2.1f1
- ARFoundation4.1.7
- macOS BigSur 11.5.2
- iPhone 11Pro
- iOS14.7.1

ARFoundationのサンプルとして、ブランチ4.1の
https://github.com/Unity-Technologies/arfoundation-samples/tree/4.1
を使用しました。

# やりたいこと

ImageTrackingの画像認識にて、画像検出時と画像認識が外れる時のタイミングを取得したい。

# サンプルシーンを動かして挫折

Githubのサンプルの
Assets/Scenes/ImageTracking/ImageTrackingWithMultiplePrefabs/ImageTrackingWithMultiplePrefabs.sceneを開く。
このシーンは、複数の画像マーカーに対して、対応するPrefabを生成するサンプルシーンになります。
Image Trackingではよくあるユースケースでしょう。

## PrefabImagePairManager.csの実装を見る

```csharp:PrefabImagePairManager.cs

        void OnEnable()
        {
            m_TrackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
        }

        void OnDisable()
        {
            m_TrackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
        }

        // この関数がポイント
        void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
        {
            foreach (var trackedImage in eventArgs.added)
            {
                // Give the initial image a reasonable default scale
                var minLocalScalar = Mathf.Min(trackedImage.size.x, trackedImage.size.y) / 2;
                trackedImage.transform.localScale = new Vector3(minLocalScalar, minLocalScalar, minLocalScalar);
                AssignPrefab(trackedImage);
            }
        }

```

画像のトラッキング状況が変化したら、OnTrackedImagesChangedが呼ばれるようになっています。

ここで、

```csharp:PrefabImagePairManager.cs
foreach (var trackedImage in eventArgs.added){
    AssignPrefab(trackedImage);
}
```

と書かれているのですが、私が実機Debugで確認したところ、ある画像に対して**初めて**画像認識したときに呼ばれます。

例えば、画像A,Bを画像マーカーとして登録したときに

① 画像Aを初めて認識
② 画像Aをカメラから外す
③ 画像Bを初めての認識
④ 画像Bをカメラから外す
⑤ 画像Aを再び認識

という順番の動作をしたときに、①③のときしか認識しません。実装上では、⑤のパターンもタイミング取得したいところです。
さらに、②④の画像認識から外れたタイミングも取得したい。

そこで調べると、

```csharp:PrefabImagePairManager.cs
foreach (var trackedImage in eventArgs.removed){ //removedに変わっているよ
    AssignPrefab(trackedImage);
}
```
といういかにもな書き方もあるのですが、

https://github.com/Unity-Technologies/arfoundation-samples/issues/365

にあるとおり、eventArgs.removedは決して呼ばれることがないそうです。(困った!)

# 結論: eventArgs.updatedとtrackedImage.trackingStateを活用する

[issue](https://github.com/Unity-Technologies/arfoundation-samples/issues/365)によると、**TrackingState.Limitedになるときが離れた瞬間らしい！** ということで以下のように書く。

```csharp:PrefabImagePairManager.cs
    void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
            // 初めて認識した時
            foreach (var trackedImage in eventArgs.added)
            {
                OnTrackedEnter(trackedImage);
            }

            // trackingしている && addedでない
            foreach (var trackedImage in eventArgs.updated) 
            {
                // 離れた瞬間
                if (trackedImage.trackingState == TrackingState.Limited)
                {
                    OnTrackedExit(trackedImage); //後で定義
                }
                // trackedImageが前フレームで離れている状態かどうか
                else if (IsRemovedState(trackedImage)) //後で定義
                {
                    OnTrackedEnter(trackedImage); //後で定義
                }
                else {
                    OnTracedStay(trackedImage); //後で定義
                }
            }
            // 呼ばれることはないから書かない
            //foreach (var trackedImage in eventArgs.removed) 
            //{
            //}
    }

```
あとは、認識した画像をGuidで識別して、前フレームにtracking状態だったか、離れている状態か追っていけば良いです。

例えば、以下のように画像検出時と外れる時のタイミングを取得することができます。(ここの書き方は自由だと思います)

```csharp:PrefabImagePairManager.cs
Dictionary<Guid, bool> m_Removed = new Dictionary<Guid, bool>();

// ==中略==

// trackingし始めた瞬間
void OnTrackedEnter(ARTrackedImage trackedImage)
{
    SetRemovedState(trackedImage, false);
}

// trackingの認識から離れた瞬間
void OnTrackedExit(ARTrackedImage trackedImage)
{
    SetRemovedState(trackedImage, true);
}

bool IsRemovedState(ARTrackedImage trackedImage) {
    if(m_Removed.TryGetValue(trackedImage.referenceImage.guid, out bool isRemoved)){
        return isRemoved;
    }
    return false;
}

void SetRemovedState(ARTrackedImage trackedImage, bool isRemoved)
{
    m_Removed[trackedImage.referenceImage.guid] = isRemoved;
}

```

これで、画像認識時とはずれる時のタイミングを取得することができました！