단안 카메라의 내부 파라미터왜곡를 구하고, 왜곡 보정(undistortion)을 포함한 코드 정리 일반적인 Pinhole Camera 모델의 캘리브레이션 코드.

Camera Calibration

카메라 캘리브레이션(Camera Calibration)

✔ 카메라 내부 파라미터 값을 구하는 과정

✔ 실세계 3D 점이 2D 영상에 투영되기까지의 필요한 파라미터들 중 내부 요인의 파라미터 값을 구하는 과정

✔ 통상적으로 카메라 내부 파라미터왜곡를 구하는 것을 말한다

다크프로그래머

3차원 점들이 영상에 투영된 위치를 구하거나 역으로 영상좌표로부터 3차원 공간좌표를 복원할 때에는 이러한 내부 요인을 제거해야만 정확한 계산이 가능해집니다. 그리고 이러한 내부 요인의 파라미터 값을 구하는 과정을 카메라 캘리브레이션이라 부른다.


원본 링크

카메라 내부 파라미터

  • = 내부 파라미터 (Intrinsic Parameter)

  • , = 초점거리(focal length)
  • , = 주점(principal point)
  • = 비대칭계수(skew coefficient)

요즘 카메라들은 skew 에러가 거의 없기 때문에 카메라 모델에서 보통 비대칭 계수까지는 고려하지 않는다 (즉, skew_c = 0). 또한 현대의 일반적인 카메라는 가로방향 셀 간격과 세로방향 셀 간격의 차이가 없기 때문에 f = fx = fy라 놓아도 무방함.

원본 링크

Pinhole Camera Calibration

왜곡 모델 및 특징

  • 일반적인 카메라 모델로서 왜곡은 방사 왜곡(radial distortion)과 접선 왜곡(tangential distortion)을 고려함.
  • 방사 왜곡: 렌즈의 중심에서 멀어질수록 이미지가 더 많이 왜곡되는 현상.
  • 접선 왜곡: 렌즈와 이미지 센서의 정렬 불일치로 인해 발생하는 왜곡.
  • 왜곡 계수는 k1, k2, k3, p1, p2 등의 파라미터로 표현됨.
  • 대부분의 일반적인 렌즈 (표준, 광각, 줌 렌즈)에 적합.
  • OpenCV에서는 Fisheye Camera 모델에 최적화된 캘리브레이션 함수를 따로 제공함 ((OpenCV) Fisheye Camera Calibration).

Python code

  • tips) 체스보드가 잘 보임에도 불구하고 findChessboardCorners 가 잘 동작안하는 경우가 있다. findChessboardCornersSB 함수를 이용하면 노이즈에 강인하며 cornerSubPix 보다 더 정확한 결과를 얻을 수 있다.
import cv2 as cv
import numpy as np
import time
import os, glob
 
chess_width = 7  # 가로 코너 개수 (칸수 - 1)
chess_height = 10  # 세로 코너 개수 (칸수 - 1)
image_dir = "./data/camera/"  # 다양한 각도에서 체스보드가 촬영된 이미지 폴더 경로 (최소 10 ~ 20장)
image_format = ".jpg"
 
# 이미지 리스트 불러오기
def loadImages(path, image_format):
    image_path = path + "*" + image_format
    image_list = glob.glob(image_path)
    image_num = len(image_list)
    print(f"[IMAGE LOADER] {image_num} Images are loaded.")
    if image_num > 0:
        return image_list
    else:
        return 0
 
 
# 캘리브레이션 결과 재투영 에러 계산
def reprojection_err(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
    mean_error = 0
    for i in range(len(objpoints)):
        imgpoints2, _ = cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2)/len(imgpoints2)
        mean_error += error
    return mean_error/len(objpoints)
 
 
