AEGLOSLAB ROS 2 Jazzy 대전

제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_modelD 길이용도
plumb_bob5표준 렌즈
rational_polynomial8왜곡이 큰 렌즈
equidistant4어안 렌즈 — ROS 상수는 equidistant다. fisheye는 OpenCV 쪽 표현이며 sensor_msgs에는 없다(실행 확인)
""(빈 문자열)0왜곡 없음(이미 정류됨)

함정: 어안 렌즈에 plumb_bob을 쓰면 캘리브레이션이 수렴하지 않거나 가장자리 오차가 크다. 시야각이 대략 120도를 넘으면 어안 모델을 검토해야 한다(확인 필요: 경계는 렌즈에 따라 다름).

함정: D가 비어 있거나 전부 0이면 "왜곡 없음"으로 해석된다. 캘리브레이션을 안 했는데 이렇게 발행하면, 소비자가 정류가 필요 없다고 믿는다.

7.3.4 R과 P — 정류와 스테레오

여기가 CameraInfo에서 가장 혼동하기 쉬운 부분이다.

정류(rectify) P로 투영 원시 이미지 K + D 적용 정류 이미지 왜곡 제거 3D 계산 R: 정류 회전
필드크기의미
K3×3원시 이미지의 내부 파라미터
D가변왜곡 계수
R3×3정류 회전 — 단안은 단위행렬, 스테레오는 에피폴라 정렬
P3×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 값경고·시작 로그
11frame_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 연동, 그리고 크기가 만드는 성능 문제.