Robotics

[OpenCV] 2. 아루코마커 인식하기 (ArUco marker)

로보고니 2024. 11. 14. 21:53

서론

오늘은 OpenCV로 아루코마커 (ArUco marker)를 인식하는 것을 해보려고 한다. 아루코마커란 로봇 비전 혹은 컴퓨터 비전에서 많이 사용하는 마커이다. 마치 QR 코드처럼 우리가 카메라로 아루코마커를 인식하면 그 아루코마커가 가지고 있는 ID를 반환받아서 읽을 수 있다.

 

우리가 아루코마커를 사용하는 이유는 간편하기 때문이다. 간단히 아루코마커를 출력해서 붙여두면, 특정 위치에서 아루코마커를 인식할 수 있다. 물론, 우리가 객체인식 모델을 가지고 있다면, 굳이 아루코마커를 사용하지 않더라도 원하는 것을 인식하고 활용할 수 있다. 하지만 그렇게 딥러닝 모델을 만드는 것보다는 간단히 아루코마커를 출력해서 사용하는 편이 좋다.


아루코마커 (ArUco marker)

아루코마커를 간단히 만들 수 있는 사이트가 있다. 원래라면 OpenCV에 있는 라이브러리를 활용해서 아루코마커를 직접 만들 수 있다. 하지만 아래 첨부한 링크에서 간단하게 아루코마커를 만들 수 있으며, 나는 ID가 0, 1, 2, 3인 4x4 아루코마커를 네 개 만들어서 출력했다.

 

Online ArUco markers generator

Dictionary: Original ArUco 4x4 (50, 100, 250, 1000) 5x5 (50, 100, 250, 1000) 6x6 (50, 100, 250, 1000) 7x7 (50, 100, 250, 1000) MIP_36h12 (250) AprilTag 16h5 (30) AprilTag 25h9 (35) AprilTag 36h10 (2320) AprilTag 36h11 (587) Marker ID: Marker size, mm:

chev.me


아루코마커 인식

아루코마커 인식

위의 화면에서 볼 수 있듯이, 간단하게 아루코마커를 인식할 수 있는 코드를 만들어봤다. 여기에는 아직 카메라 캘리브레이션이 적용되지 않은 상태라서 왜곡이 있다. 하지만 아래에 나올 화면부터는 카메라 캘리블레이션이 적용되어 왜곡 없는 화면을 볼 수 있다. ([OpenCV] 1. 카메라 캘리브레이션 (Camera Calibration))

 

위의 사진처럼 아루코마커를 인식하면 특정 ID를 반환받을 수 있다. 즉, 우리는 이것을 활용해서 카메라가 어디를 보고 있고, 아루코마커를 기준으로 얼마나 떨어져있는지 등을 알 수 있다.

 

아루코마커를 통한 위치 추정

앞서 말했듯이 우리는 아루코마커를 통해 위치와 각도를 추정할 수 있다. 인식한 아루코마커의 위치와 각도에 따라서 x, y, z축 방향으로의 위치와 회전 각도를 계산할 수 있다.

아루코마커를 통한 위치 추정

import cv2
import numpy as np
import os
import time
import pickle

