Optimized ZED Camera Calibration - Carleton-SRCL/SPOT GitHub Wiki

CCS.py

Click here for raw code
  """
  This is technically version three of stero calibration for the computer vision team, dubbed Clear Cut Stereo (CCS)
  
  This version is a synopsis of CalibrationProcedure,py, stereovision_calibration.py and finally StereoVision_cal_2.py
  
  It is meant to be simple, clear and effective for the production of the needed calibration files.
  Then have supplementary error analysis for calculations for a full diagnostic.
  
  Written by Hayden Arms, November 8th, 2023.
  
  """
  
  # Import needed toolboxes
  import numpy as np
  import cv2 as cv
  import glob
  import matplotlib.pyplot as plt
  import os
  
  
  def main():
      # Calibration Settings
      squareSize = 18  # measured square size in world units (mm)
      chessboardSize = (14, 9)  # size of checker board used (number of squares in col and rows +1)
      Export = True
      ExportDirectory = r'/On table Cal'
      ImageDirectoryR = r'/*.png'  # directory to cal images
      ImageDirectoryL = r'/*.png'
      FullDiagnostic = True  # Config T/F to run full error diagnositc, set to false to simply output the stereo calibration files
  
      # Full diagnostic Settings
      isVerticalStereo = False  # left-right or up-down stereo camera arrangement
      useCalibrated = True  # calibrated or uncalibrated stereo rectification
  
      # CCS Main Body, Stereo Calibration File Generation and Export.
      imgesL, imgesR, LimgNames, RimgNames = ImageRead(chessboardSize, ImageDirectoryR, ImageDirectoryL)  # Read in Images
  
      # First analyize the checkerboard images for corner positions
      frameSize, objpoints, cornersL, imgpointsL, cornersR, imgpointsR = \
          FindChessCorners(imgesL, imgesR, FullDiagnostic, squareSize, chessboardSize)  # Find corners of checkerboard 
  
      # calibrate each camera individually for initial guess`
      ML_i, DL_i, MR_i, DR_i = InitialSingleCamCal(objpoints, imgpointsL, imgpointsR, frameSize, FullDiagnostic)
  
      # Complete Stereo Calibration using initial camera guesses
      retRMS, CamMatL, DistMatL, CamMatR, DistMatR, RotMat, TranslVec, EssenM, FundM = StereoCal(objpoints, imgpointsL, imgpointsR, ML_i, DL_i, MR_i, DR_i, frameSize, FullDiagnostic)
  
      print("Returned RMS Error for Stero Cal: {}".format(retRMS))
  
      # Export Files
      if Export == True:
          np.save(os.path.join(ExportDirectory, "Left_calibration_matrix"), CamMatL)
          np.save(os.path.join(ExportDirectory, "Left_distortion_coefficients"), DistMatL)
          np.save(os.path.join(ExportDirectory, "Right_calibration_matrix"), CamMatR)
          np.save(os.path.join(ExportDirectory, "Right_distortion_coefficients"), DistMatR)
          np.save(os.path.join(ExportDirectory, "TranslationVector"), TranslVec)
          np.save(os.path.join(ExportDirectory, "RotationVector"), RotMat)
          # camera_model = dict([('ML', CamMatL), ('MR', CamMatR), ('distL', DistMatL),
          #                     ('distR', DistMatR), ('R', RotMat), ('T', TranslVec)])
          # np.save(os.path.join(ExportDirectory,"Camera_model"),camera_model)
  
      # Now enter full diagnostic section
      if FullDiagnostic == True:
          N = len(imgesL)
          mean_error_alt = calc_rms_stereo(objpoints, imgpointsL, imgpointsR, CamMatL, DistMatL, CamMatR, DistMatR, RotMat, TranslVec, LimgNames, RimgNames)
          print("Alternative Error Calculation: {}".format(mean_error_alt))
          # DisplayRectifiedImage(CamMatL, DistMatL, CamMatR, DistMatR, frameSize,RotMat, TranslVec)
          # DisparityMap()
  
  
  def DisparityMap():
      pass
  
  
  def DisplayRectifiedImage(Cam_L, Dist_L, Cam_R, Dist_R, frmSz, RotVec, TranslVec):
      RCT = cv.stereoRectify(Cam_L, Dist_L, Cam_R, Dist_R, frmSz, RotVec, TranslVec, 'ZeroDisparity', True, 'Alpha',
                             -1)  # 'Alpha',1
  
      # OpenCV can handle left-right or up-down camera arrangements
      isVerticalStereo = abs(2) > abs(1)
  
  
  def calc_rms_stereo(objectpoints, imgpoints_l, imgpoints_r, A1, D1, A2, D2, R, T, LframeName, RframeName):
      tot_error = 0
      total_points = 0
      frame_error_L = np.ones(len(imgpoints_l))
      frame_error_R = np.ones(len(imgpoints_r))
  
      for i, objpoints in enumerate(objectpoints):
          # calculate world <-> cam1 transformation
          _, rvec_l, tvec_l, _ = cv.solvePnPRansac(objpoints, imgpoints_l[i], A1, D1)
  
          # compute reprojection error for cam1
          rp_l, _ = cv.projectPoints(objpoints, rvec_l, tvec_l, A1, D1)
          l_er = np.sum(np.square(np.float64(imgpoints_l[i] - rp_l)))
          tot_error += l_er
          total_points += len(objpoints)
          frame_error_L[i] = np.sqrt(l_er / len(objpoints))
  
          # calculate world <-> cam2 transformation
          rvec_r, tvec_r = cv.composeRT(rvec_l, tvec_l, cv.Rodrigues(R)[0], T)[:2]
  
          # compute reprojection error for cam2
          rp_r, _ = cv.projectPoints(objpoints, rvec_r, tvec_r, A2, D2)
          r_er = np.sum(np.square(imgpoints_r[i] - rp_r))
          tot_error += r_er
          total_points += len(objpoints)
          frame_error_R[i] = np.sqrt(r_er / len(objpoints))
  
      mean_error = np.sqrt(tot_error / total_points)
  
      Nrng = np.arange(len(frame_error_L))
      f, ax = plt.subplots(1, sharex=True)
      ax.bar(Nrng - 0.2, frame_error_L, width=0.3, color='#F5E338', edgecolor='k', label="Left Feed")
      ax.bar(Nrng + 0.2, frame_error_R, width=0.3, color='#4916F5', edgecolor='k', label="Right Feed")
      ax.axhline(y=mean_error, color='r', linewidth=2, linestyle='--', label="Mean error")
      ax.legend(loc="upper right")
      ax.set_title('Individual Frame Error')
      plt.xlabel('Image Pairs')
      plt.ylabel('RMS Error (Pixel Units)')
  
      print("Image  - Error Correlations")
      frame_error_R = np.transpose([frame_error_R])
      frame_error_L = np.transpose([frame_error_L])
      RframeName = np.transpose([RframeName])
      LframeName = np.transpose([LframeName])
      fullList_R = np.hstack((np.array(frame_error_R, dtype=object), np.array(RframeName, dtype=object)))
      fullList_L = np.hstack((np.array(frame_error_L, dtype=object), np.array(LframeName, dtype=object)))
      print("Left Feed")
      print(fullList_L)
      print("Right Feed")
      print(fullList_R)
  
      return mean_error
  
  
  def ImageRead(chessSz, ImageDirR, ImageDirL):
      # Import Images
      imgesL = sorted(glob.glob(ImageDirL))  # import each image set for each camera lens
      imgesR = sorted(glob.glob(ImageDirR))
      assert len(imgesL) != 0 and len(imgesL) == len(imgesR), "Not an equal amount of left and right images."
  
      N = len(imgesL)  # number of calibration images
      h_img, w_img, _ = cv.imread(imgesL[0]).shape  # image size
  
      # sorted names
      leftImgNames = []
      rightImgNames = []
      for id in range(N):
          leftImgNames.append(imgesL[id].rsplit('/', 1)[1])
          rightImgNames.append(imgesR[id].rsplit('/', 1)[1])
  
      print('{} pairs of stereo images\n'.format(N))
      print('Full Feed Frame Size = {}x{}\n'.format(h_img, w_img))
      print('Chessboard Pattern size (nx,ny) = {}x{}\n'.format(chessSz[0], chessSz[1]))
      return imgesL, imgesR, leftImgNames, rightImgNames
  
  
  def FindChessCorners(imgesL, imgesR, FD, sqSz, chessSz):
      criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 1000, 1e-5)  # termination criteria for subpix
      # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
      objp = np.zeros((chessSz[0] * chessSz[1], 3), np.float32)
      objp[:, :2] = np.mgrid[0:chessSz[0], 0:chessSz[1]].T.reshape(-1, 2)
      objp = objp * sqSz  # in mm
      # Arrays to store object points and image points from all the images.
      objpoints = []  # 3d point in real world space
      imgpointsL = []  # 2d points in image plane.
      imgpointsR = []  # 2d points in image plane.
      print("Determining Chessboard Corners for Left and Right feed inputs \n")
      for imgLeft, imgRight in zip(imgesL, imgesR):  # for each image (back and forth in left and right)
          imgL = cv.imread(imgLeft)
          imgR = cv.imread(imgRight)
          gray_img_L = cv.cvtColor(imgL, cv.COLOR_BGR2GRAY)
          gray_img_R = cv.cvtColor(imgR, cv.COLOR_BGR2GRAY)
  
          # Find the chess board corners
          retL, cornersL = cv.findChessboardCorners(gray_img_L, (chessSz[0], chessSz[1]), None)
          retR, cornersR = cv.findChessboardCorners(gray_img_R, (chessSz[0], chessSz[1]), None)
  
          # If found, add object points, image points (after refining them)
          if retL and retR:  # if both detected
              objpoints.append(objp)
              corners2L = cv.cornerSubPix(gray_img_L, cornersL, (11, 11), (-1, -1), criteria)
              corners2R = cv.cornerSubPix(gray_img_R, cornersR, (11, 11), (-1, -1), criteria)
              imgpointsL.append(corners2L)
              imgpointsR.append(corners2R)
              if FD == True:
                  # Draw and display the corners
                  cv.drawChessboardCorners(imgL, chessSz, cornersL, retL)  # draw em on
                  cv.drawChessboardCorners(imgR, chessSz, cornersR, retR)
                  Hori = np.concatenate((imgL, imgR), axis=1)
                  cv.imshow('HORIZONTAL', Hori)
                  cv.waitKey(300)  # manual wait
          else:
              print("Failed Detection")
  
      print("Determination Completed.\n Right Camera Points Found: {} \n Left Camera Points Found: {} \n".format(len(imgpointsR), len(imgpointsL)))
      cv.destroyAllWindows()
      frameSize = gray_img_L.shape[::-1]
      print("Single Feed Frame Size: {}".format(frameSize))
      return frameSize, objpoints, cornersL, imgpointsL, cornersR, imgpointsR
  
  
  def InitialSingleCamCal(opnts, pntsL, pntsR, frmSz, FD):
      print("Calibrating individual cameras... \n")
      # Calibrate each frame
      ret_L, mtx_L, dist_L, rvecs_L, tvecs_L = cv.calibrateCamera(opnts, pntsL, frmSz, None, None)
      newCameraMatrixL, roi_L = cv.getOptimalNewCameraMatrix(mtx_L, dist_L, frmSz, 1, frmSz)
      ret_R, mtx_R, dist_R, rvecs_R, tvecs_R = cv.calibrateCamera(opnts, pntsR, frmSz, None, None)
      newCameraMatrixR, roi_L = cv.getOptimalNewCameraMatrix(mtx_R, dist_R, frmSz, 1, frmSz)
      if FD == True:
          print("Reprojection error LEFT opencv:{} ".format(ret_L))
          print("Reprojection error RIGHT opencv:{} ".format(ret_R))
          # These are effecttively our initial guesses for each input frame
      print("Initial Guess Completed.\n")
      return newCameraMatrixL, dist_L, newCameraMatrixR, dist_R
  
  
  def StereoCal(objpoints, imgPL, imgPR, CamMatL, distL, CamMatR, distR, frmSz, FD):
      print("Beginning Stereo Calibration")
      criteria_stereo = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 1000, 1e-5)
      ster_flags = 0
      ster_flags |= cv.CALIB_SAME_FOCAL_LENGTH  # to true
      ster_flags |= cv.CALIB_FIX_ASPECT_RATIO  # to true
      ster_flags |= cv.CALIB_RATIONAL_MODEL
      ster_flags |= cv.CALIB_FIX_K3
      ster_flags |= cv.CALIB_FIX_K4
      ster_flags |= cv.CALIB_FIX_K5
  
      if FD == True:
          print("Flags Total:")
          print(ster_flags)
          print("Criteria:")
          print(criteria_stereo)
      # objectPoints, imagePoints1, imagePoints2, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize
      retRMS, ML_s, dL_s, MR_s, dR_s, RotMat, TranslVec, EssenM, FundM = cv.stereoCalibrate(objpoints, imgPL, imgPR, CamMatL, distL, CamMatR, distR, frmSz, criteria=criteria_stereo, flags=ster_flags)
  
      return retRMS, ML_s, dL_s, MR_s, dR_s, RotMat, TranslVec, EssenM, FundM
  
  
  if __name__ == "__main__":
      main()
      plt.show()