# 카메라 캘리브레이션
def CameraCalibation(image_list):
    # (차원, 행, 열) 3개의 좌표값이 들어갈 행렬을 체스보드 코너 총 개수만큼 생성
    objp = np.zeros((1, chess_width * chess_height, 3), np.float32)
    # z=0 이고 코너점 사이 거리를 1이라 할 때 모든 코너의 x,y,z 좌표 값 생성 (실세계 좌표)
    objp[0,:,:2] = np.mgrid[0:chess_width, 0:chess_height].T.reshape(-1, 2)
 
    objpoints = [] # 각 체스보드 이미지마다 저장될 3d 좌표
    imgpoints = [] # 각 체스보드 이미지마다 저장될 2d 좌표
 
    # cornersubpix 종료 기준 설정
    criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
 
    for image_path in image_list:
        img = cv.imread(image_path)
        img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
        # 체스보드 코너 탐색
        ret, corners = cv.findChessboardCorners(img_gray, (chess_width, chess_height), 
		        cv.CALIB_CB_ADAPTIVE_THRESH + cv.CALIB_CB_FAST_CHECK + cv.CALIB_CB_NORMALIZE_IMAGE)
        
        if ret:
            # 찾은 코너 기반 더 정확한 코너 픽셀 검출
            corners2 = cv.cornerSubPix(img_gray, corners, (11, 11), (-1, -1), criteria)
            objpoints.append(objp)
            imgpoints.append(corners2)
            # draw corner points
            #img = cv.drawChessboardCorners(img, (chess_width, chess_height), corners2, ret)
            #cv.imshow("Corner Results", img)
            #cv.waitKey(0)
        else:
            # 코너 에러 이미지는 넘어감
            print(f"[CALIB] Finding Corner Error. Skip Image : {image_path}")
            
    cv.destroyAllWindows()
    print(f"[CALIB] Calibration Start. Total {len(objpoints)} Images are used.")
 
 
    start_t = time.time() # 시간 측정용
    # 캘리브레이션 (calibration)
    ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, 
										img_gray.shape[::-1], None, None)
    end_t = time.time() - start_t
    print(f"[CALIB] Calibration Done. {end_t:.2f} sec.")
 
    # 재투영 에러 계산 (reprojection error)
    # 일반적으로 low resolution camera에서, reprojection error가 0.2pixel 이내이면
    # camera calibration이 잘되었다고봄.
    error = reprojection_err(objpoints, imgpoints, rvecs, tvecs, mtx, dist)
    print(f"[CALIB] Re-Projection Error : {error:.5f}")
 
    # 카메라 내부 파라미터, 왜곡계수 행렬 반환
    return mtx, dist
 
 
# 이미지 왜곡 보정
def undistortion(image_list, mtx, dist):
    # 모든 이미지의 해상도가 동일한 경우
    sample = cv.imread(image_list[0])
    h, w = sample.shape[:2]
 
    # 왜곡 보정 후에 사용할 최적의 카메라 행렬 계산
    # 왜곡 보정 후 유효영역(ROI) 반환
    # alpha 값 0-1, 0에 가까울 수록 보정된 영역만을 반환한다.
    newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
    
    # 왜곡된 이미지의 각 픽셀을 보정된 이미지의 어디로 이동시켜야 할지 계산함.
    # 왜곡 보정을 위한 x, y 방향의 리매핑 테이블을 반환함
    mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
 
    for image_path in image_list:
        img = cv.imread(image_path)
        # 리매핑
        undistorted_img = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
		# 좌 원본, 우 보정
        new_img = cv.hconcat([img, undistorted_img])
        cv.imshow("Undistorted", new_img)
        cv.waitKey(0)
    
    cv.destroyAllWindows()
 
 
if __name__ == '__main__':
    image_list = loadImages(image_dir, image_format)
    if image_list == 0:
        print(f"[MAIN] No Images Process is Done.")
    else:
        mtx, dist = CameraCalibation(image_list)
        undistortion(image_list, mtx, dist)
 

Fisheye Camera Calibration Code: (OpenCV) Fisheye Camera Calibration