단안 카메라의 내부 파라미터와 왜곡를 구하고, 왜곡 보정(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
값에 따른 이미지 크기 비교
참고
- Calibrate fisheye lens using OpenCV — part 1 | by Kenneth Jiang | Medium
- Calibrate fisheye lens using OpenCV — part 2 | by Kenneth Jiang | Medium
- fisheye_calibration/fisheye_calibration.cpp at main · 4rtur1t0/fisheye_calibration · GitHub
- Camera calibration for fisheye lense - OpenCV
- 다크 프로그래머 :: 카메라 왜곡보정 - 이론 및 실제