대상: 로봇 비전, SLAM, AprilTag, ArUco 등에서 정확한 카메라 좌표를 얻고 싶은 개발자
환경: Ubuntu 20.04 + Python 3 + OpenCV 4.x
준비물: 인쇄한 체커보드 패턴 (흑백 격자), 카메라 (USB/CSI/웹캠 등)
1. 문제/주제 요약
카메라로 3D 공간을 인식하려면 왜곡 없는 정확한 이미지 좌표 → 3D 변환이 필수입니다.
하지만 대부분의 렌즈는 다음과 같은 왜곡(distortion) 을 가지고 있습니다 👇
- 직선이 휘어보이는 배럴 왜곡(barrel distortion)
- 중심부와 가장자리의 초점 차이
이를 보정하기 위한 과정이 카메라 캘리브레이션(Camera Calibration) 입니다.
즉, OpenCV로 K (Intrinsic)과 D (왜곡 계수)를 추정하는 단계입니다.
2. 원리 요약
OpenCV는 체커보드(Checkerboard) 이미지 여러 장을 이용해
3D-2D 점 대응 관계를 계산하고, 다음을 추정합니다:
- 내부 파라미터 (Intrinsic)
$$
K =
\begin{bmatrix}
f_x & 0 & c_x \
0 & f_y & c_y \
0 & 0 & 1
\end{bmatrix}
$$ - 왜곡 계수 (Distortion Coefficients)
$$
D = [k_1, k_2, p_1, p_2, k_3]
$$ - 외부 파라미터 (Extrinsic)
각 체커보드 사진에서의 회전($R$)과 이동($t$)
💡 이 과정을 통해 “이 카메라가 3D 세상에서 어떻게 이미지를 보는가”를 수학적으로 표현할 수 있습니다.
3. 준비 단계
(1) 체커보드 인쇄
- 일반적으로 9×6 코너(즉, 10×7 칸짜리 보드)가 많이 사용됩니다.
- 흑백 패턴을 A4 용지에 인쇄하고, 평평한 판자에 부착합니다.
- 패턴 PDF 예시: OpenCV 공식 패턴
⚙️ 코너 수 설정 시 주의:
9x6체커보드는 “코너 점이 9×6개”지, 사각형이 9×6개가 아닙니다.
즉, OpenCV에 전달할 코너 수는(9, 6)입니다.
4. 캘리브레이션 코드 예제
📂 camera_calibration.py
import cv2
import numpy as np
import glob
# 1️⃣ 체커보드 내부 코너 개수
CHECKERBOARD = (9, 6)
# 2️⃣ 체커보드의 실제 각 사각형 한 변의 길이 (예: 25mm)
square_size = 0.025 # 단위: m
# 3️⃣ 체커보드 3D 좌표 준비
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp *= square_size
# 4️⃣ 저장용 배열
objpoints = [] # 3D 점 (실세계)
imgpoints = [] # 2D 점 (이미지)
# 5️⃣ 캘리브레이션용 이미지 불러오기
images = glob.glob('./images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 6️⃣ 체커보드 코너 찾기
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners_sub = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1),
criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
imgpoints.append(corners_sub)
cv2.drawChessboardCorners(img, CHECKERBOARD, corners_sub, ret)
cv2.imshow('Corners', img)
cv2.waitKey(200)
cv2.destroyAllWindows()
# 7️⃣ 카메라 캘리브레이션 수행
ret, K, D, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
print("\n✅ Calibration complete!")
print("Intrinsic matrix (K):\n", K)
print("Distortion coefficients (D):\n", D)
# 8️⃣ 예시: 첫 이미지 undistort 테스트
img = cv2.imread(images[0])
h, w = img.shape[:2]
new_K, roi = cv2.getOptimalNewCameraMatrix(K, D, (w, h), 1, (w, h))
undistorted = cv2.undistort(img, K, D, None, new_K)
cv2.imshow('Original', img)
cv2.imshow('Undistorted', undistorted)
cv2.waitKey(0)
cv2.destroyAllWindows()
5. 촬영 가이드
✅ 캘리브레이션 시 주의사항
| 항목 | 설명 |
|---|---|
| 각도 다양성 | 체커보드를 다양한 각도·위치에서 촬영 (평면, 비스듬히 등) |
| 조명 균일 | 반사나 그림자 최소화 |
| 프레임 수 | 최소 10장 이상 (보통 15~20장 권장) |
| 해상도 | 가능한 한 고해상도 유지 (너무 낮으면 코너 검출 실패) |
6. 결과 예시
출력 예시:
Intrinsic matrix (K):
[[625.4 0. 315.2]
[ 0. 624.9 240.3]
[ 0. 0. 1. ]]
Distortion coefficients (D):
[[-0.332, 0.124, 0.002, -0.001, 0.0]]
보정된 이미지:
- 왼쪽: 원본 (직선이 휘어 있음)
- 오른쪽: 보정 후 (직선 유지)
7. ROS와 연동하기
OpenCV에서 구한 값을 ROS 카메라 파라미터로 변환할 수 있습니다.
camera_info.yaml 예시:
image_width: 640
image_height: 480
camera_name: usb_cam
camera_matrix:
rows: 3
cols: 3
data: [625.4, 0.0, 315.2, 0.0, 624.9, 240.3, 0.0, 0.0, 1.0]
distortion_model: plumb_bob
distortion_coefficients:
rows: 1
cols: 5
data: [-0.332, 0.124, 0.002, -0.001, 0.0]
rectification_matrix:
rows: 3
cols: 3
data: [1, 0, 0, 0, 1, 0, 0, 0, 1]
projection_matrix:
rows: 3
cols: 4
data: [625.4, 0.0, 315.2, 0.0, 0.0, 624.9, 240.3, 0.0, 0.0, 0.0, 1.0, 0.0]
💡
camera_info_publisher노드나image_proc에서 이 YAML을 불러오면
RViz나 SLAM 노드가 자동으로 보정된 카메라 모델을 사용합니다.
8. 자주 하는 실수 & 해결 팁
| 증상 | 원인 | 해결 |
|---|---|---|
| “체커보드 코너 찾기 실패” | 조명 불균일, 각도 부족 | 여러 각도, 거리에서 재촬영 |
| 보정 후 이미지가 더 왜곡됨 | 코너 수 잘못 지정 | (9,6) 확인 (코너 개수 기준) |
| Undistort 결과가 잘림 | alpha=1로 getOptimalNewCameraMatrix 사용 | |
| 3D 포인트 스케일이 이상함 | square_size 단위 오류 | 실제 mm→m 단위 일치 필요 |
| ROS에서 좌표가 뒤집힘 | frame_id 불일치 | TF(camera_link) 확인 |
9. 정리
| 항목 | 의미 | 예시 |
|---|---|---|
| Intrinsic (K) | 카메라 내부 파라미터 | 초점 거리, 중심 좌표 |
| Distortion (D) | 렌즈 왜곡 계수 | $$k_1, k_2, p_1, p_2, k_3$$ |
| Extrinsic (R, t) | 카메라 위치/자세 | TF(base_link → camera_link) |
| Calibration | 체커보드로 K, D 추정 | cv2.calibrateCamera() |
| Undistortion | 보정 영상 출력 | cv2.undistort() |
📌 요약
- 체커보드 코너를 여러 장에서 검출 →
K,D계산 - 왜곡 없는 이미지로 보정 → SLAM, ArUco, Depth 정밀도 향상
- 결과는 ROS
camera_info.yaml에 그대로 활용 가능