카메라 교정
실습 준비
- 실습 이미지 다운로드: fisheye01.jpg
- 실습 이미지 다운로드: fisheye02.jpg
- 실습 이미지 다운로드: fisheye03.jpg
- 실습 이미지 다운로드: fisheye04.jpg
어안 렌즈로 같은 체커보드를 각도를 바꿔 가며 촬영한 사진 4장을 사용.
import glob
import cv2 as cv
import numpy as np
from yeom import to_pil
파일 열기:
src = cv.imread("fisheye01.jpg")
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY) # 교차점 검출은 그레이스케일에서 수행
카메라 왜곡
카메라 왜곡(camera distortion)은 렌즈의 형태 때문에 직선이 휘어 보이는 현상.
- 배럴(barrel) 왜곡: 화면 중심이 볼록하게 부풀어 보임
- 핀쿠션(pincushion) 왜곡: 화면 중심이 오목하게 눌려 보임


왜곡을 바로잡는 방법은 크게 세 가지.
- 교정 패턴(calibration pattern): 크기를 아는 패턴을 여러 각도에서 촬영해 렌즈의 왜곡을 계산. 체커보드 패턴, 원형 패턴 등을 사용
- 기하학적 단서(geometric clues): 이미지 안의 직선과 소실점을 이용
- 딥러닝 기반: 여러 렌즈의 이미지를 학습한 모델로 보정. 사진 한 장으로도 가능하지만 학습된 모델이 필요
이번 시간에는 가장 널리 쓰이는 체커보드 패턴 방식을 사용.
체커보드 패턴 준비
- 구글에서
checkerboard pattern camera calibration으로 검색 (checker대신chess도 가능) - 체커보드 패턴 모음에서 내려받기 가능
- 인쇄하거나 모니터에 띄워 두고, 교정하려는 카메라로 각도를 바꿔 가며 여러 장 촬영

체커보드 패턴의 교차점 찾기
cv.findChessboardCorners()는 체커보드에서 검은 칸과 흰 칸이 만나는 내부 교차점을 찾음.fisheye01.jpg는 칸짜리 보드이고,내부 교차점은 개.(7, 10)대신(10, 7)도 보드를 90도 돌려 본 것으로 해석하여, 검출에 성공.
patternSize = 7, 10 # (한 행의 교차점 수, 행의 수)
flags = (
cv.CALIB_CB_ADAPTIVE_THRESH # 조명이 고르지 않아도 이진화
| cv.CALIB_CB_FAST_CHECK # 패턴이 없으면 빠르게 포기
| cv.CALIB_CB_NORMALIZE_IMAGE # 명암 대비를 보정한 뒤 검출
)
retval, corners = cv.findChessboardCorners(
gray,
patternSize,
flags=flags,
)
retval, corners.shape
(True, (70, 1, 2))
retval이 True이면 검출에 성공한 것이고, corners에는 개의 교차점 좌표가 담김.
교차점을 더 정확하게
cv.findChessboardCorners()가 찾은 초기 교차점 좌표를 cv.cornerSubPix()로 보정. 주변 밝기 변화를 이용해 픽셀보다 작은 단위까지 정밀화.
criteria = (
cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, # 두 조건 중 먼저 만족하면 종료
30, # 최대 반복 횟수
0.001, # 위치 변화가 이보다 작으면 종료
)
corners2 = cv.cornerSubPix(
gray, corners, (11, 11), (-1, -1), criteria
) # (11, 11)은 탐색 창의 반쪽 크기, (-1, -1)은 제외 영역 없음
img_corner = cv.drawChessboardCorners(
src.copy(), patternSize, corners2, retval
)
to_pil(img_corner)

