제7부 센서·인지 파이프라인
7.3 카메라 모델·캘리브레이션·CameraInfo
7.3.1 이 장에서 배우는 것 / 선수 지식
카메라는 3D 세계를 2D 픽셀로 투영하는 장치이고, 그 투영을 수식으로 기술한 것이 카메라 모델이다. 이 모델의 파라미터를 모르면 이미지에서 3D 정보를 얻을 수 없다 — "저 사람이 3 m 앞에 있다"를 계산할 수 없다.
ROS에서 이 파라미터를 담는 것이 sensor_msgs/CameraInfo 다. 그런데 이 메시지는 필드가 많고 의미가 혼동을 일으킨다 — K와 P가 왜 둘 다 있는지, R이 무엇인지, 정류 전후에 무엇을 써야 하는지.
이 장은 그 필드들을 정확히 세우고, 캘리브레이션 실무와 검증까지 다룬다. 투영 기하의 바탕이 되는 좌표변환·동차변환은 자매서 robotics(RB-21·RB-24)가 다루고, 카메라 모델 유도는 RB-242가 자리를 맡고 있다(RB의 집필 상태는 A.5.9), 여기서는 ROS에서 어떻게 다루는가에 집중한다.
배우는 것:
- 핀홀 모델 —
K행렬의 의미. - 왜곡 모델 —
D의 내용과 모델 종류. R과P— 정류와 스테레오에서의 역할.- 정류 전후 — 언제
K를 쓰고 언제P를 쓰는가. - 캘리브레이션 절차 — 실무 지침.
camera_info_manager— 파라미터 저장·배포.- 검증 — 캘리브레이션이 좋은지 어떻게 아는가.
선수 지식: 7.1.4(광학 프레임), 7.2(이미지 파이프라인·정류). 선형대수 기초를 가정한다.
정직: 왜곡 모델 이름과
D의 길이는sensor_msgs/distortion_models.hpp가 정본이며, Jazzy에서 직접 확인했다(실행 확인 — 7.3.11): 정의된 상수는plumb_bob·rational_polynomial·equidistant셋뿐이다. 캘리브레이션 도구의 인자 형식·수렴 기준은 확인하지 못했다 — 실제 카메라와 체커보드가 필요하다(목차 3).
7.3.2 핀홀 모델과 K
3D 점을 2D 픽셀로 투영하는 기본 모델이다.
$$ \begin{bmatrix} u' \\ v' \\ w' \end{bmatrix} = K \begin{bmatrix} X \\ Y \\ Z \end{bmatrix}, \qquad K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} $$
$$u = \frac{u'}{w'}, \qquad v = \frac{v'}{w'}$$
여기서 $(X, Y, Z)$는 광학 프레임 기준이다(7.1.4) — z가 앞, x가 오른쪽, y가 아래.
| 파라미터 | 의미 | 단위 |
|---|---|---|
| $f_x, f_y$ | 초점거리 | 픽셀 |
| $c_x, c_y$ | 주점(광축이 이미지와 만나는 점) | 픽셀 |
직관적 해석:
| 관찰 | 의미 |
|---|---|
| $f$가 크다 | 망원 — 좁은 시야, 물체가 크게 |
| $f$가 작다 | 광각 — 넓은 시야 |
| $c$가 이미지 중심 근처 | 정상(대략 width/2, height/2) |
| $f_x \ne f_y$ | 픽셀이 정사각이 아니거나 캘리브레이션 오차 |
시야각(FOV) 계산:
$$\text{FOV}_h = 2 \arctan\left(\frac{W}{2 f_x}\right)$$
견해:
K를 받으면 FOV를 계산해 상식과 맞는지 확인하라. 광각 카메라인데 30도가 나오면 캘리브레이션이 잘못됐거나 해상도가 다른 것이다. 이 한 줄 검산이 잘못된 캘리브레이션 오류를 즉시 검출한다.
역투영(픽셀 → 광선):
$$ \begin{bmatrix} X \\ Y \\ Z \end{bmatrix} = Z \begin{bmatrix} (u - c_x)/f_x \\ (v - c_y)/f_y \\ 1 \end{bmatrix} $$
깊이 $Z$를 알아야 3D 점이 정해진다. 단일 이미지로는 광선만 알 수 있고, 깊이는 스테레오·깊이 센서·물체 크기 가정에서 온다.
7.3.3 왜곡과 D
실제 렌즈는 핀홀 모델에서 벗어난다. 특히 광각에서 직선이 휘어 보인다.
plumb_bob(방사·접선 왜곡) — 가장 흔한 모델:
$$D = [k_1, k_2, p_1, p_2, k_3]$$
| 계수 | 종류 | 효과 |
|---|---|---|
| $k_1, k_2, k_3$ | 방사(radial) | 배럴·핀쿠션 왜곡 |
| $p_1, p_2$ | 접선(tangential) | 렌즈·센서 비평행 |
$$ \begin{aligned} x_d &= x(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + 2p_1 xy + p_2(r^2 + 2x^2) \\ y_d &= y(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + p_1(r^2 + 2y^2) + 2p_2 xy \end{aligned} $$
왜곡 모델 종류:
distortion_model | D 길이 | 용도 |
|---|---|---|
plumb_bob | 5 | 표준 렌즈 |
rational_polynomial | 8 | 왜곡이 큰 렌즈 |
equidistant | 4 | 어안 렌즈 — ROS 상수는 equidistant다. fisheye는 OpenCV 쪽 표현이며 sensor_msgs에는 없다(실행 확인) |
""(빈 문자열) | 0 | 왜곡 없음(이미 정류됨) |
함정: 어안 렌즈에
plumb_bob을 쓰면 캘리브레이션이 수렴하지 않거나 가장자리 오차가 크다. 시야각이 대략 120도를 넘으면 어안 모델을 검토해야 한다(확인 필요: 경계는 렌즈에 따라 다름).
함정:
D가 비어 있거나 전부 0이면 "왜곡 없음"으로 해석된다. 캘리브레이션을 안 했는데 이렇게 발행하면, 소비자가 정류가 필요 없다고 믿는다.
7.3.4 R과 P — 정류와 스테레오
여기가 CameraInfo에서 가장 혼동하기 쉬운 부분이다.
| 필드 | 크기 | 의미 |
|---|---|---|
K | 3×3 | 원시 이미지의 내부 파라미터 |
D | 가변 | 왜곡 계수 |
R | 3×3 | 정류 회전 — 단안은 단위행렬, 스테레오는 에피폴라 정렬 |
P | 3×4 | 정류된 이미지의 투영 행렬 |
$$ P = \begin{bmatrix} f'_x & 0 & c'_x & T_x \\ 0 & f'_y & c'_y & T_y \\ 0 & 0 & 1 & 0 \end{bmatrix} $$
핵심 규칙:
| 사용하는 이미지 | 사용할 파라미터 |
|---|---|
image_raw(원시) | K + D |
image_rect·image_rect_color(정류) | P(왜곡 없음) |
함정: 정류된 이미지에
K를 쓰는 것이 매우 흔한 실수다. 정류 과정에서 초점거리·주점이 바뀔 수 있으므로($f'_x \ne f_x$일 수 있음),K를 쓰면 미세하지만 체계적인 오차가 난다.image_rect_*를 쓴다면P의 왼쪽 3×3을 쓰라.
// 정류 이미지에서 역투영
const double fx = info->p[0]; // P[0][0]
const double fy = info->p[5]; // P[1][1]
const double cx = info->p[2]; // P[0][2]
const double cy = info->p[6]; // P[1][2]
// 왜곡 보정 불필요 — 이미 정류됨
스테레오에서 P의 T_x:
$$T_x = -f'_x \cdot B$$
여기서 $B$는 베이스라인(두 카메라 사이 거리, m). 왼쪽 카메라는 $T_x = 0$, 오른쪽 카메라가 음수 값을 갖는다.
시차(disparity)에서 깊이:
$$Z = \frac{f'_x \cdot B}{d}$$
함정:
T_x의 부호와 단위를 확인하라. 캘리브레이션 도구가 만든 값을 그대로 쓰면 되지만, 수동으로 채울 때 부호를 틀리면 깊이가 음수가 되거나 좌우가 뒤바뀐다.
binning과 roi:
| 필드 | 의미 |
|---|---|
binning_x·binning_y | 픽셀 묶기(해상도 축소) |
roi | 관심 영역(크롭) |
함정: 해상도를 바꾸면
K·P도 바뀌어야 한다. 1920×1080으로 캘리브레이션하고 960×540으로 발행하면 $f$와 $c$가 절반이 되어야 한다. 드라이버가 이를 자동으로 처리하지 않으면 모든 3D 계산이 2배 틀린다.binning·roi는 이것을 명시적으로 표현하는 수단이지만, 지원 여부는 소비자마다 다르다(확인 필요).
7.3.5 캘리브레이션 실무
# 체커보드 캘리브레이션 (ros-jazzy-camera-calibration 실재 — 실행 확인)
ros2 run camera_calibration cameracalibrator \
--size 8x6 --square 0.025 \
--ros-args -r image:=/camera/image_raw -p camera:=/camera
| 인자 | 의미 |
|---|---|
--size | 내부 코너 수(칸 수가 아님) |
--square | 한 칸의 크기(m) |
함정:
--size는 코너 수다. 9×7 칸 체커보드의 내부 코너는 8×6이다. 이것을 틀리면 검출이 안 되거나 스케일이 틀린다.
좋은 캘리브레이션을 위한 지침:
| 지침 | 이유 |
|---|---|
| 화면 전 영역을 덮는다 | 가장자리 왜곡을 보정하려면 |
| 다양한 거리 | 초점거리·주점 분리 |
| 다양한 기울기 | 파라미터 상관관계 해소 |
| 회전(보드를 회전) | 동일 |
| 충분한 샘플(40~60장) | 통계적 안정 |
| 선명한 이미지 | 흐리면 코너 검출 오차 |
| 조명 균일 | 그림자·반사 회피 |
도구가 보여주는 진행 막대(X·Y·Size·Skew)가 무엇이 부족한지 알려준다 — 전부 채워야 CALIBRATE 버튼이 활성화된다.
견해: 캘리브레이션 품질이 이후 모든 3D 계산의 상한을 정한다. 부정확하게 한 캘리브레이션으로 정밀한 물체 위치를 기대할 수 없다. 20분을 더 쓰는 것이 나중에 며칠을 아낀다.
함정: 캘리브레이션 보드가 평평해야 한다. 종이에 인쇄해 손에 들면 휘어서 오차가 크다. 평판에 붙이거나 인쇄된 보드를 쓰라. 그리고 인쇄 스케일을 실측하라 — 프린터가 정확히 100%로 출력하지 않는 경우가 있고,
--square값이 틀리면 모든 거리 계산이 비례해 틀린다.
결과 저장:
캘리브레이션 도구가 YAML을 만든다.
image_width: 1920
image_height: 1080
camera_name: front_camera
camera_matrix:
rows: 3
cols: 3
data: [1234.5, 0.0, 960.2, 0.0, 1235.1, 540.8, 0.0, 0.0, 1.0]
distortion_model: plumb_bob
distortion_coefficients:
rows: 1
cols: 5
data: [-0.31, 0.11, 0.001, -0.0005, 0.0]
rectification_matrix:
rows: 3
cols: 3
data: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
projection_matrix:
rows: 3
cols: 4
data: [1200.3, 0.0, 958.7, 0.0, 0.0, 1201.0, 539.2, 0.0, 0.0, 0.0, 1.0, 0.0]
7.3.6 camera_info_manager
드라이버가 캘리브레이션 파일을 읽어 CameraInfo를 발행하는 표준 방법이다.
#include "camera_info_manager/camera_info_manager.hpp"
cinfo_manager_ = std::make_shared<camera_info_manager::CameraInfoManager>(
this, "front_camera", "package://my_camera/config/front_camera.yaml");
if (!cinfo_manager_->isCalibrated()) {
RCLCPP_WARN(get_logger(),
"캘리브레이션 파일 없음 — 기본값 사용. 3D 계산이 부정확합니다.");
}
// 발행 시
auto info = cinfo_manager_->getCameraInfo();
info.header.stamp = stamp; // ★ 이미지와 같은 시각(7.2.9)
info.header.frame_id = optical_frame_; // ★ 광학 프레임(7.1.4)
cam_pub_.publish(image_msg, info);
| 기능 | 설명 |
|---|---|
| 파일 로드 | package://·file:// URL |
| 서비스 제공 | set_camera_info로 런타임 설정 |
| 검증 | 해상도 일치 확인 |
견해: 캘리브레이션이 없을 때 경고를 남기라. 조용히 기본값(단위 행렬 등)을 쓰면 3D 계산이 무의미한 값을 내는데, 그것이 그럴듯해 보여 나중에 발견된다. 시작 로그에 캘리브레이션 상태를 명시하는 것이 좋다(1.11.8).
함정: 캘리브레이션 파일이 패키지 설치 규칙에 없으면 배포본에서 사라진다(4.1.4).
install(DIRECTORY config ...)를 확인하라.
카메라마다 개별 파일:
| 상황 | 방법 |
|---|---|
| 같은 모델 여러 대 | 각각 캘리브레이션(개체차가 있다) |
| 파일 이름 | camera_name으로 구분 |
| 로봇마다 다름 | 로봇 ID를 경로에 |
견해: "같은 모델이니 캘리브레이션을 공유하자"는 유혹을 피하라. 렌즈 장착 편차·조립 공차로 개체차가 있으며, 정밀 작업에서 무시할 수 없다. 대략적 용도라면 공유해도 되지만, 그 결정을 의식적으로 하라.
7.3.7 검증
캘리브레이션이 좋은지 어떻게 아는가.
① 재투영 오차(reprojection error):
캘리브레이션 도구가 출력한다. 일반적으로 1픽셀 이하가 좋다고 여겨진다(확인 필요: 요구 정밀도에 따라 다름).
| 오차 | 해석 |
|---|---|
| < 0.5 px | 좋음 |
| 0.5~1.0 px | 대체로 사용 가능 |
| > 1.0 px | 재캘리브레이션 검토 |
정직: 재투영 오차가 낮다고 캘리브레이션이 반드시 좋은 것은 아니다 — 샘플이 편향되면 오차가 낮아도 일반화가 안 된다. 화면 전 영역·다양한 거리를 덮어야 하는 이유가 이것이다.
② 정류 결과 시각 확인 — 가장 직관적:
# 원시와 정류를 나란히
ros2 run rqt_image_view rqt_image_view /camera/image_raw
ros2 run rqt_image_view rqt_image_view /camera/image_rect_color
직선이 직선으로 보이는가. 문틀·책상 모서리·바닥 타일을 보고 확인한다. 원시에서 휘었던 직선이 정류 후 곧게 펴져야 한다.
③ 알려진 거리 측정:
1 m 앞에 물체를 두고 3D 위치를 계산 → 1 m가 나오는가
스케일 오차가 여기서 드러난다. 체커보드 칸 크기(--square)를 잘못 넣었으면 모든 거리가 비례해 틀린다.
④ 스테레오 검증:
| 확인 | 방법 |
|---|---|
| 에피폴라 정렬 | 정류 후 같은 물체가 두 이미지에서 같은 행에 |
| 베이스라인 | P[3] / -P[0] 가 실측과 맞는가 |
| 시차 맵 | 평면 벽이 평평하게 나오는가 |
⑤ 자동 검사:
# 개념: CameraInfo 검증
def check_camera_info(info):
warnings = []
fx, fy = info.k[0], info.k[4]
cx, cy = info.k[2], info.k[5]
if fx <= 0 or fy <= 0:
warnings.append('초점거리가 0 이하 — 캘리브레이션 없음')
if abs(fx - fy) / max(fx, fy) > 0.05:
warnings.append(f'fx({fx:.1f})와 fy({fy:.1f})가 5% 이상 차이')
if abs(cx - info.width / 2) > info.width * 0.15:
warnings.append(f'주점 cx가 중심에서 크게 벗어남: {cx:.1f}')
if all(d == 0.0 for d in info.d):
warnings.append('왜곡 계수가 전부 0 — 캘리브레이션 확인')
import math
fov = 2 * math.degrees(math.atan(info.width / (2 * fx)))
warnings.append(f'[정보] 수평 FOV ≈ {fov:.1f}도 — 렌즈 사양과 비교')
if info.header.frame_id and not info.header.frame_id.endswith('optical_frame'):
warnings.append(f'frame_id가 광학 프레임이 아닐 수 있음: {info.header.frame_id}')
return warnings
견해: 7.1.9의 센서 규약 검사와 함께 이 검사를 개발 도구로 두라. 새 카메라를 장착할 때 한 번 실행하면 명백한 오류가 즉시 드러난다.
7.3.8 캘리브레이션이 어긋나는 경우
캘리브레이션은 영구적이지 않다.
| 원인 | 영향 |
|---|---|
| 렌즈 초점 조정 | 초점거리 변경 → 전면 재캘리브레이션 |
| 충격·진동 | 렌즈·센서 정렬 변화 |
| 온도 변화 | 열팽창(정밀 응용에서) |
| 해상도·모드 변경 | K·P 스케일링 필요(7.3.4) |
| 렌즈 교체·재장착 | 전면 재캘리브레이션 |
| 스테레오 리그 변형 | 베이스라인·R 변화 |
함정: 오토포커스 카메라는 캘리브레이션이 계속 바뀐다. 로봇 비전에는 고정 초점을 쓰거나, 오토포커스를 끄고 고정한 뒤 캘리브레이션해야 한다.
견해: 정기 재캘리브레이션을 운영 절차에 넣으라. 그리고 재투영 오차 같은 지표를 진단으로 감시할 수 있으면 좋다(6.9) — 캘리브레이션이 어긋나면 3D 계산이 서서히 악화되는데, 이것을 감지할 수단이 없으면 원인 불명의 성능 저하가 된다.
스테레오는 특히 민감하다. 두 카메라의 상대 자세가 조금만 변해도 깊이가 크게 어긋난다. 리그가 강성이어야 하고, 온도·진동 환경에서는 정기 검증이 필요하다.
7.3.11 왜곡 모델 이름의 정본 — 실측
이 장이 (확인 필요: ROS에서의 정확한 이름)으로 남긴 항목이다. 정본은 sensor_msgs/distortion_models.hpp이고, CameraInfo.msg 주석이 직접 그 헤더를 가리킨다(실행 확인 — Jazzy · sensor_msgs 5.3.8).
정의된 상수는 셋뿐이다:
| 상수 | 문자열 | D 길이 |
|---|---|---|
PLUMB_BOB | "plumb_bob" | 5 — (k1, k2, t1, t2, k3) |
RATIONAL_POLYNOMIAL | "rational_polynomial" | 8 |
EQUIDISTANT | "equidistant" | 4 — 어안 |
fisheye라는 이름은 sensor_msgs에 없다. OpenCV의 어안 모듈(cv::fisheye)에서 온 표현이라 자료에 자주 보이지만, distortion_model 필드에 넣을 문자열은 equidistant다. 다른 값을 넣으면 소비자가 모델을 알아보지 못한다.
함정:
distortion_model은 자유 문자열이라 오타가 조용히 통과한다. 필드 검증이 없으므로"equidistant "(뒤 공백)나"fisheye"를 써도 발행은 되고, 소비하는 쪽에서 "모르는 모델"로 처리해 왜곡 보정을 건너뛴다. 결과는 "이미지가 미묘하게 휘어 있는데 에러는 없는" 상태다 —ros2 topic echo /camera/camera_info --field distortion_model로 실제 값을 확인하라.
참고: 캘리브레이션 도구는
ros-jazzy-camera-calibration으로 실재한다(실행 확인).
7.3.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 정류 이미지에 K 사용 | 체계적 3D 오차 | P 사용(7.3.4) |
| 2 | --size를 칸 수로 입력 | 검출 실패·스케일 오류 | 내부 코너 수 |
| 3 | --square 실측 안 함 | 모든 거리가 비례해 틀림 | 인쇄물 실측 |
| 4 | 보드가 휘어 있음 | 오차 증가 | 평판 부착 |
| 5 | 화면 일부만 덮음 | 가장자리 왜곡 미보정 | 전 영역 커버 |
| 6 | 어안에 plumb_bob | 수렴 실패·큰 오차 | 어안 모델 |
| 7 | 해상도 변경 후 그대로 | 3D 계산 배수 오차 | K·P 스케일링 |
| 8 | 오토포커스 사용 | 캘리브레이션이 계속 변함 | 고정 초점 |
| 9 | 같은 모델 캘리브레이션 공유 | 개체차 오차 | 개별 캘리브레이션 |
| 10 | 캘리브레이션 없이 조용히 동작 | 무의미한 3D 값 | 경고·시작 로그 |
| 11 | frame_id가 광학 프레임 아님 | 90도 회전(7.1.4) | _optical_frame |
| 12 | 이미지와 스탬프 불일치 | 짝 못 지음 | 같은 시각(7.2.9) |
| 13 | 캘리브레이션 파일 미설치 | 배포본에서 없음 | install(DIRECTORY config) |
| 14 | 재투영 오차만 보고 판단 | 편향된 샘플 | 시각·거리 검증 병행 |
| 15 | 왜곡 보정이 안 먹음 | distortion_model 문자열 오타(fisheye 등) | 7.3.11 |
7.3.10 체크포인트
K행렬의 네 파라미터와 각각의 의미·단위를 말할 수 있는가. FOV를 계산해 검산하는 방법은?- 픽셀에서 3D 광선을 얻는 식을 쓸 수 있는가. 왜 깊이가 별도로 필요한가.
plumb_bob의 다섯 계수를 방사·접선으로 구분할 수 있는가. 어안 렌즈에는 왜 다른 모델이 필요한가.K·D·R·P네 필드의 역할을 각각 말할 수 있는가.- 원시 이미지와 정류 이미지에 각각 무엇을 써야 하는가. 틀리면 어떤 오차가 나는가.
- 스테레오에서
P의T_x가 무엇이며, 시차에서 깊이를 얻는 식은? - 해상도를 바꾸면 왜
K·P도 바뀌어야 하는가. - 좋은 캘리브레이션을 위한 지침 일곱 가지를 들 수 있는가.
--size와--square의 흔한 실수는? camera_info_manager가 하는 일과, 캘리브레이션이 없을 때 경고를 남겨야 하는 이유는?- 캘리브레이션 검증 방법 다섯 가지를 말할 수 있는가. 재투영 오차만으로 판단하면 안 되는 이유는?
- 캘리브레이션이 어긋나는 원인 여섯 가지와, 오토포커스가 문제인 이유는?
- ROS가 정의하는 왜곡 모델 세 가지의 정확한 문자열은?
fisheye를 쓰면 무슨 일이 생기는가(7.3.11).
다음 장 7.4은 3D 데이터의 표준 형식 — PointCloud2를 다룬다. 필드 레이아웃, 반복자, PCL 연동, 그리고 크기가 만드는 성능 문제.