Robotics

[OpenCV] 1. 카메라 캘리브레이션 (Camera Calibration)

로보고니 2024. 11. 13. 20:26

서론

OpenCV를 사용하여 로봇 비전을 하기 전에 가장 먼저 해야 하는 작업이 있다. 바로 카메라 캘리브레이션 (Camera Calibration)이다. 이것은 말 그대로 카메라를 교정하는 것이다.

카메라 렌즈의 왜곡

우리가 카메라를 보면 보통 이미지가 왜곡되어 있다. 이것은 카메라의 원리가 핀홀 효과를 사용하기 때문에 주변부가 둥글게 왜곡되기 때문이다. 그리고 우리는 이것을 보정하여 오른쪽 이미지처럼 직선이 되도록 펼쳐줄 것이다. 그리고 이것을 위해 카메라의 내부 파라미터와 외부 파라미터를 보정할 것이다.


카메라 파라미터

카메라의 파라미터에는 크게 두 가지가 있다.

  1. 카메라 렌즈 시스템의 내부 파라미터(Internal parameters): 초점 거리 (focal length), 광학 중심 (optical center), 렌즈의 방사 왜곡 계수 (radial distortion coefficients of the lens)
  2. 외부 파라미터(External parameters): 이것은 일부 시계 좌표계에 대한 카메라의 방향, 회전, 이동을 나타낸다.

 

체커 보드

우리는 카메라 캘리브레이션을 위해서 체커 보드(checkerboard pattern)를 사용할 것이다. 해당 사이트에서 체커 보드 패턴을 만들고 간단히 출력할 수 있다.

https://markhedleyjones.com/projects/calibration-checkerboard-collection

출력한 체커보드

출력한 체커보드를 벽에 잘 고정하여 붙여준 뒤, 보정하고자 하는 카메라를 가지고 여러 구도로 체커 보드를 촬영한다.

이때, OpenCV를 사용하여 간단한 방법으로 이미지를 촬영하고 저장할 수 있다.

import cv2
import datetime

# 카메라 장치 열기
cap = cv2.VideoCapture(0)

# 영상 캡처 루프
while True:
    # 프레임 읽기
    ret, frame = cap.read()
    if not ret:
        print("카메라에서 프레임을 가져올 수 없습니다.")
        break

    # 프레임을 화면에 표시
    cv2.imshow("Video", frame)

    # 키 입력 대기
    key = cv2.waitKey(1) & 0xFF

    # 'a' 키를 누르면 프레임 캡처하여 저장
    if key == ord('a'):
        # 파일 이름에 현재 날짜와 시간을 추가하여 저장
        filename = datetime.datetime.now().strftime("./checkerboards/capture_%Y%m%d_%H%M%S.png")
        cv2.imwrite(filename, frame)
        print(f"{filename} 이미지가 저장되었습니다.")

    # 'q' 키를 누르면 종료
    elif key == ord('q'):
        break

# 자원 해제
cap.release()
cv2.destroyAllWindows()

이 코드를 실행하면 키보드에서 a를 누르면 이미지가 저장되고, q를 누르면 창이 닫히며 프로그램이 종료된다.

다양한 구도에서 이미지를 촬영하고 저장하면 된다. 나는 약 40장 정도 촬영했다.

다양한 구도의 체커 보드


Arducam UC-684 Camera parameter

내가 사용한 카메라는 아두캠 UC-684 카메라이다. 그리고 이것의 카메라 파라미터가 인터넷상에 없기 때문에 직접 카메라 캘리브레이션을 통해서 값을 구해야 한다.

 

위에서 언급한 방법으로 여러 장의 체커 보드를 촬영했다.

import cv2
import numpy as np
import os
import glob
import pickle