def live_aruco_detection(calibration_data):
    """
    실시간으로 비디오를 받아 ArUco 마커를 검출하고 3D 포즈를 추정하는 함수
    
    Args:
        calibration_data: 카메라 캘리브레이션 데이터를 포함한 딕셔너리
            - camera_matrix: 카메라 내부 파라미터 행렬
            - dist_coeffs: 왜곡 계수
    """
    # 캘리브레이션 데이터 추출
    camera_matrix = calibration_data['camera_matrix']
    dist_coeffs = calibration_data['dist_coeffs']
    
    # ArUco 검출기 설정
    aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)
    aruco_params = cv2.aruco.DetectorParameters()
    detector = cv2.aruco.ArucoDetector(aruco_dict, aruco_params)
    
    # 마커 크기 설정 (미터 단위)
    marker_size = 0.05  # 예: 5cm = 0.05m
    
    # 카메라 설정
    cap = cv2.VideoCapture(0)
    
    # 카메라 초기화 대기
    time.sleep(2)
    
    while True:
        ret, frame = cap.read()
        if not ret:
            print("Failed to grab frame")
            break
            
        # 이미지 왜곡 보정
        frame_undistorted = cv2.undistort(frame, camera_matrix, dist_coeffs)
        
        # 마커 검출
        corners, ids, rejected = detector.detectMarkers(frame_undistorted)
        
        # 마커가 검출되면 표시 및 포즈 추정
        if ids is not None:
            # 검출된 마커 표시
            cv2.aruco.drawDetectedMarkers(frame_undistorted, corners, ids)
            
            # 각 마커의 포즈 추정
            rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
                corners, marker_size, camera_matrix, dist_coeffs
            )
            
            # 각 마커에 대해 처리
            for i in range(len(ids)):
                # 좌표축 표시
                cv2.drawFrameAxes(frame_undistorted, camera_matrix, dist_coeffs, 
                                rvecs[i], tvecs[i], marker_size/2)
                
                # 마커의 3D 위치 표시
                pos_x = tvecs[i][0][0]
                pos_y = tvecs[i][0][1]
                pos_z = tvecs[i][0][2]
                
                # 회전 벡터를 오일러 각도로 변환
                rot_matrix, _ = cv2.Rodrigues(rvecs[i])
                euler_angles = cv2.RQDecomp3x3(rot_matrix)[0]
                
                # 마커 정보 표시
                corner = corners[i][0]
                center_x = int(np.mean(corner[:, 0]))
                center_y = int(np.mean(corner[:, 1]))
                
                cv2.putText(frame_undistorted, 
                          f"ID: {ids[i][0]}", 
                          (center_x, center_y - 40), 
                          cv2.FONT_HERSHEY_SIMPLEX, 
                          0.5, (0, 0, 0), 2)
                          
                cv2.putText(frame_undistorted,
                          f"Pos: ({pos_x:.2f}, {pos_y:.2f}, {pos_z:.2f})m",
                          (center_x, center_y),
                          cv2.FONT_HERSHEY_SIMPLEX,
                          0.5, (0, 0, 0), 2)
                          
                cv2.putText(frame_undistorted,
                          f"Rot: ({euler_angles[0]:.1f}, {euler_angles[1]:.1f}, {euler_angles[2]:.1f})deg",
                          (center_x, center_y + 20),
                          cv2.FONT_HERSHEY_SIMPLEX,
                          0.5, (0, 0, 0), 2)
                
                # 코너 포인트 표시
                for point in corner:
                    x, y = int(point[0]), int(point[1])
                    cv2.circle(frame_undistorted, (x, y), 4, (0, 0, 255), -1)
        
        # 프레임 표시
        cv2.imshow('ArUco Marker Detection', frame_undistorted)
        
        # 'q' 키를 누르면 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    # 리소스 해제
    cap.release()
    cv2.destroyAllWindows()

def main():
    # 캘리브레이션 데이터 로드
    try:
        with open('camera_calibration.pkl', 'rb') as f:
            calibration_data = pickle.load(f)
        print("Calibration data loaded successfully")
    except FileNotFoundError:
        print("Error: Camera calibration file not found")
        return
    except Exception as e:
        print(f"Error loading calibration data: {e}")
        return
    
    print("Starting ArUco marker detection...")
    live_aruco_detection(calibration_data)

if __name__ == "__main__":
    main()

카메라 캘리브레이션을 한 뒤 아루코마커를 검출하면 더 정확한 값을 얻을 수 있다. 그리고 위의 코드는 카메라 캘리브레이션을 적용했기 때문에, 코드를 원활히 작동하기 위해서는 이전 글을 참고하기 바란다.

 

더 정교한 아루코마커를 통한 위치 추정

위에서 보여준 사진은 아직 아루코마커의 기준 길이를 적용하지 않았을 때이다. 하지만 아래에서 보이는 사진과 코드는 아루코마커의 크기를 적용하였다. 즉, 아루코마커가 50mm임을 기준으로 각 아루코마커가 카메라의 중심으로 얼마나 떨어져 있고 회전되어 있는지 확인할 수 있다. 물론 아주 정확하지는 않지만, 약간의 오차를 가지고 위치와 회전 각도를 받을 수 있다.

더 정교한 아루코마커를 통한 위치 추정

import cv2
import numpy as np
import os
import time
import pickle