교차점을 이은 선이 곧지 않고 바깥쪽으로 휘어 있음. 이 휘어진 정도가 곧 렌즈 왜곡의 크기.
모든 이미지에서 교차점 좌표 찾기
교정에는 여러 장의 사진이 필요. 사진마다 실제 패턴의 3차원 좌표(objpoints)와 이미지에서 찾은 2차원 좌표(imgpoints)를 짝지어 모음.
filenames = sorted(glob.glob("fisheye*.jpg")) # *은 "아무 글자들"
# 패턴 한 장의 3차원 기준 좌표. 평면이므로 z는 0
objp = np.zeros(
(1, patternSize[0] * patternSize[1], 3), np.float32
)
objp[0, :, :2] = np.mgrid[
0 : patternSize[0], 0 : patternSize[1]
].T.reshape(-1, 2)
objpoints = [] # 3차원 좌표
imgpoints = [] # 2차원 좌표
for fname in filenames:
gray = cv.imread(fname, cv.IMREAD_GRAYSCALE)
retval, corners = cv.findChessboardCorners(
gray, patternSize, flags=flags
)
if retval: # 교차점을 찾은 사진만 사용
objpoints.append(objp)
imgpoints.append(
cv.cornerSubPix(
gray, corners, (11, 11), (-1, -1), criteria
)
)
len(filenames), len(objpoints)
(4, 4)
objp에서는 1칸을 1로 설정. 따라서 tvecs의 이동 거리는 칸 단위. 실제 칸 크기를 밀리미터로 입력하면 이동 거리도 밀리미터 단위.
여기서는 교정 흐름 확인을 위해 4장만 사용. 실제 촬영에서는 보드가 화면의 여러 위치와 기울기에 나타나도록 사진을 충분히 수집. 검출에 실패한 사진은 retval이 False이므로 자동 제외.
카메라 교정
이 수업의 범위는 cv.calibrateCamera()를 이용한 일반 카메라 모델의 교정 절차. 왜곡이 매우 큰 어안 렌즈에는 전용 cv.fisheye 모델이 필요할 수 있다.
cv.calibrateCamera()는 모아 둔 좌표 대응으로 카메라의 특성과 렌즈 왜곡을 한 번에 계산.
ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
) # shape[::-1]로 (너비, 높이) 순서 전달
ret
1.1849587929426413
ret: 전체 재투영 오차의 RMS(픽셀). 작을수록 검출 교차점과 재투영 교차점이 가까움mtx: 카메라 행렬. 3차원 좌표를 2차원 이미지로 옮기는 변환dist: 렌즈의 왜곡 계수rvecs: 사진마다의 카메라 회전tvecs: 사진마다의 카메라 평행 이동
mtx
array([[3.78511664e+03, 0.00000000e+00, 1.52170852e+03],
[0.00000000e+00, 3.79341712e+03, 1.51555447e+03],
[0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
대각선의 두 값은 초점 거리, 마지막 열의 두 값은 렌즈 중심의 위치. 이미지가 이므로 렌즈 중심이 약 으로 화면 중앙에 가깝게 추정.
dist
array([[-7.49157957e-01, -7.30155632e-01, 1.94433065e-03,
6.78806602e-04, 2.31663304e+00]])
첫 방사 왜곡 계수는 음수로 추정되어 배럴 왜곡의 방향과 일치. 교정 품질은 계수 부호가 아니라 재투영 오차와 보정 결과로 확인.
재투영 오차 해석
ret은 작을수록 좋지만 모든 카메라에 공통인 합격 기준은 없다. 보정 결과가 좋지 않으면 촬영 위치와 각도를 다양하게 바꿔 다시 교정.
이미지에서 렌즈 왜곡 교정
구한 mtx와 dist로 같은 카메라로 찍은 이미지의 왜곡을 보정.
src = cv.imread("fisheye01.jpg")
h, w = src.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(
mtx, dist, (w, h), 1, (w, h)
) # 1은 원본 픽셀을 모두 남기는 설정
dst = cv.undistort(src, mtx, dist, None, newcameramtx)
roi
(148, 151, 2773, 2772)
roi는 보정 결과에서 빈 영역 없이 온전히 채워진 사각형 영역. (x, y, 너비, 높이) 순서이므로 여백을 잘라내려면 dst[y:y + 높이, x:x + 너비]로 잘라냄.
to_pil(dst)
결과 비교
교정 전에는 체커보드의 직선이 바깥쪽으로 휘어 보임.

교정 후에는 같은 직선이 곧게 펴짐.
