├── Images ├── pose_test.jpg ├── pose_output.gif ├── test_image_1.png ├── output_sample.png └── pose_output_image.png ├── calibration_matrix.npy ├── distortion_coefficients.npy ├── LICENSE ├── generate_aruco_tags.py ├── detect_aruco_images.py ├── detect_aruco_video.py ├── utils.py ├── pose_estimation.py ├── calibration.py └── README.md /Images/pose_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/Images/pose_test.jpg -------------------------------------------------------------------------------- /Images/pose_output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/Images/pose_output.gif -------------------------------------------------------------------------------- /Images/test_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/Images/test_image_1.png -------------------------------------------------------------------------------- /calibration_matrix.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/calibration_matrix.npy -------------------------------------------------------------------------------- /Images/output_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/Images/output_sample.png -------------------------------------------------------------------------------- /Images/pose_output_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/Images/pose_output_image.png -------------------------------------------------------------------------------- /distortion_coefficients.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSNCodes/ArUCo-Markers-Pose-Estimation-Generation-Python/HEAD/distortion_coefficients.npy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SowmiyaNarayanan G 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /generate_aruco_tags.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Sample Command:- 3 | python generate_aruco_tags.py --id 24 --type DICT_5X5_100 -o tags/ 4 | ''' 5 | 6 | 7 | import numpy as np 8 | import argparse 9 | from utils import ARUCO_DICT 10 | import cv2 11 | import sys 12 | 13 | 14 | ap = argparse.ArgumentParser() 15 | ap.add_argument("-o", "--output", required=True, help="path to output folder to save ArUCo tag") 16 | ap.add_argument("-i", "--id", type=int, required=True, help="ID of ArUCo tag to generate") 17 | ap.add_argument("-t", "--type", type=str, default="DICT_ARUCO_ORIGINAL", help="type of ArUCo tag to generate") 18 | ap.add_argument("-s", "--size", type=int, default=200, help="Size of the ArUCo tag") 19 | args = vars(ap.parse_args()) 20 | 21 | 22 | # Check to see if the dictionary is supported 23 | if ARUCO_DICT.get(args["type"], None) is None: 24 | print(f"ArUCo tag type '{args['type']}' is not supported") 25 | sys.exit(0) 26 | 27 | arucoDict = cv2.aruco.Dictionary_get(ARUCO_DICT[args["type"]]) 28 | 29 | print("Generating ArUCo tag of type '{}' with ID '{}'".format(args["type"], args["id"])) 30 | tag_size = args["size"] 31 | tag = np.zeros((tag_size, tag_size, 1), dtype="uint8") 32 | cv2.aruco.drawMarker(arucoDict, args["id"], tag_size, tag, 1) 33 | 34 | # Save the tag generated 35 | tag_name = f'{args["output"]}/{args["type"]}_id_{args["id"]}.png' 36 | cv2.imwrite(tag_name, tag) 37 | cv2.imshow("ArUCo Tag", tag) 38 | cv2.waitKey(0) 39 | cv2.destroyAllWindows() -------------------------------------------------------------------------------- /detect_aruco_images.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Sample Command:- 3 | python detect_aruco_images.py --image Images/test_image_1.png --type DICT_5X5_100 4 | ''' 5 | import numpy as np 6 | from utils import ARUCO_DICT, aruco_display 7 | import argparse 8 | import cv2 9 | import sys 10 | 11 | 12 | ap = argparse.ArgumentParser() 13 | ap.add_argument("-i", "--image", required=True, help="path to input image containing ArUCo tag") 14 | ap.add_argument("-t", "--type", type=str, default="DICT_ARUCO_ORIGINAL", help="type of ArUCo tag to detect") 15 | ap.add_argument("-s", "--save_output", action="store_false") 16 | args = vars(ap.parse_args()) 17 | 18 | 19 | 20 | print("Loading image...") 21 | image = cv2.imread(args["image"]) 22 | h,w,_ = image.shape 23 | width=600 24 | height = int(width*(h/w)) 25 | image = cv2.resize(image, (width, height), interpolation=cv2.INTER_CUBIC) 26 | 27 | 28 | # verify that the supplied ArUCo tag exists and is supported by OpenCV 29 | if ARUCO_DICT.get(args["type"], None) is None: 30 | print(f"ArUCo tag type '{args['type']}' is not supported") 31 | sys.exit(0) 32 | 33 | # load the ArUCo dictionary, grab the ArUCo parameters, and detect 34 | # the markers 35 | print("Detecting '{}' tags....".format(args["type"])) 36 | arucoDict = cv2.aruco.getPredefinedDictionary(ARUCO_DICT[args["type"]]) 37 | arucoParams = cv2.aruco.DetectorParameters() 38 | arucoDetector = cv2.aruco.ArucoDetector(arucoDict, arucoParams) 39 | corners, ids, rejected = arucoDetector.detectMarkers(image) 40 | 41 | detected_markers = aruco_display(corners, ids, rejected, image) 42 | cv2.imshow("Image", detected_markers) 43 | 44 | if args["save_output"]: 45 | cv2.imwrite("output_sample.png",detected_markers) 46 | 47 | cv2.waitKey(0) -------------------------------------------------------------------------------- /detect_aruco_video.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Sample Command:- 3 | python detect_aruco_video.py --type DICT_5X5_100 --camera True 4 | python detect_aruco_video.py --type DICT_5X5_100 --camera False --video test_video.mp4 5 | ''' 6 | 7 | import numpy as np 8 | from utils import ARUCO_DICT, aruco_display 9 | import argparse 10 | import time 11 | import cv2 12 | import sys 13 | 14 | 15 | ap = argparse.ArgumentParser() 16 | ap.add_argument("-i", "--camera", required=True, help="Set to True if using webcam") 17 | ap.add_argument("-v", "--video", help="Path to the video file") 18 | ap.add_argument("-t", "--type", type=str, default="DICT_ARUCO_ORIGINAL", help="Type of ArUCo tag to detect") 19 | args = vars(ap.parse_args()) 20 | 21 | if args["camera"].lower() == "true": 22 | video = cv2.VideoCapture(0) 23 | time.sleep(2.0) 24 | 25 | else: 26 | if args["video"] is None: 27 | print("[Error] Video file location is not provided") 28 | sys.exit(1) 29 | 30 | video = cv2.VideoCapture(args["video"]) 31 | 32 | if ARUCO_DICT.get(args["type"], None) is None: 33 | print(f"ArUCo tag type '{args['type']}' is not supported") 34 | sys.exit(0) 35 | 36 | arucoDict = cv2.aruco.getPredefinedDictionary(ARUCO_DICT[args["type"]]) 37 | arucoParams = cv2.aruco.DetectorParameters() 38 | arucoDetector = cv2.aruco.ArucoDetector(arucoDict, arucoParams) 39 | 40 | while True: 41 | ret, frame = video.read() 42 | 43 | if ret is False: 44 | break 45 | 46 | h, w, _ = frame.shape 47 | 48 | width=1000 49 | height = int(width*(h/w)) 50 | frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_CUBIC) 51 | corners, ids, rejected = arucoDetector.detectMarkers(frame) 52 | 53 | detected_markers = aruco_display(corners, ids, rejected, frame) 54 | 55 | cv2.imshow("Image", detected_markers) 56 | 57 | key = cv2.waitKey(1) & 0xFF 58 | if key == ord("q"): 59 | break 60 | 61 | cv2.destroyAllWindows() 62 | video.release() -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | ARUCO_DICT = { 4 | "DICT_4X4_50": cv2.aruco.DICT_4X4_50, 5 | "DICT_4X4_100": cv2.aruco.DICT_4X4_100, 6 | "DICT_4X4_250": cv2.aruco.DICT_4X4_250, 7 | "DICT_4X4_1000": cv2.aruco.DICT_4X4_1000, 8 | "DICT_5X5_50": cv2.aruco.DICT_5X5_50, 9 | "DICT_5X5_100": cv2.aruco.DICT_5X5_100, 10 | "DICT_5X5_250": cv2.aruco.DICT_5X5_250, 11 | "DICT_5X5_1000": cv2.aruco.DICT_5X5_1000, 12 | "DICT_6X6_50": cv2.aruco.DICT_6X6_50, 13 | "DICT_6X6_100": cv2.aruco.DICT_6X6_100, 14 | "DICT_6X6_250": cv2.aruco.DICT_6X6_250, 15 | "DICT_6X6_1000": cv2.aruco.DICT_6X6_1000, 16 | "DICT_7X7_50": cv2.aruco.DICT_7X7_50, 17 | "DICT_7X7_100": cv2.aruco.DICT_7X7_100, 18 | "DICT_7X7_250": cv2.aruco.DICT_7X7_250, 19 | "DICT_7X7_1000": cv2.aruco.DICT_7X7_1000, 20 | "DICT_ARUCO_ORIGINAL": cv2.aruco.DICT_ARUCO_ORIGINAL, 21 | "DICT_APRILTAG_16h5": cv2.aruco.DICT_APRILTAG_16h5, 22 | "DICT_APRILTAG_25h9": cv2.aruco.DICT_APRILTAG_25h9, 23 | "DICT_APRILTAG_36h10": cv2.aruco.DICT_APRILTAG_36h10, 24 | "DICT_APRILTAG_36h11": cv2.aruco.DICT_APRILTAG_36h11 25 | } 26 | 27 | def aruco_display(corners, ids, rejected, image): 28 | if len(corners) > 0: 29 | # flatten the ArUco IDs list 30 | ids = ids.flatten() 31 | # loop over the detected ArUCo corners 32 | for (markerCorner, markerID) in zip(corners, ids): 33 | # extract the marker corners (which are always returned in 34 | # top-left, top-right, bottom-right, and bottom-left order) 35 | corners = markerCorner.reshape((4, 2)) 36 | (topLeft, topRight, bottomRight, bottomLeft) = corners 37 | # convert each of the (x, y)-coordinate pairs to integers 38 | topRight = (int(topRight[0]), int(topRight[1])) 39 | bottomRight = (int(bottomRight[0]), int(bottomRight[1])) 40 | bottomLeft = (int(bottomLeft[0]), int(bottomLeft[1])) 41 | topLeft = (int(topLeft[0]), int(topLeft[1])) 42 | 43 | cv2.line(image, topLeft, topRight, (0, 255, 0), 2) 44 | cv2.line(image, topRight, bottomRight, (0, 255, 0), 2) 45 | cv2.line(image, bottomRight, bottomLeft, (0, 255, 0), 2) 46 | cv2.line(image, bottomLeft, topLeft, (0, 255, 0), 2) 47 | # compute and draw the center (x, y)-coordinates of the ArUco 48 | # marker 49 | cX = int((topLeft[0] + bottomRight[0]) / 2.0) 50 | cY = int((topLeft[1] + bottomRight[1]) / 2.0) 51 | cv2.circle(image, (cX, cY), 4, (0, 0, 255), -1) 52 | # draw the ArUco marker ID on the image 53 | cv2.putText(image, str(markerID),(topLeft[0], topLeft[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 54 | 0.5, (0, 255, 0), 2) 55 | print("[Inference] ArUco marker ID: {}".format(markerID)) 56 | # show the output image 57 | return image -------------------------------------------------------------------------------- /pose_estimation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Sample Usage:- 3 | python pose_estimation.py --K_Matrix calibration_matrix.npy --D_Coeff distortion_coefficients.npy --type DICT_5X5_100 4 | ''' 5 | 6 | 7 | import numpy as np 8 | import cv2 9 | import sys 10 | from utils import ARUCO_DICT 11 | import argparse 12 | import time 13 | 14 | 15 | def pose_esitmation(frame, aruco_dict_type, matrix_coefficients, distortion_coefficients): 16 | 17 | ''' 18 | frame - Frame from the video stream 19 | matrix_coefficients - Intrinsic matrix of the calibrated camera 20 | distortion_coefficients - Distortion coefficients associated with your camera 21 | 22 | return:- 23 | frame - The frame with the axis drawn on it 24 | ''' 25 | 26 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 27 | cv2.aruco_dict = cv2.aruco.Dictionary_get(aruco_dict_type) 28 | parameters = cv2.aruco.DetectorParameters_create() 29 | 30 | 31 | corners, ids, rejected_img_points = cv2.aruco.detectMarkers(gray, cv2.aruco_dict,parameters=parameters, 32 | cameraMatrix=matrix_coefficients, 33 | distCoeff=distortion_coefficients) 34 | 35 | # If markers are detected 36 | if len(corners) > 0: 37 | for i in range(0, len(ids)): 38 | # Estimate pose of each marker and return the values rvec and tvec---(different from those of camera coefficients) 39 | rvec, tvec, markerPoints = cv2.aruco.estimatePoseSingleMarkers(corners[i], 0.02, matrix_coefficients, 40 | distortion_coefficients) 41 | # Draw a square around the markers 42 | cv2.aruco.drawDetectedMarkers(frame, corners) 43 | 44 | # Draw Axis 45 | cv2.aruco.drawAxis(frame, matrix_coefficients, distortion_coefficients, rvec, tvec, 0.01) 46 | 47 | return frame 48 | 49 | if __name__ == '__main__': 50 | 51 | ap = argparse.ArgumentParser() 52 | ap.add_argument("-k", "--K_Matrix", required=True, help="Path to calibration matrix (numpy file)") 53 | ap.add_argument("-d", "--D_Coeff", required=True, help="Path to distortion coefficients (numpy file)") 54 | ap.add_argument("-t", "--type", type=str, default="DICT_ARUCO_ORIGINAL", help="Type of ArUCo tag to detect") 55 | args = vars(ap.parse_args()) 56 | 57 | 58 | if ARUCO_DICT.get(args["type"], None) is None: 59 | print(f"ArUCo tag type '{args['type']}' is not supported") 60 | sys.exit(0) 61 | 62 | aruco_dict_type = ARUCO_DICT[args["type"]] 63 | calibration_matrix_path = args["K_Matrix"] 64 | distortion_coefficients_path = args["D_Coeff"] 65 | 66 | k = np.load(calibration_matrix_path) 67 | d = np.load(distortion_coefficients_path) 68 | 69 | video = cv2.VideoCapture(0) 70 | time.sleep(2.0) 71 | 72 | while True: 73 | ret, frame = video.read() 74 | 75 | if not ret: 76 | break 77 | 78 | output = pose_esitmation(frame, aruco_dict_type, k, d) 79 | 80 | cv2.imshow('Estimated Pose', output) 81 | 82 | key = cv2.waitKey(1) & 0xFF 83 | if key == ord('q'): 84 | break 85 | 86 | video.release() 87 | cv2.destroyAllWindows() -------------------------------------------------------------------------------- /calibration.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Sample Usage:- 3 | python calibration.py --dir calibration_checkerboard/ --square_size 0.024 4 | ''' 5 | 6 | import numpy as np 7 | import cv2 8 | import os 9 | import argparse 10 | 11 | 12 | def calibrate(dirpath, square_size, width, height, visualize=False): 13 | """ Apply camera calibration operation for images in the given directory path. """ 14 | 15 | # termination criteria 16 | criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 17 | 18 | # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(8,6,0) 19 | objp = np.zeros((height*width, 3), np.float32) 20 | objp[:, :2] = np.mgrid[0:width, 0:height].T.reshape(-1, 2) 21 | 22 | objp = objp * square_size 23 | 24 | # Arrays to store object points and image points from all the images. 25 | objpoints = [] # 3d point in real world space 26 | imgpoints = [] # 2d points in image plane. 27 | 28 | images = os.listdir(dirpath) 29 | 30 | for fname in images: 31 | img = cv2.imread(os.path.join(dirpath, fname)) 32 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 33 | 34 | # Find the chess board corners 35 | ret, corners = cv2.findChessboardCorners(gray, (width, height), None) 36 | 37 | # If found, add object points, image points (after refining them) 38 | if ret: 39 | objpoints.append(objp) 40 | 41 | corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) 42 | imgpoints.append(corners2) 43 | 44 | # Draw and display the corners 45 | img = cv2.drawChessboardCorners(img, (width, height), corners2, ret) 46 | 47 | if visualize: 48 | cv2.imshow('img',img) 49 | cv2.waitKey(0) 50 | 51 | 52 | ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) 53 | 54 | return [ret, mtx, dist, rvecs, tvecs] 55 | 56 | 57 | if __name__ == '__main__': 58 | ap = argparse.ArgumentParser() 59 | ap.add_argument("-d", "--dir", required=True, help="Path to folder containing checkerboard images for calibration") 60 | ap.add_argument("-w", "--width", type=int, help="Width of checkerboard (default=9)", default=9) 61 | ap.add_argument("-t", "--height", type=int, help="Height of checkerboard (default=6)", default=6) 62 | ap.add_argument("-s", "--square_size", type=float, default=1, help="Length of one edge (in metres)") 63 | ap.add_argument("-v", "--visualize", type=str, default="False", help="To visualize each checkerboard image") 64 | args = vars(ap.parse_args()) 65 | 66 | dirpath = args['dir'] 67 | # 2.4 cm == 0.024 m 68 | # square_size = 0.024 69 | square_size = args['square_size'] 70 | 71 | width = args['width'] 72 | height = args['height'] 73 | 74 | if args["visualize"].lower() == "true": 75 | visualize = True 76 | else: 77 | visualize = False 78 | 79 | ret, mtx, dist, rvecs, tvecs = calibrate(dirpath, square_size, visualize=visualize, width=width, height=height) 80 | 81 | print(mtx) 82 | print(dist) 83 | 84 | np.save("calibration_matrix", mtx) 85 | np.save("distortion_coefficients", dist) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArUCo-Markers-Pose-Estimation-Generation-Python 2 | 3 | This repository contains all the code you need to generate an ArucoTag, 4 | detect ArucoTags in images and videos, and then use the detected tags 5 | to estimate the pose of the object. In addition to this, I have also 6 | included the code required to obtain the calibration matrix for your 7 | camera. 8 | 9 | 10 | 11 | ## 1. ArUCo Marker Generation 12 | The file `generate_aruco_tags.py` contains the code for ArUCo Marker Generation. 13 | You need to specify the type of marker you want to generate. 14 | 15 | The command for running is :- 16 | `python generate_aruco_tags.py --id 24 --type DICT_5X5_100 --output tags/` 17 | 18 | You can find more details on other parameters using `python generate_aruco_tags.py --help` 19 | 20 | ## 2. ArUCo Marker Detection 21 | The files `detect_aruco_images.py` and `detect_aruco_video.py` contains the code for detecting 22 | ArUCo Markers in images and videos respectively. You need to specify the path to the image or 23 | video file and the type of marker you want to detect. 24 | 25 | The command for running is :- 26 | **For inference on images** 27 | `python detect_aruco_images.py --image Images/test_image_1.png --type DICT_5X5_100` 28 | **For inference using webcam feed** 29 | `python detect_aruco_video.py --type DICT_5X5_100 --camera True ` 30 | **For inference using video file** 31 | `python detect_aruco_video.py --type DICT_5X5_100 --camera False --video test_video.mp4` 32 | 33 | You can find more details on other parameters using `python detect_aruco_images.py --help` 34 | and `python detect_aruco_video.py --help` 35 | 36 | ## 3. Calibration 37 | The file `calibration.py` contains the code necessary for calibrating your camera. This step 38 | has several pre-requisites. You need to have a folder containing a set of checkerboard images 39 | taken using your camera. Make sure that these checkerboard images are of different poses and 40 | orientation. You need to provide the path to this directory and the size of the square in metres. 41 | You can also change the shape of the checkerboard pattern using the parameters given. Make sure this 42 | matches with your checkerboard pattern. This code will generate two numpy files `calibration_matrix.npy` and `distortion_coefficients.npy`. These files are required to execute the next step that involves pose estimation. 43 | Note that the calibration and distortion numpy files given in my repository is obtained specifically for my camera 44 | and might not work well for yours. 45 | 46 | The command for running is :- 47 | `python calibration.py --dir calibration_checkerboard/ --square_size 0.024` 48 | 49 | You can find more details on other parameters using `python calibration.py --help` 50 | 51 | ## 4. Pose Estimation 52 | The file `pose_estimation.py` contains the code that performs pose estimation after detecting the 53 | ArUCo markers. This is done in real-time for each frame obtained from the web-cam feed. You need to specify 54 | the path to the camera calibration matrix and distortion coefficients obtained from the previous step as well 55 | as the type for ArUCo marker you want to detect. Note that this code could be easily modified to perform 56 | pose estimation on images and video files. 57 | 58 | The command for running is :- 59 | `python pose_estimation.py --K_Matrix calibration_matrix.npy --D_Coeff distortion_coefficients.npy --type DICT_5X5_100` 60 | 61 | 62 | You can find more details on other parameters using `python pose_estimation.py --help` 63 | 64 | ## Output 65 | 66 | 67 | 68 | 69 | 70 | ### Notes 71 | The `utils.py` contains the ArUCo Markers dictionary and the other utility function to display the detected markers. 72 | 73 | Feel free to reach out to me in case of any issues. 74 | If you find this repo useful in any way please do star ⭐️ it so that others can reap it's benefits as well. 75 | 76 | Happy Learning! Keep chasing your dreams! 77 | 78 | ## References 79 | 1. https://docs.opencv.org/4.x/d9/d6d/tutorial_table_of_content_aruco.html 80 | 2. https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html 81 | --------------------------------------------------------------------------------