ZED Camera, Multiple ArUco Marker Pose Detection with Varying ArUco Size - Carleton-SRCL/SPOT GitHub Wiki

MultisizeAruco_CVIS_PPL_Integration_v6.py

Click here for raw code
  import cv2 as cv
  from cv2 import aruco
  import numpy as np
  import math
  
  """
  This version (Version 6) showcases the OPTIMIZED complete detection and pose determination of aruco markers of different sizes
  
      - Austin Dahlseide
  """
  
  calib_data_path = r"C:\On_Table_Cal"
  L_cam_mat = np.load(calib_data_path + '/Left_calibration_matrix.npy')
  R_cam_mat = np.load(calib_data_path + '/Right_calibration_matrix.npy')
  L_dist_coef = np.load(calib_data_path + '/Left_distortion_coefficients.npy')
  R_dist_coef = np.load(calib_data_path + '/Right_distortion_coefficients.npy')
  r_vectors = np.load(calib_data_path +'/RotationVector.npy')
  t_vectors = np.load(calib_data_path +'/TranslationVector.npy')
  
  marker_dict = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
  
  param_markers = aruco.EstimateParameters()
  
  # Dictionary to Store Local Position Vectors For Each Chosen Marker ID
  Local_marker_positions = {
      1: {"R_local": np.array([-9.0125, -7.25, -15.0], dtype=np.float32), "Y_4": 7.9, "Face": 4},
      2: {"R_local": np.array([-9.1625, 7.55, -15.0], dtype=np.float32), "Y_4": 22.7, "Face": 4},
      3: {"R_local": np.array([9.1375, -7.20, -15.0], dtype=np.float32), "Y_4": 7.95, "Face": 4},
      4: {"R_local": np.array([8.5875, 7.45, -15.0], dtype=np.float32), "Y_4": 22.6, "Face": 4},
      5: {"R_local": np.array([8.4375, -2.95, -15.0], dtype=np.float32), "Y_4": 4.2, "Face": 1},
      6: {"R_local": np.array([-1.6625, 11.35, -15.0], dtype=np.float32), "Y_4": 26.5, "Face": 1},
      7: {"R_local": np.array([8.1375, -10.55, -15.0], dtype=np.float32), "Y_4": 4.6, "Face": 1},
      8: {"R_local": np.array([-8.9125, -10.35, -15.0], dtype=np.float32), "Y_4": 4.8, "Face": 2},
      9: {"R_local": np.array([-8.8125, 7.95, -15.0], dtype=np.float32), "Y_4": 23.1, "Face": 2},
      10: {"R_local": np.array([7.7275, -10.45, -15.0], dtype=np.float32), "Y_4": 4.7, "Face": 2},
      11: {"R_local": np.array([9.2275, 7.45, -15.0], dtype=np.float32), "Y_4": 22.6, "Face": 2},
      13: {"R_local": np.array([6.0375, -7.5, -15.0], dtype=np.float32), "Y_4": 7.65, "Face": 3},
      14: {"R_local": np.array([-9.1025, 7.45, -15.0], dtype=np.float32), "Y_4": 22.6, "Face": 3},
      15: {"R_local": np.array([-7.5125, -7.45, -15.0], dtype=np.float32), "Y_4": 7.7, "Face": 3},
      21: {"R_local": np.array([-9.8625, 12.8, -15.0], dtype=np.float32), "Y_4": 2.35, "Face": 1},
      22: {"R_local": np.array([-5.9625, 12.75, -15.0], dtype=np.float32), "Y_4": 2.4, "Face": 1},
      23: {"R_local": np.array([-2.0625, 12.75, -15.0], dtype=np.float32), "Y_4": 2.4, "Face": 1},
      24: {"R_local": np.array([0.7375, 4.75, -15.0], dtype=np.float32), "Y_4": 10.4, "Face": 1},
      25: {"R_local": np.array([0.7875, 0.9, -15.0], dtype=np.float32), "Y_4": 14.25, "Face": 1},}  # Format for variables ----> ID#: {"R_local": np.array([rLx, rLy, rLz], dtype=np.float32), "Y_4": 5.0},
  
  # ID 12 is NOT used.
  
  # Define a Dictionary With Marker Sizes Corresponding to Marker IDs
  marker_sizes = {
      1: {"Marker_Size": 6.8},
      2: {"Marker_Size": 6.8},
      3: {"Marker_Size": 6.8},
      4: {"Marker_Size": 6.8},
      5: {"Marker_Size": 6.8},
      6: {"Marker_Size": 6.8},
      7: {"Marker_Size": 6.8},
      8: {"Marker_Size": 6.8},
      9: {"Marker_Size": 6.8},
      10: {"Marker_Size": 6.8},
      11: {"Marker_Size": 6.8},
      13: {"Marker_Size": 6.8},
      14: {"Marker_Size": 6.8},
      15: {"Marker_Size": 6.8},
      21: {"Marker_Size": 3.5},
      22: {"Marker_Size": 3.5},
      23: {"Marker_Size": 3.5},
      24: {"Marker_Size": 3.5},
      25: {"Marker_Size": 3.5},}
  
  """
  # ***IMPORTANT*** Confirm position of ZED camera RELATIVE to Centre of Mass of Chaser Platform
  """
  Z_1 = 14.6   # Translation Distance in the Z-direction From Centre of Chaser Platform to Centre of ZED Camera (in [cm])
  Y_0 = 3.5    # Distance From Top Surface of Chaser Platform to Centre of ZED Camera (in [cm])
  
  """
  THE DISTANCE THESE ARE APART ARE CORRECT. THEIR POSITION IS ****NOT****
  """
  X_1_R = 9.2125  # Translation Distance in the X-direction From Centre of Chaser Platform to Right Lens of ZED Camera (in [cm])
  X_1_L = -2.7375 # Translation Distance in the X-direction From Centre of Chaser Platform to Left Lens of ZED Camera (in [cm])
  
  def calculate_rotation_matrix(pitch_marker):
      C_2_theta = np.array([
          [math.cos(pitch_marker), 0, -1 * math.sin(pitch_marker)],
          [0, 1, 0],
          [math.sin(pitch_marker), 0, math.cos(pitch_marker)]
      ], dtype=np.float32)
      return C_2_theta
  
  def calculate_marker_position(marker_id, R_flip, rVec, tVec, i, is_left_frame):
      if marker_id not in Local_marker_positions: # Skip processing for markers not in the Local_marker_positions dictionary
          return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan  
      
      R_ct = np.matrix(cv.Rodrigues(rVec[0])[0])
      R_tc = R_ct.T
  
      roll_marker, pitch_marker, yaw_marker = rotationMatrixToEulerAngles(R_flip * R_tc)
  
      roll_deg = math.degrees(roll_marker)
      pitch_deg = math.degrees(pitch_marker)
      yaw_deg = math.degrees(yaw_marker)
  
      # Calculating the distance
      distance = np.linalg.norm(tVec[0])
  
      # Retrieve the data dictionary from the main dictionary
      marker_data = Local_marker_positions.get(marker_id, {"R_local": np.zeros(3), "Y_4": 0.0, "Face": 0})  # Calls Marker Position Vector from Dictionary
      R_local = marker_data["R_local"]                            # Position of the Marker in the Local Coordinate System (Origin at the center of the target platform)
      Y_4 = marker_data["Y_4"]                                    # Distance From Top Surface of the Target Platform to Centre of the ArUco Marker (in [cm])
      Face = marker_data["Face"]
  
      # Calculate rotation matrix based on the pitch angle
      C_2_theta = calculate_rotation_matrix(pitch_marker)         # Euler angle (in this case Pitch) is given in [rad]
  
      # Additional calculations
      Y_2 = Y_4 - Y_0                                             # Distance From the Centre of the ZED Camera to the Centre of the ArUco Marker (in [cm])
      Z_y = math.sqrt((distance * distance) - (Y_2 * Y_2))        # Projection of Marker Distance to ZED Camera From the Side View (y-z) to the Top View (x-z)
  
      X_2 = tVec[0][0][0]                                         # Translation Distance in the X-direction of the Marker Relative to the Centre of the ZED Camera (in [cm])
      Z_2 = math.sqrt((Z_y * Z_y) - (X_2 * X_2))                  # Projection of Marker Distance to ZED Camera From the Side View (y-z) to the Top View (x-z)
  
      if is_left_frame:
          X_1 = X_1_L
      else:
          X_1 = X_1_R
  
      R_global = np.dot(C_2_theta, R_local)                       # Converts From Local Reference Frame to Global Reference Frame
      X_3 = -R_global[0]                                          # Translation Distance in X-direction of the Marker Relative to the Centre of the Target Platform in the Global Frame (in [cm])
      Z_3 = -R_global[2]                                          # Translation Distance in Z-direction of the Marker Relative to the Centre of the Target Platform in the Global Frame (in [cm])
  
      Z_abs = (Z_1 + Z_2 + Z_3)                                   # Displacement in the Z-direction of the Centre of the Target Platform Relative to the Centre of the Chaser Platform (in [cm])
      X_abs = (X_1 + X_2 + X_3)                                   # Displacement in the X-direction of the Centre of the Target Platform Relative to the Centre of the Chaser Platform (in [cm])
  
      R_abs = math.sqrt((Z_abs * Z_abs) + (X_abs * X_abs))        # Distance Between the Centre of the Target Platform and the Centre of the Chaser Platform (in [cm])
  
      # Get the Global Orientation of the Target Platform
      Yaw = pitch_deg
      if Face == 1:
          Yaw += Face_1
      elif Face == 2:
          Yaw += Face_2
      elif Face == 3:
          Yaw += Face_3
      elif Face == 4:
          Yaw += Face_4
  
      # Ignore data from 4.0 cm markers when R_abs is greater than or equal to 70 cm
      if marker_sizes.get(marker_id, {"Marker_Size": 0.0})["Marker_Size"] == 4.0 and R_abs >= 70.0:
          return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
  
      return roll_deg, pitch_deg, yaw_deg, distance, R_local, C_2_theta, Z_y, X_2, Z_2, R_global, X_3, Z_3, Z_abs, X_abs, R_abs, Yaw
  
  def isRotationMatrix(R):
      Rt = np.transpose(R)
      shouldBeIdentity = np.dot(Rt, R)
      I = np.identity(3, dtype=R.dtype)
      n = np.linalg.norm(I - shouldBeIdentity)
      return n < 1e-6
  
  # Calculates rotation matrix to euler angles (x and z are swapped).
  def rotationMatrixToEulerAngles(R):
      assert (isRotationMatrix(R))
  
      sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0])
  
      singular = sy < 1e-6
  
      if not singular:
          x = math.atan2(R[2, 1], R[2, 2])
          y = math.atan2(-R[2, 0], sy)
          z = math.atan2(R[1, 0], R[0, 0])
      else:
          x = math.atan2(-R[1, 2], R[1, 1])
          y = math.atan2(-R[2, 0], sy)
          z = 0
  
      return np.array([x, y, z])
  
  def DnD(marker_sizes, marker_corners, marker_IDs, frame, cam_mat, dist_coef, rVec, tVec, roll_dict, pitch_dict, yaw_dict):
      
      for i, ids in enumerate(marker_IDs):
          # Detect and Draw
          marker_size_data = marker_sizes.get(ids[0], {"Marker_Size": 0.0})
          MARKER_SIZE = marker_size_data["Marker_Size"]
  
          if MARKER_SIZE <= 0.0:
              continue  # Skips this iteration
  
          rVec, tVec, _ = aruco.estimatePoseSingleMarkers(marker_corners[i], MARKER_SIZE, cam_mat, dist_coef)
  
          # Append the rotation vector of each marker to the list
          rvecs_list.extend(rVec)      
  
          #-- Obtain the rotation matrix tag->camera
          R_ct = np.matrix(cv.Rodrigues(rVec[0])[0])
          R_tc = R_ct.T
  
          #-- Get the attitude in terms of euler 321 (Needs to be flipped first)
          roll_marker, pitch_marker, yaw_marker = rotationMatrixToEulerAngles(R_flip * R_tc)
  
          # Call the function for each marker in the right frame
          _, _, _, _, _, _, _, _, _, _, _, _, Z_abs, X_abs, R_abs, Yaw = calculate_marker_position(
              ids[0], R_flip, rVec, tVec, i, is_left_frame=False)
          
          # Call the function for each marker in the left frame
          _, _, _, _, _, _, _, _, _, _, _, _, Z_abs, X_abs, R_abs, Yaw = calculate_marker_position(
              ids[0], R_flip, rVec, tVec, i, is_left_frame=True)
  
          # Store Euler angles in dictionaries based on marker ID
          roll_dict[ids[0]] = math.degrees(roll_marker)
          pitch_dict[ids[0]] = math.degrees(pitch_marker)
          yaw_dict[ids[0]] = math.degrees(yaw_marker)
  
          print("Marker ID:",MARKER_SIZE ,"C-C: ", R_abs, "Yaw: ", Yaw)
  
      return frame, roll_dict, pitch_dict, yaw_dict, rVec, tVec
  
  #--- 180 deg rotation matrix around the x axis
  R_flip  = np.zeros((3,3), dtype=np.float32)
  R_flip[0,0] = 1.0
  R_flip[1,1] =-1.0
  R_flip[2,2] =-1.0
  
  # Initialize an Empty List to Store the Path Planning Required Data
  PPL_list = []
  
  # Initialize empty dictionaries for Euler Angles & Centre-to-Centre Distance
  roll_dict_R = {}
  pitch_dict_R = {}
  yaw_dict_R = {}
  R_abs_dict_R = {}
  
  roll_dict_L = {}
  pitch_dict_L = {}
  yaw_dict_L = {}
  R_abs_dict_L = {}
  
  # Initialize an empty list to store the rotation vectors 
  rvecs_list =[]
  
  # Angles to Add to Each Face
  Face_1 = 180
  Face_2 = 90
  Face_3 = 0
  Face_4 = 270
  
  cap = cv.VideoCapture(0)
  cap.set(cv.CAP_PROP_FRAME_WIDTH, 2560)
  cap.set(cv.CAP_PROP_FRAME_HEIGHT, 720)
  cap.set(cv.CAP_PROP_FPS, 30)           # Set FPS Rate
  
  while True:
      ret, Fullframe = cap.read()
      if not ret:
          break # check if imported correctly
  
      # Initialize Empty Lists for Distances in the Left and Right Frames
      distances_left = []
      distances_right = []
      
      # Initialize Empty Lists to Store Global Yaw Values for Both Left and Right Frames
      yaw_values_left = []
      yaw_values_right = []
  
      # Initialize Empty Lists to Store Global X-Y Values for Both Left and Right Frames
      X_left = []
      X_right = []
      Y_left = []
      Y_right = []
  
      h,w,_ = Fullframe.shape # Split inputs into left and right
      w_c = w // 2
      frame_left = Fullframe[0:h, 0:w_c]
      frame_right = Fullframe[0:h, w_c:w]
      R_gray_frame = cv.cvtColor(frame_right, cv.COLOR_BGR2GRAY)
      L_gray_frame = cv.cvtColor(frame_left, cv.COLOR_BGR2GRAY)
      L_marker_corners, L_marker_IDs, reject = aruco.detectMarkers(L_gray_frame, marker_dict, parameters=aruco.DetectorParameters())
      R_marker_corners, R_marker_IDs, reject = aruco.detectMarkers(R_gray_frame, marker_dict, parameters=aruco.DetectorParameters())
  
      # Ensures that rVec and tVec are Defined Outside the Loop
      rVec_L = None
      tVec_L = None
      rVec_R = None
      tVec_R = None
  
      for marker_id in roll_dict_L.keys():
          roll_dict_L[marker_id] = None
          pitch_dict_L[marker_id] = None
          yaw_dict_L[marker_id] = None
          R_abs_dict_L[marker_id] = None
      for marker_id in roll_dict_R.keys():
          roll_dict_R[marker_id] = None
          pitch_dict_R[marker_id] = None
          yaw_dict_R[marker_id] = None
          R_abs_dict_R[marker_id] = None
  
      # Split into left and right frames and process. Detecting markers in each frame then combining them into a single
      # Reset Euler angles for all marker IDs to None at the beginning of each iteration
  
      # frame for display
      if L_marker_corners:
          L_img, roll_dict_L, pitch_dict_L, yaw_dict_L, rVec_L, tVec_L = DnD(marker_sizes, L_marker_corners, L_marker_IDs, frame_left, L_cam_mat, L_dist_coef, r_vectors, t_vectors, roll_dict_L, pitch_dict_L, yaw_dict_L)
  
          # Calculate and Append the Calculated Centre to Centre Distance and Global Yaw Angle to the List for Left Frame
          for i, ids in enumerate(L_marker_IDs):
              roll_deg, pitch_deg, yaw_deg, distance, _, _, _, _, _, _, _, _, Z_abs, X_abs, R_abs, Yaw = calculate_marker_position(
                  ids[0], R_flip, rVec_L, tVec_L, i, is_left_frame=True)
              distances_left.append(R_abs)
              yaw_values_left.append(Yaw)
              X_left.append(X_abs)
              Y_left.append(Z_abs) # Actually "Z," but for purposes of path planning data labelling it has been changed to Y
              
      else:
          L_img = frame_left
  
      if R_marker_corners:
          R_img, roll_dict_R, pitch_dict_R, yaw_dict_R, rVec_R, tVec_R = DnD(marker_sizes, R_marker_corners, R_marker_IDs, frame_right, R_cam_mat, R_dist_coef, r_vectors, t_vectors, roll_dict_R, pitch_dict_R, yaw_dict_R)
  
          # Calculate and Append the Calculated Centre to Centre Distance and Global Yaw Angle to the List for Right Frame
          for i, ids in enumerate(R_marker_IDs):
              roll_deg, pitch_deg, yaw_deg, distance, _, _, _, _, _, _, _, _, Z_abs, X_abs, R_abs, Yaw = calculate_marker_position(
                  ids[0], R_flip, rVec_R, tVec_R, i, is_left_frame=False)
              distances_right.append(R_abs)
              yaw_values_right.append(Yaw)
              X_right.append(X_abs)
              Y_right.append(Z_abs) # Actually "Z," but for purposes of path planning data labelling it has been changed to Y
              
      else:
          R_img = frame_right
  
      # Average the Yaw Values of All Detected Markers For Each ZED Camera Frame    
      average_yaw_left = np.nanmean(yaw_values_left)
      average_yaw_right = np.nanmean(yaw_values_right)
  
      # Average the X and Z Distance Values of All Detected Markers For Each ZED Camera Frame
      avg_X_left = np.nanmean(X_left)
      avg_X_right = np.nanmean(X_right)
      avg_Y_left = np.nanmean(Y_left)
      avg_Y_right = np.nanmean(Y_right)
  
      # Average the Centre to Centre Distance and Global Yaw Angle Between the Two Frames
      Yaw_Global = np.nanmean([average_yaw_left, average_yaw_right])
      x = np.nanmean([avg_X_left, avg_X_right])
      y = np.nanmean([avg_Y_left, avg_Y_right])
  
      print("X: ", x, "Y: ", y, "Yaw: ", Yaw_Global)
  
      # Append data to the list
      PPL_list.append([x, y, Yaw_Global])
  
      # stack side by side
      Hori = np.concatenate((L_img, R_img), axis=1) 
      cv.imshow('HORIZONTAL', Hori) 
      key = cv.waitKey(1)
      if key == ord("q") or key == ord('Q'):
          break
  cap.release()
  cv.destroyAllWindows()

