ZEDArUcoDetectionManager.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #if ZED_OPENCV_FOR_UNITY
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using OpenCVForUnity.CoreModule;
  6. using OpenCVForUnity.ArucoModule;
  7. using OpenCVForUnity.UnityUtils;
  8. using OpenCVForUnity.Calib3dModule;
  9. /// <summary>
  10. /// Whenever the ZED grabs/captures an image, uses OpenCV to detect ArUCO markers, calculates their
  11. /// world positions and rotations, and uses them to call the appropriate functions on any MarkerObject components in the scene.
  12. /// </summary>
  13. public class ZEDArUcoDetectionManager : MonoBehaviour
  14. {
  15. /// <summary>
  16. /// Scene's ZEDToOpenCVRetriever, which creates OpenCV mats and deploys events each time the ZED grabs an image.
  17. /// It's how we get the image and required matrices that we use to look for markers.
  18. /// </summary>
  19. public ZEDToOpenCVRetriever imageRetriever;
  20. /// <summary>
  21. /// Physical width of the printed ArUco markers.
  22. /// Used to find the proper world position of the markers, so make sure this is accurate.
  23. /// </summary>
  24. [Tooltip("Physical width of the printed ArUco markers.\r\n" +
  25. "Used to find the proper world position of the markers, so make sure this is accurate.")]
  26. public float markerWidthMeters = 0.2f;
  27. /// <summary>
  28. /// Pre-defined OpenCV dictionary of marker images used to identify markers in the scene.
  29. /// Note that images included in this project in the Resources folder are from the DICT_4X4 ones.
  30. /// See: https://docs.opencv.org/trunk/d9/d6a/group__aruco.html#gaf5d7e909fe8ff2ad2108e354669ecd17
  31. /// You can access any of the pre-defined dictionaries via this project: https://github.com/okalachev/arucogen
  32. /// </summary>
  33. [Tooltip("Pre-defined OpenCV dictionary of marker images used to identify markers in the scene.\r\n" +
  34. "Note that images included in this project in the Resources folder are from the DICT_4X4 ones.")]
  35. public PreDefinedmarkerDictionary markerDictionary = PreDefinedmarkerDictionary.DICT_4X4_50;
  36. /// <summary>
  37. /// Contains all MarkerObjects that have registered, sorted by their markerID values.
  38. /// We iterate through them each grab, calling MarkerDetected() on each when their
  39. /// corresponding markers are visible, and MarkerNotDetected() otherwise.
  40. /// </summary>
  41. private static Dictionary<int, List<MarkerObject>> registeredMarkers = new Dictionary<int, List<MarkerObject>>();
  42. public delegate void MarkersDetectedEvent(Dictionary<int, List<sl.Pose>> detectedposes);
  43. public event MarkersDetectedEvent OnMarkersDetected;
  44. /// <summary>
  45. /// Adds a MarkerObject to the registeredMarkers dictionary, so its MarkerDetected() function will get called when
  46. /// a marker is detected with its corresponding markerID.
  47. /// </summary>
  48. /// <param name="marker"></param>
  49. public static void RegisterMarker(MarkerObject marker)
  50. {
  51. if (!registeredMarkers.ContainsKey(marker.markerID))
  52. {
  53. registeredMarkers.Add(marker.markerID, new List<MarkerObject>());
  54. }
  55. List<MarkerObject> idlist = registeredMarkers[marker.markerID];
  56. if (!idlist.Contains(marker))
  57. {
  58. idlist.Add(marker);
  59. }
  60. else
  61. {
  62. Debug.LogError("Tried to register " + marker.gameObject.name + " as a new marker, but it was already registered.");
  63. }
  64. }
  65. /// <summary>
  66. /// Removes a MarkerObject from the registeredMarkers dictionary. Called when a MarkerObject is destroyed.
  67. /// </summary>
  68. /// <param name="marker"></param>
  69. public static void DeregisterMarker(MarkerObject marker)
  70. {
  71. if (registeredMarkers.ContainsKey(marker.markerID) && registeredMarkers[marker.markerID].Contains(marker))
  72. {
  73. registeredMarkers[marker.markerID].Remove(marker);
  74. }
  75. else
  76. {
  77. Debug.LogError("Tried to deregister " + marker.gameObject.name + " but it wasn't registered.");
  78. }
  79. }
  80. void Start()
  81. {
  82. //We'll listen for updates from a ZEDToOpenCVRetriever, which will call an event whenever it has a new image from the ZED.
  83. if (!imageRetriever) imageRetriever = ZEDToOpenCVRetriever.GetInstance();
  84. imageRetriever.OnImageUpdated_LeftGrayscale += ImageUpdated;
  85. }
  86. /// <summary>
  87. /// Looks for markers in the most recent ZED image, and updates all registered MarkerObjects accordingly.
  88. /// </summary>
  89. private void ImageUpdated(Camera cam, Mat camMat, Mat iamgeMat)
  90. {
  91. Dictionary predict = Aruco.getPredefinedDictionary((int)markerDictionary); //Load the selected pre-defined dictionary.
  92. //Create empty structures to hold the output of Aruco.detectMarkers.
  93. List<Mat> corners = new List<Mat>();
  94. Mat ids = new Mat();
  95. DetectorParameters detectparams = DetectorParameters.create();
  96. List<Mat> rejectedpoints = new List<Mat>(); //There is no overload for detectMarkers that will take camMat without this also.
  97. //Call OpenCV to tell us which markers were detected, and give their 2D positions.
  98. Aruco.detectMarkers(iamgeMat, predict, corners, ids, detectparams, rejectedpoints, camMat);
  99. //Make matrices to hold the output rotations and translations of each detected marker.
  100. Mat rotvectors = new Mat();
  101. Mat transvectors = new Mat();
  102. //Convert the 2D corner positions into a 3D pose for each detected marker.
  103. Aruco.estimatePoseSingleMarkers(corners, markerWidthMeters, camMat, new Mat(), rotvectors, transvectors);
  104. //Now we have ids, rotvectors and transvectors, which are all vertical arrays holding info about each detection:
  105. // - ids: An Nx1 array (N = number of markers detected) where each slot is the ID of a detected marker in the dictionary.
  106. // - rotvectors: An Nx3 array where each row is for an individual detection: The first row is the rotation of the marker
  107. // listed in the first row of ids, etc. The columns are the X, Y and Z angles of that marker's rotation, BUT they are not
  108. // directly usable in Unity because they're calculated very differetly. We'll deal with that soon.
  109. // - transvectors: An Nx1 array like rotvectors with each row corresponding to a detected marker, with a double[3] array with the X, Y and Z positions.
  110. // positions. These three values are usable in Unity - they're just relative to the camera, not the world, which is easy to fix.
  111. //Convert matrix of IDs into a List, to simply things for those not familiar with using Matrices.
  112. List<int> detectedIDs = new List<int>();
  113. for (int i = 0; i < ids.height(); i++)
  114. {
  115. int id = (int)ids.get(i, 0)[0];
  116. if (!detectedIDs.Contains(id)) detectedIDs.Add(id);
  117. }
  118. //We'll go through every ID that's been registered into registered Markers, and see if we found any markers in the scene with that ID.
  119. Dictionary<int, List<sl.Pose>> detectedWorldPoses = new Dictionary<int, List<sl.Pose>>(); //Key is marker ID, value is world space poses.
  120. //foreach (int id in registeredMarkers.Keys)
  121. for(int index = 0; index < transvectors.rows(); index++)
  122. {
  123. int id = (int)ids.get(index, 0)[0];
  124. if (!registeredMarkers.ContainsKey(id) || registeredMarkers[id].Count == 0) continue; //Don't waste time if the list is empty. Can happen if markers are added, then removed.
  125. if (detectedIDs.Contains(id)) //At least one MarkerObject needs to be updated. Convert pose to world space and call MarkerDetected() on it.
  126. {
  127. //Translation is just pose relative to camera. But we need to flip Y because of OpenCV's different coordinate system.
  128. Vector3 localpos;
  129. localpos.x = (float)transvectors.get(index, 0)[0];
  130. localpos.y = -(float)transvectors.get(index, 0)[1];
  131. localpos.z = (float)transvectors.get(index, 0)[2];
  132. Vector3 worldpos = cam.transform.TransformPoint(localpos); //Convert from local to world space.
  133. //Because of different coordinate frame, we need to flip the Y direction, which is pointing down instead of up.
  134. //We need to do this before we calculate the 3x3 rotation matrix soon, as that makes it muuuch harder to work with.
  135. double[] flip = rotvectors.get(index, 0);
  136. flip[1] = -flip[1];
  137. rotvectors.put(index, 0, flip);
  138. //Convert this rotation vector to a 3x3 matrix, which will hold values we can use in Unity.
  139. Mat rotmatrix = new Mat();
  140. Calib3d.Rodrigues(rotvectors.row(index), rotmatrix);
  141. //This new 3x3 matrix holds a vector pointing right in the first column, a vector pointing up in the second,
  142. //and a vector pointing forward in the third column. Rows 0, 1 and 2 are the X, Y and Z values of each vector.
  143. //We'll grab the forward and up vectors, which we can put into Quaternion.LookRotation() to get a representative Quaternion.
  144. Vector3 forward;
  145. forward.x = (float)rotmatrix.get(2, 0)[0];
  146. forward.y = (float)rotmatrix.get(2, 1)[0];
  147. forward.z = (float)rotmatrix.get(2, 2)[0];
  148. Vector3 up;
  149. up.x = (float)rotmatrix.get(1, 0)[0];
  150. up.y = (float)rotmatrix.get(1, 1)[0];
  151. up.z = (float)rotmatrix.get(1, 2)[0];
  152. Quaternion rot = Quaternion.LookRotation(forward, up);
  153. //Compensate for flip on Z axis.
  154. rot *= Quaternion.Euler(0, 0, 180);
  155. //Convert from local space to world space by multiplying the camera's world rotation with it.
  156. Quaternion worldrot = cam.transform.rotation * rot;
  157. if(!detectedWorldPoses.ContainsKey(id))
  158. {
  159. detectedWorldPoses.Add(id, new List<sl.Pose>());
  160. }
  161. detectedWorldPoses[id].Add(new sl.Pose() { translation = worldpos, rotation = worldrot });
  162. foreach (MarkerObject marker in registeredMarkers[id])
  163. {
  164. marker.MarkerDetectedSingle(worldpos, worldrot);
  165. }
  166. }
  167. }
  168. //Call the event that gives all marker world poses, if any listeners.
  169. if (OnMarkersDetected != null) OnMarkersDetected.Invoke(detectedWorldPoses);
  170. //foreach (int detectedid in detectedWorldPoses.Keys)
  171. foreach(int key in registeredMarkers.Keys)
  172. {
  173. if (detectedWorldPoses.ContainsKey(key))
  174. {
  175. foreach (MarkerObject marker in registeredMarkers[key])
  176. {
  177. marker.MarkerDetectedAll(detectedWorldPoses[key]);
  178. }
  179. }
  180. else
  181. {
  182. foreach (MarkerObject marker in registeredMarkers[key])
  183. {
  184. marker.MarkerNotDetected();
  185. }
  186. }
  187. }
  188. }
  189. /// <summary>
  190. /// Enum of OpenCV pre-defined dictionary indexes. Used for calling Aruco.getPredefinedDictionary().
  191. /// Allows for listing the dictionaries in Unity's inspector.
  192. /// </summary>
  193. public enum PreDefinedmarkerDictionary
  194. {
  195. DICT_4X4_50 = Aruco.DICT_4X4_50,
  196. DICT_4X4_100 = Aruco.DICT_4X4_100,
  197. DICT_4X4_250 = Aruco.DICT_4X4_250,
  198. DICT_4X4_1000 = Aruco.DICT_4X4_1000,
  199. DICT_5X5_50 = Aruco.DICT_5X5_50,
  200. DICT_5X5_100 = Aruco.DICT_5X5_100,
  201. DICT_5X5_250 = Aruco.DICT_5X5_250,
  202. DICT_5X5_1000 = Aruco.DICT_5X5_1000,
  203. DICT_6X6_50 = Aruco.DICT_6X6_50,
  204. DICT_6X6_100 = Aruco.DICT_6X6_100,
  205. DICT_6X6_250 = Aruco.DICT_6X6_250,
  206. DICT_6X6_1000 = Aruco.DICT_6X6_1000,
  207. DICT_7X7_50 = Aruco.DICT_7X7_50,
  208. DICT_7X7_100 = Aruco.DICT_7X7_100,
  209. DICT_7X7_250 = Aruco.DICT_7X7_250,
  210. DICT_7X7_1000 = Aruco.DICT_7X7_1000,
  211. DICT_ARUCO_ORIGINAL = Aruco.DICT_ARUCO_ORIGINAL,
  212. DICT_APRILTAG_16h5 = Aruco.DICT_APRILTAG_16h5,
  213. DICT_APRILTAG_25h9 = Aruco.DICT_APRILTAG_25h9,
  214. DICT_APRILTAG_36h10 = Aruco.DICT_APRILTAG_36h10,
  215. DICT_APRILTAG_36h11 = Aruco.DICT_APRILTAG_36h11
  216. }
  217. }
  218. #endif