Purpose

  • This function is the improved calibration file, intended to be more clear and effective in producing the needed calibration files. Additionally, supplementary error analysis calculations are carried out for a full diagnostic of the calibration.

Inputs

  • squareSize
    • This is the real world measured size of the squares on the checkerboard in mm.
  • chessboardSize
    • Like in CalibrationProcedure.py, the size of the chessboard is required to determine number of squares on the printout. This is determined by counting the number of columns and rows of full squares (+1). A board with 13 columns and 8 rows of full squares would be input as (14,9) due to it having 13+1 columns and 8+1 rows.

Figure 6: Chessboard image used in stereo calibration.

  • ExportDirectory
    • This is the directory where the right/left calibration matrices, distortion coefficients, and translation/rotation vectors will be stored.
  • ImageDirectoryR
    • All the captured images from the right camera are stored in this directory.
  • ImageDirectoryL
    • All the captured images from the left camera are stored in this directory.
  • FullDiagnostic
    • This variable determines whether the to run the error analysis diagnostic (True) or not (False).

Outputs

  • Left_calibration_matrix.npy
    • This is the calibration matrix of the stereo camera left input calibration matrix.
  • Left_distortion_coefficients.npy
    • These are the determined distortion coefficients for the stereo camera left lens.
  • Right_calibration_matrix.npy
    • This is the calibration matrix of the stereo camera right input calibration matrix.
  • Right_distortion_coefficients.npy
    • These are the determined distortion coefficients for the stereo camera right lens.
  • mean_error_alt
    • This function takes calibration-related parameters and calculates the mean error using an alternative method involving computing the reprojection errors for both left and right cameras and then calculating the mean error. This provides an additional diagnostic measure for assessing the quality of the stereo calibration. The mean error is a measure of how well the stereo calibration model fits the observed data.

Click here to go BACK

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