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

Fisheye Camera Calibration

왜곡 모델 및 특징

  • 일반적으로 화각 160도 이상의 렌즈 카메라를 Fisheye(어안) 카메라라고 표현함.
  • 어안 렌즈의 특징은 매우 넓은 시야각(180도 이상)으로 인해 왜곡이 더욱 극단적이다.
  • 왜곡 계수는 k1, k2, k3, k4의 방사 왜곡 4개 파라미터로 표현됨.
  • 접선 왜곡은 무시되며 방사 왜곡 중심으로 모델링됨.

OpenCV Fisheye Calibration

  • OpenCV 버전 3.0부터 fisheye 모듈이 추가됨.

  • Charuco 타겟 이미지를 사용하면 체스보드판과 달리 전체를 볼 필요가 없으므로 모서리 보정에 더욱 유용하다.

  • , 를 0으로 두기도 함.

  • ​: 2차 항의 방사 왜곡 계수. 가장 큰 영향을 미치며, 렌즈 중심에서의 기본적인 왜곡을 조정함.

  • ​: 4차 항의 방사 왜곡 계수. 왜곡의 비선형성을 보정하며, 중심에서 더 멀리 떨어진 부분에 영향을 준다.

  • ​: 6차 항의 방사 왜곡 계수. 왜곡 보정을 더욱 세밀하게 조정한다.

  • ​: 8차 항의 방사 왜곡 계수. 높은 차수의 왜곡 보정을 위한 추가적인 계수.

Python Code

import cv2 as cv
import numpy as np
import glob
 
 
chess_width = 6  # 가로 코너 개수 (칸수 - 1) 8 - 1.
chess_height = 8  # 세로 코너 개수 (칸수 - 1) 11 - 1.
  
# 다양한 각도에서 체스보드가 촬영된 이미지 폴더 경로 (최소 10 ~ 20장)
image_dir = "./data/fisheye/fisheye_images1/"
prefix = ".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 fisheye_reprojection_err(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
    mean_error = 0
    for i in range(len(objpoints)):
        # FOR FISHEYE
        imgpoints2, _ = cv.fisheye.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        imgpoints2 = np.reshape(imgpoints2, (len(imgpoints2[0]), 1, 2))
        error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2)/len(imgpoints2)
        mean_error += error
    
    return mean_error/len(objpoints)
 
 
# 카메라 캘리브레이션
def FisheyeCameraCalibation(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 좌표
 
            #img = cv.drawChess
    # 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)
 
            #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}")
    
    print(f"[CALIB] Calibration Start. Total {len(objpoints)} Images are used.")
        
    cv.destroyAllWindows()    
 
    # Fisheye 캘리브레이션 (calibration)
    rms, fmtx, fdist, frvecs, ftvecs = cv.fisheye.calibrate(
                            objpoints,
                            imgpoints,
                            img_gray.shape[::-1],
                            None,
                            None,
		#flags=cv.fisheye.CALIB_RECOMPUTE_EXTRINSIC+cv.fisheye.CALIB_CHECK_COND+cv.fisheye.CALIB_FIX_SKEW,
                            # criteria에서 반복 횟수에 따라 결과가 매우 달라짐. 적당한 반복횟수를 찾는 것이 중요.
                            criteria=(cv.TERM_CRITERIA_EPS+cv.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
                        )
    
    # 재투영 에러 계산 (reprojection error)
    # 일반적으로 low resolution camera에서, reprojection error가 0.2pixel 이내이면
    # camera calibration이 잘되었다고봄.
    fish_error = fisheye_reprojection_err(objpoints, imgpoints, frvecs, ftvecs, fmtx, fdist)
    print(f"[CALIB] Fisheye Re-Projection Error : {fish_error:.5f}")
    
    # 카메라 내부 파라미터, 왜곡계수 행렬 반환
    return fmtx, fdist
 
 
# 이미지 Fisheye 왜곡 보정
def fisheye_undistortion(image_list, mtx, dist, balance=0.0, dim2=None, dim3=None):
    results_images = []
    # 모든 이미지의 해상도가 동일한 경우
    sample = cv.imread(image_list[0])
 
    # dim2와 dim3은 입력 및 출력 이미지가 카메라 메트릭스와 다를 때 설정
    dim1 = sample.shape[:2][::-1]
    DIM = dim1
    
    assert dim1[0]/dim1[1] == DIM[0]/DIM[1], "Image to undistort needs to have same aspect ratio as the ones used in calibration"
    if not dim2:
         dim2 = dim1   
    if not dim3:
         dim3 = dim1
 
    scaled_K = mtx * dim1[0] / DIM[0]
    scaled_K[2][2] = 1.0 
 
    # 왜곡 보정 후에 사용할 최적의 카메라 행렬 계산
    # 왜곡 보정 후 유효영역(ROI) 반환
    new_K = cv.fisheye.estimateNewCameraMatrixForUndistortRectify(mtx, dist, dim2, np.eye(3), balance=balance)
    
    # 왜곡된 이미지의 각 픽셀을 보정된 이미지의 어디로 이동시켜야 할지 계산함.
    # 왜곡 보정을 위한 x, y 방향의 리매핑 테이블을 반환함
    mapx, mapy = cv.fisheye.initUndistortRectifyMap(mtx, dist, np.eye(3), new_K, dim3, cv.CV_16SC2)
 
 
    for image_path in image_list:
        img = cv.imread(image_path)
        # 리매핑
        undistorted_img = cv.remap(img, mapx, mapy,
    				interpolation=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT)
        
        results_images.append(undistorted_img)
 
        new_img = cv.hconcat([img, undistorted_img])
        cv.imshow("Undistorted", new_img)
        cv.waitKey(0)
    
    cv.destroyAllWindows()
 
    return results_images
 
 
if __name__ == '__main__':
    image_list = loadImages(image_dir, prefix)
    if image_list == 0:
        print(f"[MAIN] No Images Process is Done.")
    else:
        fmtx, fdist = FisheyeCameraCalibation(image_list)
        fund_images = fisheye_undistortion(image_list, fmtx, fdist)
  • cv.fisheye.estimateNewCameraMatrixForUndistortRectify() 함수에서 balance 값에 따른 이미지 크기 비교

참고