Purpose

  • The purpose of this function is to detect and determine the pose of the platforms using ArUco markers in a stereo camera setup. It achieves this by utilizing the OpenCV library for computer vision tasks such as marker detection and pose estimation. The function also calculates the position and orientation of the platforms relative to the table reference frame, incorporating camera calibration data and marker dictionaries. Additionally, it includes functions for converting rotation matrices to Euler angles and vice versa, along with tools for processing marker data and displaying the results in real-time using a graphical user interface.

Inputs

  • Left_calibration_matrix.npy
    • The calibration procedure estimates the intrinsic parameters such as focal lengths and optical centers of the left camera. The resulting camera matrix is then used for undistorting images and mapping 3D points to 2D image coordinates.
  • Right_calibration_matrix.npy
    • The calibration procedure estimates the intrinsic parameters such as focal lengths and optical centers of the right camera. The resulting camera matrix is then used for undistorting images and mapping 3D points to 2D image coordinates.
  • Left_distortion_coefficients.npy
    • These are the distortion coefficients and used in the camera calibration functions to correct for distortion effects in the left camera's images.
  • Right_distortion_coefficients.npy
    • These are the distortion coefficients and used in the camera calibration functions to correct for distortion effects in the right camera's images.
  • RotationVector.npy
    • In camera calibration and pose estimation, rotation vectors are often obtained as part of the calibration process. They represent the rotation of the camera from its initial position. These rotation vectors are crucial for determining the pose of the camera relative to a known coordinate system.
  • TranslationVector.npy
    • In stereo vision, the translation vector is crucial for determining the baseline between the left and right cameras. The baseline, along with the focal lengths, is used to compute depth information for 3D reconstruction.
  • Aruco Marker Dictionary and Parameters
    • This function uses 4x4 sized Aruco Markers (aruco.DICT_4X4_50), as well as the measured size of the printed Aruco markers.

Outputs

  • The primary purpose of the script is to visualize and analyze the detected ArUco markers, calculate their poses, and collect relevant data for further processing by the Path Planning sub-team.
  • The console prints information about each detected ArUco marker, including:
    • Marker ID (MARKER_SIZE)
    • Centre-to-centre distance (R_abs)
    • Yaw angle (Yaw)
    • Global X, Y, and Z positions
  • This script appends data to a list to be used by Path Planning called PPL_list, which contains information about the global X, Y, and Yaw values.
  • The Euler angles for roll, pitch, and yaw are stored in dictionaries (roll_dict_R, pitch_dict_R, yaw_dict_R, roll_dict_L, pitch_dict_L, yaw_dict_L) for each detected marker ID.
  • The script displays live stereo feed of adjacent left and right frames with ArUco markers, pose and distance information annotated directly on the feed.

Click here to go BACK

⚠️ **GitHub.com Fallback** ⚠️