AR Foundation Multiple Image Tracking - How to detect one at a time

Hello, I am using AR foundation 4.2.7 with Unity 2021.3.7f1 for Image tracking.
I have added Prefab Image Pair Manager to the AR session origin to detect multiple images with different prefabs. When I run the app, the paired prefab will spawn accordingly for each image. The problem for my case is that once an image is detected, the spawned prefab will still stay in the scene. I would like to track one image at a time instead of having multiple prefabs all at once. So when a new image is detected, the previous spawned prefab should be removed in the scene. Could I get help on what to adjust to make it work like this? Thank you.

Whenever a prefab is spawned just destroy the previously placed one?
What did you already try? This doesn't sound too complicated

For now I have a button to reset the ARsession to clear the scene. Could you guide me where in the script I should look into and what I should add? It will be super helpful to understand. Thank you for the quick reply.

Easiest is to have 1 singleton manager class with a list of gameobjects) (or just 1 gameobject if that's all ya need).
Then every time you spawn an object add it to the list via a method (maybe in the Start() of a script on the prefab).
Then when you call the method to add destroy the old gameobject and add the new one to the list instead.

If just one, simply find object by tag and destoy in start (and don't destroy the object itself haha). Not the cleanest but gets the job done

Thank you for the reply. Instead, I modified the Prefab Image Pair Manager script and now the app can detect one prefab at a time. But now the issue is, when I want to scan something again, the prefab will not show anymore. So this works for detecting prefabs only once. What should I adjust to keep the ability to view the prefabs no matter if it is the first time or not? For example, I have three images and corresponding prefabs. I can scan each image and see one prefab at a time, but once I try to scan any of the three images again, it wont show anymore. Here is the part that I adjusted.

    void AssignPrefab(ARTrackedImage trackedImage)
        {

                //destroy previous prefab if there was one

                if (m_CurrentPrefab != null)
                {
                    Destroy(m_CurrentPrefab);
                    m_CurrentPrefab = null;
                }

                //initiate the new prefab
                if (m_PrefabsDictionary.TryGetValue(trackedImage.referenceImage.guid, out var prefab))
                    //   m_Instantiated[trackedImage.referenceImage.guid] = Instantiate(prefab, trackedImage.transform);
                    m_CurrentPrefab = Instantiate(prefab, trackedImage.transform);



        }

and the full modified Prefab Image Pair Manager script:

using System;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARSubsystems;
using UnityEngine.XR.ARFoundation;

namespace UnityEngine.XR.ARFoundation.Samples
{
    /// <summary>
    /// This component listens for images detected by the <c>XRImageTrackingSubsystem</c>
    /// and overlays some prefabs on top of the detected image.
    /// </summary>
    [RequireComponent(typeof(ARTrackedImageManager))]
    public class PrefabImagePairManager : MonoBehaviour, ISerializationCallbackReceiver
    {
        /// <summary>
        /// Used to associate an `XRReferenceImage` with a Prefab by using the `XRReferenceImage`'s guid as a unique identifier for a particular reference image.
        /// </summary>

        private GameObject m_CurrentPrefab;

        [Serializable]
        struct NamedPrefab
        {
            // System.Guid isn't serializable, so we store the Guid as a string. At runtime, this is converted back to a System.Guid
            public string imageGuid;
            public GameObject imagePrefab;



            public NamedPrefab(Guid guid, GameObject prefab)
            {
                imageGuid = guid.ToString();
                imagePrefab = prefab;
            }
        }

        [SerializeField]
        [HideInInspector]
        List<NamedPrefab> m_PrefabsList = new List<NamedPrefab>();

        Dictionary<Guid, GameObject> m_PrefabsDictionary = new Dictionary<Guid, GameObject>();
        Dictionary<Guid, GameObject> m_Instantiated = new Dictionary<Guid, GameObject>();
        ARTrackedImageManager m_TrackedImageManager;

        [SerializeField]
        [Tooltip("Reference Image Library")]
        XRReferenceImageLibrary m_ImageLibrary;

        /// <summary>
        /// Get the <c>XRReferenceImageLibrary</c>
        /// </summary>
        public XRReferenceImageLibrary imageLibrary
        {
            get => m_ImageLibrary;
            set => m_ImageLibrary = value;
        }

        public void OnBeforeSerialize()
        {
            m_PrefabsList.Clear();
            foreach (var kvp in m_PrefabsDictionary)
            {
                m_PrefabsList.Add(new NamedPrefab(kvp.Key, kvp.Value));
            }
        }

        public void OnAfterDeserialize()
        {
            m_PrefabsDictionary = new Dictionary<Guid, GameObject>();
            foreach (var entry in m_PrefabsList)
            {
                m_PrefabsDictionary.Add(Guid.Parse(entry.imageGuid), entry.imagePrefab);
            }
        }

        void Awake()
        {
            m_TrackedImageManager = GetComponent<ARTrackedImageManager>();
        }

        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);

            }
        }

        void AssignPrefab(ARTrackedImage trackedImage)
        {

                //destroy previous prefab if there was one

                if (m_CurrentPrefab != null)
                {
                    Destroy(m_CurrentPrefab);
                    m_CurrentPrefab = null;
                }

                //initiate the new prefab
                if (m_PrefabsDictionary.TryGetValue(trackedImage.referenceImage.guid, out var prefab))
                    //   m_Instantiated[trackedImage.referenceImage.guid] = Instantiate(prefab, trackedImage.transform);
                    m_CurrentPrefab = Instantiate(prefab, trackedImage.transform);



        }

        public GameObject GetPrefabForReferenceImage(XRReferenceImage referenceImage)
            => m_PrefabsDictionary.TryGetValue(referenceImage.guid, out var prefab) ? prefab : null;

        public void SetPrefabForReferenceImage(XRReferenceImage referenceImage, GameObject alternativePrefab)
        {
            m_PrefabsDictionary[referenceImage.guid] = alternativePrefab;
            if (m_Instantiated.TryGetValue(referenceImage.guid, out var instantiatedPrefab))
            {
                m_Instantiated[referenceImage.guid] = Instantiate(alternativePrefab, instantiatedPrefab.transform.parent);
                Destroy(instantiatedPrefab);
            }
        }

#if UNITY_EDITOR
        /// <summary>
        /// This customizes the inspector component and updates the prefab list when
        /// the reference image library is changed.
        /// </summary>
        [CustomEditor(typeof(PrefabImagePairManager))]
        class PrefabImagePairManagerInspector : Editor
        {
            List<XRReferenceImage> m_ReferenceImages = new List<XRReferenceImage>();
            bool m_IsExpanded = true;

            bool HasLibraryChanged(XRReferenceImageLibrary library)
            {
                if (library == null)
                    return m_ReferenceImages.Count == 0;

                if (m_ReferenceImages.Count != library.count)
                    return true;

                for (int i = 0; i < library.count; i++)
                {
                    if (m_ReferenceImages[i] != library[i])
                        return true;
                }

                return false;
            }

            public override void OnInspectorGUI()
            {
                //customized inspector
                var behaviour = serializedObject.targetObject as PrefabImagePairManager;

                serializedObject.Update();
                using (new EditorGUI.DisabledScope(true))
                {
                    EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Script"));
                }

                var libraryProperty = serializedObject.FindProperty(nameof(m_ImageLibrary));
                EditorGUILayout.PropertyField(libraryProperty);
                var library = libraryProperty.objectReferenceValue as XRReferenceImageLibrary;

                //check library changes
                if (HasLibraryChanged(library))
                {
                    if (library)
                    {
                        var tempDictionary = new Dictionary<Guid, GameObject>();
                        foreach (var referenceImage in library)
                        {
                            tempDictionary.Add(referenceImage.guid, behaviour.GetPrefabForReferenceImage(referenceImage));
                        }
                        behaviour.m_PrefabsDictionary = tempDictionary;
                    }
                }

                // update current
                m_ReferenceImages.Clear();
                if (library)
                {
                    foreach (var referenceImage in library)
                    {
                        m_ReferenceImages.Add(referenceImage);
                    }
                }

                //show prefab list
                m_IsExpanded = EditorGUILayout.Foldout(m_IsExpanded, "Prefab List");
                if (m_IsExpanded)
                {
                    using (new EditorGUI.IndentLevelScope())
                    {
                        EditorGUI.BeginChangeCheck();

                        var tempDictionary = new Dictionary<Guid, GameObject>();
                        foreach (var image in library)
                        {
                            var prefab = (GameObject)EditorGUILayout.ObjectField(image.name, behaviour.m_PrefabsDictionary[image.guid], typeof(GameObject), false);
                            tempDictionary.Add(image.guid, prefab);
                        }

                        if (EditorGUI.EndChangeCheck())
                        {
                            Undo.RecordObject(target, "Update Prefab");
                            behaviour.m_PrefabsDictionary = tempDictionary;
                            EditorUtility.SetDirty(target);
                        }
                    }
                }

                serializedObject.ApplyModifiedProperties();
            }
        }
#endif
    }
}
2 Likes

I found what I was missing - 'OnTrackedImagesChanged' method was not handling the 'updated' case.
Below is what I added if anyone wants to modify as I did.

 void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
        {

            foreach (var trackedImage in eventArgs.added)
            {
                AssignPrefab(trackedImage);
            }

            foreach (var trackedImage in eventArgs.updated)
            {
                // If the updated image is currently being tracked, assign its prefab
                if (trackedImage.trackingState == TrackingState.Tracking)
                {
                    AssignPrefab(trackedImage);
                }
            }


        }
3 Likes

Hey! I'm trying to get something similar working. I'm a complete newbie at Unity and C# so trying to add this script, but keep getting an error "PrefabImagePairManager already defines a member oninspectorGUI with the same parameter types". Could you pleae help me out? Thanks in advance!

@SAKDT you are awesome. I have watched the entire youtube before finding this.
My only issue is that the Video Players attached to the prefabs are now not playing nor poping up.
Any idea how could one solve that..?

Update:
I figured out that when we handle the updated tracking state (in comment #6) we keep destroying the prefab and that is why the video player doesn't work.
I'll try to create a separate function for tracking state that only updates the position.