def calibrate_camera():
    # 체커보드의 차원 정의
    CHECKERBOARD = (7,10)  # 체커보드 행과 열당 내부 코너 수
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    
    # 각 체커보드 이미지에 대한 3D 점 벡터를 저장할 벡터 생성
    objpoints = []
    # 각 체커보드 이미지에 대한 2D 점 벡터를 저장할 벡터 생성
    imgpoints = [] 
    
    # 3D 점의 세계 좌표 정의
    objp = np.zeros((1, CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
    objp[0,:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
    
    # 주어진 디렉터리에 저장된 개별 이미지의 경로 추출
    images = glob.glob('./checkerboards/*.png')
    
    for fname in images:
        img = cv2.imread(fname)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        
        # 체커보드 코너 찾기
        ret, corners = cv2.findChessboardCorners(gray,
                                               CHECKERBOARD,
                                               cv2.CALIB_CB_ADAPTIVE_THRESH +
                                               cv2.CALIB_CB_FAST_CHECK +
                                               cv2.CALIB_CB_NORMALIZE_IMAGE)
        
        if ret == True:
            objpoints.append(objp)
            corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
            imgpoints.append(corners2)
            
            # 코너 그리기 및 표시
            img = cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
            cv2.imshow('img', img)
            cv2.waitKey(0)
    
    cv2.destroyAllWindows()
    
    # 카메라 캘리브레이션 수행
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints,
                                                      gray.shape[::-1], None, None)
    
    # 결과 출력
    print("Camera matrix : \n")
    print(mtx)
    print("\ndist : \n")
    print(dist)
    print("\nrvecs : \n")
    print(rvecs)
    print("\ntvecs : \n")
    print(tvecs)
    
    # 캘리브레이션 결과를 파일로 저장
    calibration_data = {
        'camera_matrix': mtx,
        'dist_coeffs': dist,
        'rvecs': rvecs,
        'tvecs': tvecs
    }
    
    with open('camera_calibration.pkl', 'wb') as f:
        pickle.dump(calibration_data, f)
    
    return calibration_data

def live_video_correction(calibration_data):
    mtx = calibration_data['camera_matrix']
    dist = calibration_data['dist_coeffs']
    
    cap = cv2.VideoCapture(0)
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        # 프레임 크기 가져오기
        h, w = frame.shape[:2]
        
        # 최적의 카메라 행렬 구하기
        newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
        
        # 왜곡 보정
        dst = cv2.undistort(frame, mtx, dist, None, newcameramtx)
        
        # ROI로 이미지 자르기
        x, y, w, h = roi
        if all(v > 0 for v in [x, y, w, h]):
            dst = dst[y:y+h, x:x+w]
        
        # 원본과 보정된 이미지를 나란히 표시
        original = cv2.resize(frame, (640, 480))
        corrected = cv2.resize(dst, (640, 480))
        combined = np.hstack((original, corrected))
        
        # 결과 표시
        cv2.imshow('Original | Corrected', combined)
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    # 이미 캘리브레이션 파일이 있는지 확인
    if os.path.exists('camera_calibration.pkl'):
        print("Loading existing calibration data...")
        with open('camera_calibration.pkl', 'rb') as f:
            calibration_data = pickle.load(f)
    else:
        print("Performing new camera calibration...")
        calibration_data = calibrate_camera()
    
    # 실시간 비디오 보정 실행
    print("Starting live video correction...")
    live_video_correction(calibration_data)

그리고 해당 코드를 작동하게 되면, 저장된 이미지들을 모두 불러와서 카메라 캘리브레이션을 시작하게 된다.

캘리브레이션의 과정은 아래와 같다.

체커 보드 카메라 캘리브레이션

체커 보더의 각 꼭지점에 마커가 생기고 각 마커를 연결하며 카메라의 왜곡 정도를 측정한다. 그리고 수학적으로 이를 계산하여 카메라 파라미터를 계산한 뒤 보정한다.

Arducam UC-684 Camera parameter

위의 결과가 아두캠 UC-684의 내부 카메라 행렬과 렌즈 왜곡 계수이다. 그리고 해당 값들을 가지고 카메라 캘리브레이션이 적용되어 카메라의 왜곡을 줄일 수 있게 된다.

카메라 캘리브레이션 된 결과


마무리

오늘은 체커 보드를 가지고 카메라 캘리브레이션을 진행했다. 카메라의 왜곡을 줄이고 정확한 이미지를 얻을 수 있기 때문에, 로봇 비전, 컴퓨터 비전을 위해서 가장 먼저 해야 하는 작업이다. 다음 글에서는 조정된 카메라를 가지고 아루코마커 (Aruco marker)를 가지고 카메라 속 객체의 위치 좌표를 반환하는 작업을 해볼 것이다.