ZED2DObjectVisualizer.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. using UnityEngine.UI;
  7. /// <summary>
  8. /// For the ZED 2D Object Detection sample.
  9. /// Listens for new object detections (via the ZEDManager.OnObjectDetection event) and moves + resizes canvas prefabs
  10. /// to represent them.
  11. /// <para>Works by instantiating a pool of prefabs, and each frame going through the DetectedFrame received from the event
  12. /// to make sure each detected object has a representative GameObject. Also disables GameObjects whose objects are no
  13. /// longer visible and returns them to the pool.</para>
  14. /// </summary>
  15. public class ZED2DObjectVisualizer : MonoBehaviour
  16. {
  17. /// <summary>
  18. /// The scene's ZEDManager.
  19. /// If you want to visualize detections from multiple ZEDs at once you will need multiple ZED3DObjectVisualizer commponents in the scene.
  20. /// </summary>
  21. [Tooltip("The scene's ZEDManager.\r\n" +
  22. "If you want to visualize detections from multiple ZEDs at once you will need multiple ZED3DObjectVisualizer commponents in the scene. ")]
  23. public ZEDManager zedManager;
  24. /// <summary>
  25. /// The scene's canvas. This will be adjusted to have required settings/components so that the bounding boxes
  26. /// will line up properly with the ZED video feed.
  27. /// </summary>
  28. [Tooltip("The scene's canvas. This will be adjusted to have required settings/components so that the bounding boxes " +
  29. "will line up properly with the ZED video feed.")]
  30. public Canvas canvas;
  31. /// <summary>
  32. /// If true, the ZED Object Detection manual will be started as soon as the ZED is initiated.
  33. /// This avoids having to press the Start Object Detection button in ZEDManager's Inspector.
  34. /// </summary>
  35. [Tooltip("If true, the ZED Object Detection manual will be started as soon as the ZED is initiated. " +
  36. "This avoids having to press the Start Object Detection button in ZEDManager's Inspector.")]
  37. public bool startObjectDetectionAutomatically = true;
  38. /// <summary>
  39. /// Prefab object that's instantiated to represent detected objects.
  40. /// This should ideally be the 2D Bounding Box prefab. But otherwise, it expects the object to have a BBox2DHandler script in the root object,
  41. /// and the RectTransform should be bottom-left-aligned (pivot set to 0, 0).
  42. /// </summary>
  43. [Space(5)]
  44. [Header("Box Appearance")]
  45. [Tooltip("Prefab object that's instantiated to represent detected objects. " +
  46. "This class expects the object to have the default Unity cube as a mesh - otherwise, it may be scaled incorrectly.\r\n" +
  47. "It also expects a BBox3DHandler component in the root object, but you won't have errors if it lacks one. ")]
  48. public GameObject boundingBoxPrefab;
  49. /// <summary>
  50. /// The colors that will be cycled through when assigning colors to new bounding boxes.
  51. /// </summary>
  52. [Tooltip("The colors that will be cycled through when assigning colors to new bounding boxes. ")]
  53. //[ColorUsage(true, true)] //Uncomment to enable HDR colors in versions of Unity that support it.
  54. public List<Color> boxColors = new List<Color>()
  55. {
  56. new Color(.231f, .909f, .69f, 1),
  57. new Color(.098f, .686f, .816f, 1),
  58. new Color(.412f, .4f, .804f, 1),
  59. new Color(1, .725f, 0f, 1),
  60. new Color(.989f, .388f, .419f, 1)
  61. };
  62. /// <summary>
  63. /// When a detected object is first given a box and assigned a color, we store it so that if the object
  64. /// disappears and appears again later, it's assigned the same color.
  65. /// This is also solvable by making the color a function of the ID number itself, but then you can get
  66. /// repeat colors under certain conditions.
  67. /// </summary>
  68. private Dictionary<int, Color> idColorDict = new Dictionary<int, Color>();
  69. /// <summary>
  70. /// If true, draws a 2D mask over where the SDK believes the detected object is.
  71. /// </summary>
  72. [Space(5)]
  73. [Header("Mask")]
  74. public bool showObjectMask = true;
  75. /// <summary>
  76. /// Used to warn the user only once if they enable the mask but the mask was not enabled when object detection was initialized. See OnValidate.
  77. /// </summary>
  78. private bool lastShowObjectMaskValue;
  79. /// <summary>
  80. /// Display bounding boxes of objects that are actively being tracked by object tracking, where valid positions are known.
  81. /// </summary>
  82. [Space(5)]
  83. [Header("Filters")]
  84. [Tooltip("Display bounding boxes of objects that are actively being tracked by object tracking, where valid positions are known. ")]
  85. public bool showONTracked = true;
  86. /// <summary>
  87. /// Display bounding boxes of objects that were actively being tracked by object tracking, but that were lost very recently.
  88. /// </summary>
  89. [Tooltip("Display bounding boxes of objects that were actively being tracked by object tracking, but that were lost very recently.")]
  90. public bool showSEARCHINGTracked = false;
  91. /// <summary>
  92. /// Display bounding boxes of objects that are visible but not actively being tracked by object tracking (usually because object tracking is disabled in ZEDManager).
  93. /// </summary>
  94. [Tooltip("Display bounding boxes of objects that are visible but not actively being tracked by object tracking (usually because object tracking is disabled in ZEDManager).")]
  95. public bool showOFFTracked = false;
  96. /// <summary>
  97. /// Used to know which of the available colors will be assigned to the next bounding box to be used.
  98. /// </summary>
  99. private int nextColorIndex = 0;
  100. /// <summary>
  101. /// Pre-instantiated bbox prefabs currently not in use.
  102. /// </summary>
  103. private Stack<GameObject> bboxPool = new Stack<GameObject>();
  104. /// <summary>
  105. /// All active RectTransforms within GameObjects that were instantiated to the prefab and that currently represent a detected object.
  106. /// Key is the object's objectID.
  107. /// </summary>
  108. private Dictionary<int, RectTransform> liveBBoxes = new Dictionary<int, RectTransform>();
  109. /// <summary>
  110. /// List of all 2D masks created in a frame. Used so that they can all be disposed of in the frame afterward.
  111. /// </summary>
  112. private List<Texture2D> lastFrameMasks = new List<Texture2D>();
  113. private void Start()
  114. {
  115. if (!zedManager)
  116. {
  117. zedManager = FindObjectOfType<ZEDManager>();
  118. }
  119. zedManager.OnObjectDetection += Visualize2DBoundingBoxes;
  120. zedManager.OnZEDReady += OnZEDReady;
  121. if (!canvas) //If we don't have a canvas in the scene, we need one.
  122. {
  123. GameObject canvasgo = new GameObject("Canvas - " + zedManager.name);
  124. canvas = canvasgo.AddComponent<Canvas>();
  125. }
  126. lastShowObjectMaskValue = showObjectMask;
  127. }
  128. private void OnZEDReady()
  129. {
  130. if (startObjectDetectionAutomatically && !zedManager.IsObjectDetectionRunning)
  131. {
  132. zedManager.StartObjectDetection();
  133. }
  134. //Enforce some specific settings on the canvas that are needed for things to line up.
  135. canvas.renderMode = RenderMode.ScreenSpaceCamera;
  136. canvas.worldCamera = zedManager.GetLeftCamera();
  137. //Canvas needs to have its plane distance set within the camera's view frustum.
  138. canvas.planeDistance = 1;
  139. CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
  140. if (!scaler)
  141. {
  142. scaler = canvas.gameObject.AddComponent<CanvasScaler>();
  143. }
  144. scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
  145. scaler.referenceResolution = new Vector2(zedManager.zedCamera.ImageWidth, zedManager.zedCamera.ImageHeight);
  146. }
  147. //TEST
  148. private void Update()
  149. {
  150. //zedManager.GetLeftCamera().ResetProjectionMatrix();
  151. }
  152. /// <summary>
  153. /// Given a frame of object detections, positions a canvas object to represent every visible object
  154. /// to encompass the object within the 2D image from the ZED.
  155. /// <para>Called from ZEDManager.OnObjectDetection each time there's a new detection frame available.</para>
  156. /// </summary>
  157. public void Visualize2DBoundingBoxes(DetectionFrame dframe)
  158. {
  159. //Clear any masks that were displayed last frame, to avoid memory leaks.
  160. DestroyLastFrameMaskTextures();
  161. //Debug.Log("Received frame with " + dframe.detectedObjects.Count + " objects.");
  162. //Get a list of all active IDs from last frame, and we'll remove each box that's visible this frame.
  163. //At the end, we'll clear the remaining boxes, as those are objects no longer visible to the ZED.
  164. List<int> activeids = liveBBoxes.Keys.ToList();
  165. List<DetectedObject> newobjects = dframe.GetFilteredObjectList(showONTracked, showSEARCHINGTracked, showOFFTracked);
  166. //Test just setting box to first available.
  167. foreach (DetectedObject dobj in newobjects)
  168. {
  169. //Remove the ID from the list we'll use to clear no-longer-visible boxes.
  170. if (activeids.Contains(dobj.id)) activeids.Remove(dobj.id);
  171. //Get the relevant box. This function will create a new one if it wasn't designated yet.
  172. RectTransform bbox = GetBBoxForObject(dobj);
  173. BBox2DHandler idtext = bbox.GetComponentInChildren<BBox2DHandler>();
  174. if (idtext)
  175. {
  176. float disttobox = Vector3.Distance(dobj.detectingZEDManager.GetLeftCameraTransform().position, dobj.Get3DWorldPosition());
  177. idtext.SetDistance(disttobox);
  178. }
  179. #if UNITY_2018_3_OR_NEWER
  180. float xmod = canvas.GetComponent<RectTransform>().rect.width / zedManager.zedCamera.ImageWidth;
  181. Rect objrect = dobj.Get2DBoundingBoxRect(xmod);
  182. #else
  183. Rect objrect = dobj.Get2DBoundingBoxRect();
  184. #endif
  185. //Adjust the size of the RectTransform to encompass the object.
  186. bbox.sizeDelta = new Vector2(objrect.width, objrect.height);
  187. bbox.anchoredPosition = new Vector2(objrect.x, objrect.y);
  188. /*
  189. #if UNITY_2018_3_OR_NEWER
  190. float xmod = canvas.GetComponent<RectTransform>().rect.width / zedManager.zedCamera.ImageWidth;
  191. bbox.anchoredPosition = new Vector2(bbox.anchoredPosition.x * xmod, bbox.anchoredPosition.y);
  192. bbox.sizeDelta *= xmod;
  193. #endif
  194. */
  195. //Apply the mask.
  196. if (showObjectMask)
  197. {
  198. //Make a new image for this new mask.
  199. Texture2D maskimage;
  200. if (dobj.GetMaskTexture(out maskimage, false))
  201. {
  202. idtext.SetMaskImage(maskimage); //Apply to 2D bbox.
  203. lastFrameMasks.Add(maskimage); //Cache the texture so it's deleted next time we update our objects.
  204. }
  205. }
  206. }
  207. //Remove boxes for objects that the ZED can no longer see.
  208. foreach (int id in activeids)
  209. {
  210. ReturnBoxToPool(id, liveBBoxes[id]);
  211. }
  212. SortActiveObjectsByDepth(); //Sort all object transforms so that ones with further depth appear behind objects that are closer.
  213. }
  214. /// <summary>
  215. /// Returs the RectTransform within the GameObject (instantiated from boundingBoxPrefab) that represents the provided DetectedObject.
  216. /// If none exists, it retrieves one from the pool (or instantiates a new one if none is available) and
  217. /// sets it up with the proper ID and colors.
  218. /// </summary>
  219. private RectTransform GetBBoxForObject(DetectedObject dobj)
  220. {
  221. if (!liveBBoxes.ContainsKey(dobj.id))
  222. {
  223. GameObject newbox = GetAvailableBBox();
  224. newbox.transform.SetParent(canvas.transform, false);
  225. newbox.name = "Object #" + dobj.id;
  226. Color col;
  227. if (idColorDict.ContainsKey(dobj.id))
  228. {
  229. col = idColorDict[dobj.id];
  230. }
  231. else
  232. {
  233. col = GetNextColor();
  234. idColorDict.Add(dobj.id, col);
  235. }
  236. BBox2DHandler boxhandler = newbox.GetComponent<BBox2DHandler>();
  237. if (boxhandler)
  238. {
  239. boxhandler.SetColor(col);
  240. boxhandler.SetID(dobj.id);
  241. }
  242. RectTransform newrecttrans = newbox.GetComponent<RectTransform>();
  243. if (!newrecttrans)
  244. {
  245. Debug.LogError("BBox prefab needs a RectTransform in the root object.");
  246. return null;
  247. }
  248. liveBBoxes[dobj.id] = newrecttrans;
  249. return newrecttrans;
  250. }
  251. else return liveBBoxes[dobj.id];
  252. }
  253. /// <summary>
  254. /// Gets an available GameObject (instantiated from boundingBoxPrefab) from the pool,
  255. /// or instantiates a new one if none are available.
  256. /// </summary>
  257. /// <returns></returns>
  258. private GameObject GetAvailableBBox()
  259. {
  260. if (bboxPool.Count == 0)
  261. {
  262. GameObject newbbox = Instantiate(boundingBoxPrefab);
  263. newbbox.transform.SetParent(transform, false);
  264. bboxPool.Push(newbbox);
  265. }
  266. GameObject bbox = bboxPool.Pop();
  267. bbox.SetActive(true);
  268. return bbox;
  269. }
  270. /// <summary>
  271. /// Disables a RectTransform's GameObject that was being used to represent an object (of the given id) and
  272. /// puts it back into the pool for later use.
  273. /// </summary>
  274. private void ReturnBoxToPool(int id, RectTransform bbox)
  275. {
  276. bbox.gameObject.SetActive(false);
  277. bbox.name = "Unused";
  278. bboxPool.Push(bbox.gameObject);
  279. if (liveBBoxes.ContainsKey(id))
  280. {
  281. liveBBoxes.Remove(id);
  282. }
  283. else
  284. {
  285. Debug.LogError("Tried to remove object ID " + id + " from active bboxes, but it wasn't in the dictionary.");
  286. }
  287. }
  288. /// <summary>
  289. /// Returns a color from the boxColors list.
  290. /// Colors are returned sequentially in order of their appearance in that list.
  291. /// </summary>
  292. /// <returns></returns>
  293. private Color GetNextColor()
  294. {
  295. if (boxColors.Count == 0)
  296. {
  297. return new Color(.043f, .808f, .435f, 1);
  298. }
  299. if (nextColorIndex >= boxColors.Count)
  300. {
  301. nextColorIndex = 0;
  302. }
  303. Color returncol = boxColors[nextColorIndex];
  304. nextColorIndex++;
  305. return returncol;
  306. }
  307. /// <summary>
  308. /// Sorts all objects in the canvas based on their distance from the camera, so that closer objects overlap further objects.
  309. /// </summary>
  310. private void SortActiveObjectsByDepth()
  311. {
  312. List<BBox2DHandler> handlers = new List<BBox2DHandler>();
  313. foreach (Transform child in canvas.transform)
  314. {
  315. BBox2DHandler handler = child.GetComponent<BBox2DHandler>();
  316. if (handler) handlers.Add(handler);
  317. }
  318. handlers.Sort((x, y) => y.currentDistance.CompareTo(x.currentDistance));
  319. for (int i = 0; i < handlers.Count; i++)
  320. {
  321. handlers[i].transform.SetSiblingIndex(i);
  322. }
  323. }
  324. /// <summary>
  325. /// Destroys all textures added to the lastFrameMasks the last time Object Detection was called.
  326. /// Called when we're done using them (before updating with new data) to avoid memory leaks.
  327. /// </summary>
  328. private void DestroyLastFrameMaskTextures()
  329. {
  330. if (lastFrameMasks.Count > 0)
  331. {
  332. for (int i = 0; i < lastFrameMasks.Count; i++)
  333. {
  334. Destroy(lastFrameMasks[i]);
  335. }
  336. lastFrameMasks.Clear();
  337. }
  338. }
  339. private void OnValidate()
  340. {
  341. //If the user changes the showObjectMask setting to true, warn them if its ZEDManager has objectDetection2DMask set to false, because masks won't show up.
  342. if (Application.isPlaying && showObjectMask != lastShowObjectMaskValue)
  343. {
  344. lastShowObjectMaskValue = showObjectMask;
  345. if (!zedManager) zedManager = ZEDManager.GetInstance(sl.ZED_CAMERA_ID.CAMERA_ID_01);
  346. if(showObjectMask == true && zedManager != null && zedManager.objectDetection2DMask == false)
  347. {
  348. Debug.LogError("ZED2DObjectVisualizer has showObjectMask enabled, but its ZEDManager has objectDetection2DMask disabled. " +
  349. "objectDetection2DMask must be enabled when Object Detection is started or masks will not be visible.");
  350. }
  351. }
  352. }
  353. private void OnDestroy()
  354. {
  355. if (zedManager)
  356. {
  357. zedManager.OnObjectDetection -= Visualize2DBoundingBoxes;
  358. zedManager.OnZEDReady -= OnZEDReady;
  359. }
  360. DestroyLastFrameMaskTextures();
  361. }
  362. }