def live_aruco_detection(calibration_data):
    """
    실시간으로 비디오를 받아 ArUco 마커를 검출하고 3D 포즈를 추정하는 함수
    
    Args:
        calibration_data: 카메라 캘리브레이션 데이터를 포함한 딕셔너리
            - camera_matrix: 카메라 내부 파라미터 행렬
            - dist_coeffs: 왜곡 계수
    """
    # 캘리브레이션 데이터 추출
    camera_matrix = calibration_data['camera_matrix']
    dist_coeffs = calibration_data['dist_coeffs']
    
    # ArUco 검출기 설정
    aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)
    aruco_params = cv2.aruco.DetectorParameters()
    detector = cv2.aruco.ArucoDetector(aruco_dict, aruco_params)
    
    # 마커 크기 및 3D 좌표 설정 (밀리미터 단위)
    marker_size = 50  # 35mm
    marker_3d_edges = np.array([
        [0, 0, 0],
        [0, marker_size, 0],
        [marker_size, marker_size, 0],
        [marker_size, 0, 0]
    ], dtype='float32').reshape((4, 1, 3))
    
    # 색상 정의
    blue_BGR = (255, 0, 0)
    
    # 카메라 설정
    cap = cv2.VideoCapture(0)
    
    # 카메라 초기화 대기
    time.sleep(2)
    
    while True:
        ret, frame = cap.read()
        if not ret:
            print("Failed to grab frame")
            break
            
        # 이미지 왜곡 보정
        frame_undistorted = cv2.undistort(frame, camera_matrix, dist_coeffs)
        
        # 마커 검출
        corners, ids, rejected = detector.detectMarkers(frame_undistorted)
        
        # 마커가 검출되면 표시 및 포즈 추정
        if corners:
            for corner in corners:
                # 코너 포인트 추출 및 표시
                corner = np.array(corner).reshape((4, 2))
                (topLeft, topRight, bottomRight, bottomLeft) = corner
                
                # 코너 포인트 좌표 변환
                topRightPoint = (int(topRight[0]), int(topRight[1]))
                topLeftPoint = (int(topLeft[0]), int(topLeft[1]))
                bottomRightPoint = (int(bottomRight[0]), int(bottomRight[1]))
                bottomLeftPoint = (int(bottomLeft[0]), int(bottomLeft[1]))
                
                # 코너 포인트 표시
                cv2.circle(frame_undistorted, topLeftPoint, 4, blue_BGR, -1)
                cv2.circle(frame_undistorted, topRightPoint, 4, blue_BGR, -1)
                cv2.circle(frame_undistorted, bottomRightPoint, 4, blue_BGR, -1)
                cv2.circle(frame_undistorted, bottomLeftPoint, 4, blue_BGR, -1)
                
                # PnP로 포즈 추정
                ret, rvec, tvec = cv2.solvePnP(
                    marker_3d_edges, 
                    corner, 
                    camera_matrix, 
                    dist_coeffs
                )
                
                if ret:
                    # 위치 및 회전 정보 계산
                    x = round(tvec[0][0], 2)
                    y = round(tvec[1][0], 2)
                    z = round(tvec[2][0], 2)
                    rx = round(np.rad2deg(rvec[0][0]), 2)
                    ry = round(np.rad2deg(rvec[1][0]), 2)
                    rz = round(np.rad2deg(rvec[2][0]), 2)
                    
                    # 위치 및 회전 정보 표시
                    pos_text = f"Pos: ({x}, {y}, {z})mm"
                    rot_text = f"Rot: ({rx}, {ry}, {rz})deg"
                    
                    cv2.putText(frame_undistorted, 
                              pos_text,
                              (int(topLeft[0]-10), int(topLeft[1]+10)),
                              cv2.FONT_HERSHEY_SIMPLEX,
                              0.5, (0, 0, 255), 2)
                              
                    cv2.putText(frame_undistorted,
                              rot_text,
                              (int(topLeft[0]-10), int(topLeft[1]+40)),
                              cv2.FONT_HERSHEY_SIMPLEX,
                              0.5, (0, 0, 255), 2)
                    
                    # 좌표축 표시
                    cv2.drawFrameAxes(frame_undistorted, camera_matrix, dist_coeffs,
                                    rvec, tvec, marker_size/2)
        
        # 프레임 표시
        cv2.imshow('ArUco Marker Detection', frame_undistorted)
        
        # 'q' 키를 누르면 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    # 리소스 해제
    cap.release()
    cv2.destroyAllWindows()

def main():
    # 캘리브레이션 데이터 로드
    try:
        with open('camera_calibration.pkl', 'rb') as f:
            calibration_data = pickle.load(f)
        print("Calibration data loaded successfully")
    except FileNotFoundError:
        print("Error: Camera calibration file not found")
        return
    except Exception as e:
        print(f"Error loading calibration data: {e}")
        return
    
    print("Starting ArUco marker detection...")
    live_aruco_detection(calibration_data)

if __name__ == "__main__":
    main()

마무리

오늘은 OpenCV를 가지고 아루코마커를 인식하고, 아루코마커의 위치와 각도를 반환하는 알고리즘을 만들어 보았다. 아루코마커를 사용함으로써 딥러닝 모델을 만들지 않더라도 쉽고 간단하게 객체의 위치나 각도를 알 수 있다는 장점이 있다. 그래서 이번 프로젝트에서 아루코마커를 기준으로 두고, 인식한 상자가 얼마나 떨어져 있고, 회전되어 있는지 알려주는 알고리즘을 만들 예정이다.