Files
fori_camera_ws/doc/camera_exposure_design_notes.md
Dongubak b1a38f36a3 Add camera exposure design notes and Q&A grounded in actual hardware specs
Design notes for multi-camera + LiDAR SLAM exposure tuning, plus a
question/answer pair that verifies the notes' assumptions (gain ceiling,
lens sweet spot, blur budget, 12-bit raw capture) against the actual
MV-CS016-10UC camera and VM0420MP5 lens datasheets and the driver source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 22:24:04 +09:00

38 KiB
Raw Permalink Blame History

멀티카메라 + LiDAR SLAM 노출 설계 노트

대상 시스템: 하드웨어 트리거 동기 카메라 3대 + LiDAR, 후처리(offline) SLAM 목적: 실내/실외 혼재 환경에서 포화(하얗게 뜸) 없이 SLAM 프론트엔드가 쓸 만한 이미지를 확보


0. 요약 (먼저 읽을 것)

항목 현재 권장
조리개 f/8 ~ f/11 f/4 (또는 f/2.8) — 실장착 렌즈 VM0420MP5 최대개방 F2.0 기준 1~2 stop 닫은 값, 회절 한계(f/5.1) 이내
노출 하한 100 µs 15 µs (MV-CS016-10UC 데이터시트 Standard exposure mode 하한 — 개체별 실측 권장)
노출 상한 5000 µs 블러 계산으로 결정 (우리 렌즈 4mm 기준 실측하면 0.61.3 ms, "보통 23 ms"는 광각 렌즈 가정 — 3.1절 참조)
게인 상한 16 dB 16.9 dB가 하드웨어 실측 상한(더 못 엶), 실사용 목표는 12 dB
측광 전체 평균 밝기 ROI + 퍼센타일, 이후 gradient 기반
타임스탬프 트리거 시각 트리거 + 지연 + 노출시간/2
저장 8-bit 12-bit raw + 프레임별 메타데이터

핵심 진단 세 가지:

  1. f/8f/11은 회절 한계를 23배 넘어섰습니다. 빛도 잃고 해상력도 잃는 이중 손해입니다.
  2. 조리개를 "가장 밝을 때" 기준으로 잡으면 실내를 물리적으로 커버할 수 없습니다. 뒤의 EV 계산으로 확인 가능합니다.
  3. 게인 실측 상한은 24 dB가 아니라 16.9 dB입니다 (카메라 데이터시트 및 camera_params_cam1/2/3.yaml 실측 주석으로 확인, MV-CS016-10UC 스펙상 0~17 dB). 아래 1.4절/3.2절/7.2절의 "24 dB" 시나리오는 이 카메라에서 재현 불가능하며, 16.9 dB 기준으로 재계산했습니다.

1. Stop(스톱) 계산법

1.1 정의

1 stop = 센서에 도달하는 빛의 양이 정확히 2배 또는 절반.

모든 노출 파라미터를 stop이라는 하나의 단위로 환산하면 서로 직접 더하고 뺄 수 있습니다.

stop = log2(빛의 양 비율)

1.2 파라미터별 환산식

노출 시간 (exposure time) — 빛의 양에 정비례

Δstop = log2(t2 / t1)
  • 100 µs → 5000 µs : log2(5000/100) = log2(50) = 5.64 stop

게인 (gain, dB) — 머신비전 카메라는 dB = 20·log10(선형배수) 규약을 씁니다

선형배수 = 10^(dB / 20)
Δstop    = dB / 6.02
  • 16 dB : 16 / 6.02 = 2.66 stop (선형 6.3배)
  • 24 dB : 24 / 6.02 = 3.99 stop (선형 15.8배)
  • 외우기: 6 dB ≈ 1 stop

조리개 (f-number, N) — 빛의 양이 1/N²에 비례

Δstop = 2 · log2(N_old / N_new)     ← 양수면 밝아짐
  • f/11 → f/4 : 2·log2(11/4) = 2·1.459 = 2.92 stop 밝아짐
  • f/8 → f/4 : 2·log2(8/4) = 2.00 stop 밝아짐
  • f/8 → f/11 : 2·log2(8/11) = -0.92 stop (약 1 stop 어두워짐)

표준 조리개 계열은 √2배씩 증가하며 각 단계가 정확히 1 stop입니다:

f/1.4  f/2  f/2.8  f/4  f/5.6  f/8  f/11  f/16

조도 (lux) — 그대로 비율의 log2

  • 직사광 100,000 lux vs 실내 300 lux : log2(100000/300) = 8.4 stop

1.3 현재 시스템의 stop 예산

노출:  100 → 5000 µs   =  5.64 stop
게인:  0   → 16 dB     =  2.66 stop
------------------------------------
합계 (조리개 고정 시)  =  8.30 stop  (약 315배)

실외 직사광 ↔ 일반 실내 차이가 8~9 stop이므로, 여유가 사실상 0입니다. 실내에서 노출 상한과 게인 상한을 동시에 최대로 써야 겨우 적정 노출이 되고, 이는 모션 블러와 노이즈가 동시에 최악인 지점입니다.

1.4 EV(Exposure Value)로 커버 범위 확인하기

Stop보다 실전적인 도구입니다. 장면 밝기를 EV100이라는 절대 숫자로 표현합니다.

적정 노출 조건식:

log2(N² / t) = EV_scene + g

  N = f-number
  t = 노출시간 [초]
  g = 게인 [stop] = dB / 6.02

이걸 뒤집으면, 주어진 세팅이 커버할 수 있는 장면 밝기가 나옵니다:

EV_scene = log2(N² / t) - g

참고 EV100 값:

장면 EV100
눈밭/수면 반사 직사광 16
맑은 날 직사광 15
흐린 날 실외 12~13
건물 그늘 / 해질녘 10~11
밝은 실내(사무실, 창가) 7~8
어두운 실내(창고, 복도) 5~6
지하주차장 3~5

현재 세팅(f/8, 1005000 µs, 016 dB) 커버 범위:

가장 밝은 쪽: t=100µs, g=0     → EV = log2(64/0.0001) - 0    = 19.29
가장 어두운쪽: t=5000µs, g=16dB → EV = log2(64/0.005) - 2.66  = 10.98

→ 커버 범위: EV 10.98 ~ 19.29

해석: 위쪽 EV 19.3은 지구상에 거의 없는 밝기입니다(직사광이 15). 즉 3~4 stop을 아무 쓸모없는 밝은 쪽에 낭비하고 있고, 아래쪽은 EV 11에서 끊겨서 실내(EV 58)에 46 stop 모자랍니다.

f/4로 열었을 때:

밝은 쪽: log2(16/0.0001)      = 17.29
어두운쪽: log2(16/0.005) - 2.66 = 8.98
→ EV 8.98 ~ 17.29

f/4 + 게인 상한 16.9 dB (실측 하드웨어 상한 — 24 dB는 이 카메라에서 불가능):

g = 16.9 / 6.02 = 2.81 stop
어두운쪽: log2(16/0.005) - 2.81 = 8.84
→ EV 8.84 ~ 17.29   ← 밝은 실내(EV 7~8) 상단에 겨우 걸침. 직사광은 커버하지만
                        어두운 실내(EV 5~6)·지하주차장(EV 3~5)은 이 조합만으론 못 미침

f/2.8 + 게인 상한 16.9 dB + 노출 상한 3 ms(블러 고려):

g = 16.9 / 6.02 = 2.81 stop
밝은 쪽: log2(7.84/0.0001)      = 16.26
어두운쪽: log2(7.84/0.003) - 2.81 = 8.55
→ EV 8.55 ~ 16.26

정정: 원래 초안은 게인 상한을 24 dB(3.99 stop)로 가정해 "f/4+24dB → EV 7.65부터 커버", "f/2.8+24dB+3ms → EV 7.71까지 커버"라고 결론 내렸었습니다. 하지만 이 카메라의 실측 게인 상한은 16.9 dB(2.81 stop)로 확인되어(0번 요약 및 3.2절 참조), 위 두 시나리오 모두 다크사이드 EV가 약 1.2 stop씩 나빠집니다. 즉 조리개를 열고 게인을 최대로 써도 "밝은 실내"(EV 78) 하단조차 안정적으로 커버하지 못하고, "어두운 실내"(EV 56)는 이 조합으로는 원천적으로 불가능합니다. 어두운 쪽 stop을 더 벌려면 게인이 아니라 더 밝은 렌즈(더 낮은 F No.) 또는 더 긴 노출 상한(3.1절의 실제 블러 한계 재계산 필요 — 아래 참조) 쪽에서 찾아야 합니다.

이 계산을 스프레드시트나 짧은 파이썬 스크립트로 만들어 두면, 렌즈를 바꾸거나 상한을 조정할 때마다 즉시 검증할 수 있습니다.

import math

def coverage(N, t_min_us, t_max_us, gain_max_db):
    g = gain_max_db / 6.02
    ev_bright = math.log2(N**2 / (t_min_us * 1e-6))
    ev_dark   = math.log2(N**2 / (t_max_us * 1e-6)) - g
    return ev_dark, ev_bright

print(coverage(8.0,  100, 5000, 16))     # 현재
print(coverage(4.0,  100, 3000, 16.9))   # 권장 — 게인은 16.9 dB가 이 카메라의 실측 하드웨어 상한(24 dB 불가)

2. 렌즈 스위트 스팟 (Sweet Spot)

2.1 왜 존재하는가

렌즈의 선명도(MTF)를 조리개에 대해 그리면 가운데가 가장 좋은 U자 뒤집힌 형태가 됩니다. 양쪽 끝이 나빠지는 이유가 서로 다릅니다.

조리개를 열었을 때(f/1.4~f/2) 나빠지는 이유 — 수차(aberration)

  • 구면수차, 코마, 비점수차 → 렌즈 주변부 광선이 한 점에 모이지 않음
  • 비네팅(주변부 광량 저하) 심함 → photometric 일관성에 직접 악영향
  • 상면만곡(field curvature) → 화면 가장자리 초점 어긋남
  • 피사계심도 얕음 → 근거리/원거리 동시에 못 잡음

실장착 렌즈 기준: ZLKC VM0420MP5(4mm, C-mount, 수동 아이리스)는 최대개방이 F2.0이 한계입니다(resource/VM0420MP5 en.pdf). f/1.4 영역은 이 렌즈로는 애초에 시험 불가 — 수차가 가장 심한 극단은 f/2.0이고, 그보다 열 수 없다는 점에서 오히려 안전한 쪽입니다.

조리개를 조였을 때(f/8~f/16) 나빠지는 이유 — 회절(diffraction)

  • 이건 렌즈 품질과 무관한 물리 법칙입니다. 좋은 렌즈를 사도 해결 안 됩니다.
  • 조리개 구멍이 작아질수록 빛이 회절해 점광원이 Airy disk로 퍼집니다.
Airy disk 지름 = 2.44 · λ · N      (λ ≈ 0.55 µm, 가시광 중심)
조리개 Airy disk 지름
f/2.8 3.8 µm
f/4 5.4 µm
f/5.6 7.5 µm
f/8 10.7 µm
f/11 14.7 µm
f/16 21.5 µm

2.2 우리 시스템에 적용

픽셀 피치 3.45 µm은 이제 가정이 아니라 확인된 값입니다 — 실장착 카메라 MV-CS016-10UC가 정확히 Sony IMX273(3.45 µm)을 씁니다(resource/MVCS01610UMUC ... datasheet.pdf). 아래 계산이 바로 우리 카메라 값입니다:

  • f/8 → Airy 10.7 µm = 픽셀 3.1개에 걸쳐 번짐
  • f/11 → Airy 14.7 µm = 픽셀 4.3개에 걸쳐 번짐

즉 현재 세팅은 하드웨어적으로 이미지를 3~4픽셀 블러 처리한 것과 같습니다. 코너 검출기(FAST/ORB/Harris)는 로컬 gradient 피크에 의존하므로, 이 정도 번짐이면 검출되는 특징점 수와 위치 반복성(repeatability)이 눈에 띄게 나빠집니다.

회절 한계 조리개 (Airy 지름 = 픽셀 2개 기준):

N_limit = 2·p / (2.44 · λ)

p = 3.45 µm → N_limit = 6.9 / 1.342 ≈ f/5.1   ← 우리 카메라(MV-CS016-10UC, IMX273)가 정확히 이 값
p = 2.74 µm → N_limit ≈ f/4.1
p = 5.86 µm → N_limit ≈ f/8.7

→ 우리 시스템의 조리개 상한선은 f/5.1입니다. f/4 권장안은 이 한계 안쪽이라 문제 없고, f/5.6은 살짝 벗어나므로 f/4 쪽이 더 안전합니다.

2.3 대부분 렌즈의 스위트 스팟

최대개방에서 2~3 stop 조인 지점, 실무적으로 f/4 ~ f/5.6이 대부분입니다.

우리 렌즈(VM0420MP5, 최대개방 F2.0)에 그대로 대입하면 2~3 stop 조인 지점이 정확히 f/4 ~ f/5.6이라, 이 일반론이 실제 렌즈 스펙과 잘 맞아떨어집니다. 다만 2.2절에서 구한 회절 한계(f/5.1)를 넘지 않으려면 f/4 쪽으로 붙이는 게 안전합니다 — f/5.6은 회절 한계를 살짝 넘습니다.

2.4 그래도 실측으로 확인할 것

이론은 이론이고, 저가 광각 렌즈는 개방에서 정말 형편없을 수 있습니다. 다음 절차로 30분 만에 확인 가능합니다:

  1. 삼각대 고정, 벽면에 슬랜티드 엣지(45도 기울인 흑백 경계) 또는 지멘스 스타 차트 부착
  2. 조리개를 f/2.8, f/4, f/5.6, f/8, f/11로 바꾸며 촬영 (노출로 밝기 보상)
  3. 각 이미지에서 측정:
    • ORB/FAST 특징점 개수 ← SLAM에는 이게 MTF보다 직접적인 지표
    • 슬랜티드 엣지 MTF50 (sfrmat, MTF Mapper 등)
    • 중앙 / 코너 각각 따로
  4. 특징점 개수가 최대가 되는 조리개를 선택

3. 조리개 결정 절차 — "가장 어두운 곳 기준" 구체화

질문: 어두운 곳 기준으로 조리개를 맞출 때, 그때의 노출값과 게인값은 무엇으로 두는가?

답: 물리적 한계값(5000 µs, 16 dB)이 아니라, "품질 허용 한계값"으로 둡니다. 그래야 여유(headroom)가 남습니다.

3.1 Step 1 — 노출 상한 t_blur_max 를 계산으로 결정

블러(px) ≈ f_px · ω · t_exp   +   f_px · v⊥ · t_exp / Z

  f_px  : 초점거리 [픽셀]
  ω     : 각속도 [rad/s]   ← 핸드헬드에서 지배적
  v⊥    : 시선 수직 속도 [m/s]
  Z     : 피사체 거리 [m]

핸드헬드 보행 기준 실측 권장값: IMU 자이로 로그에서 각속도 95 퍼센타일을 뽑아 쓰세요. 몸을 돌리거나 코너를 돌 때 1~2 rad/s가 흔히 나옵니다.

우리 렌즈+센서 조합의 실제 f_px:

f_px = f_lens[mm] / pixel_pitch[mm] = 4 / 0.00345 ≈ 1159 px

(VM0420MP5 4mm + IMX273 3.45 µm 기준. 실측 캘리브레이션 JUL8_calib.md의 cam1 fx=1200.0 / cam2 fx=1196.8과도 거의 일치 — 이 식이 신뢰할 만함을 뒷받침합니다.)

이 값은 아래 원래 예시가 가정했던 f_px=500(표준 화각)이나 f_px=300(광각)보다 훨씬 큽니다 — 즉 이 렌즈는 예시들보다 화각이 좁아서(수평 약 64°), 같은 각속도에서도 블러 허용 노출시간이 훨씬 짧습니다.

실제 f_px=1159 기준 예시 (ω = 1.5 rad/s, 블러 목표 1.0 px):

t_blur_max = 1.0 / (1159 · 1.5) = 0.58 ms

실제 f_px=1159 기준 예시 (ω = 1.0 rad/s, 블러 목표 1.5 px, 조금 더 관대한 목표):

t_blur_max = 1.5 / (1159 · 1.0) = 1.29 ms

참고용 원래 예시 (다른 화각의 렌즈였다면):

f_px = 500,  ω = 1.5 rad/s, 블러 목표 1.0 px → t_blur_max = 1.33 ms
f_px = 300(광각), ω = 1.0 rad/s, 블러 목표 1.5 px → t_blur_max = 5.0 ms

초점거리가 짧을수록(광각) 노출을 길게 쓸 수 있습니다. 우리 렌즈는 광각이 아니라서(4mm/1160px), 요약표의 "보통 23 ms"는 과도한 값입니다 — 실제로는 **0.61.3 ms** 수준을 목표로 잡고, 최종 값은 반드시 실제 IMU 각속도 95퍼센타일로 재계산하세요. 이 값이 짧아지면 1.4절의 다크사이드 EV 계산도 함께 나빠진다는 점에 유의합니다(노출 상한이 줄면 어두운 장면을 커버할 능력도 줄어듦).

3.2 Step 2 — 실사용 게인 상한 g_work 를 실측으로 결정

게인을 0 / 6 / 12 / 16.9 dB로 바꿔가며 동일 장면 촬영(이 카메라는 16.9 dB가 물리적 상한이라 18/24 dB는 설정 자체가 안 됨) → ORB 특징점 개수와 매칭 인라이어 비율을 측정 → 급격히 꺾이는 지점 직전을 g_work로 잡습니다. 보통 12 dB 근처입니다.

g_max(비상 상한)는 통상 g_work + 12 dB 정도로 열어두라고 하지만, 이 카메라(MV-CS016-10UC)는 게인 자체가 0~16.9 dB까지만 되므로 그 공식을 그대로 쓸 수 없습니다. 예: g_work = 12 dB인 경우, g_max는 24 dB가 아니라 하드웨어 물리적 한계인 16.9 dB이고, 비상용으로 실제 확보되는 여유는 16.9 - 12 = 4.9 dB(0.81 stop)뿐입니다. g_work를 더 낮게 잡을수록(예: 8 dB) 이 여유는 늘어나지만 대신 실사용 노출이 그만큼 길어져야 합니다.

3.3 Step 3 — 가장 어두운 목표 환경에서 조리개 조절

  1. 실제로 데이터를 취득해야 하는 가장 어두운 장소로 이동 (지하주차장? 실내 복도? 야간?)
  2. 카메라를 수동 모드로 두고 t = t_blur_max, gain = g_work 로 고정
  3. 조리개 링을 돌려가며 히스토그램 중앙값(median)이 8-bit 기준 DN 100~120이 되는 지점을 찾음
  4. 그 위치에서 조리개를 기계적으로 고정 (고정 나사 + 나사고정제/락타이트 또는 매니큐어). 포커스 링도 동일하게 고정.
  5. 카메라 3대 모두 동일 f-number가 되도록 맞춤 (렌즈 개체 편차가 있으므로 눈금이 아니라 실측 밝기로 맞추세요)

3.4 Step 4 — 밝은 쪽 검증

  1. 맑은 날 직사광 실외로 이동
  2. t = t_min, gain = 0 dB
  3. 관심 영역(하늘 제외)의 98 퍼센타일 < DN 250 인지 확인
  4. 포화되면 → ND 필터 추가 (ND2 = 1 stop, ND4 = 2 stop, ND8 = 3 stop)
    • 얇은 평행 평판 ND는 내부 파라미터에 유의미한 영향 없음
    • 단, 실제 사용 상태(필터 장착 상태)로 캘리브레이션 재검증 권장
  5. 포화 안 되면 → ND 불필요. 위 EV 계산상 f/4 + 100 µs 이면 EV 17.3까지 커버하므로 ND 없이 가능성이 높습니다.

3.5 Step 5 — 조리개 고정 후 캘리브레이션

조리개와 포커스를 확정한 바로 그 상태로:

  • 각 카메라 내부 캘리브레이션 (intrinsic + distortion)
  • 카메라 간 / 카메라-LiDAR 외부 캘리브레이션
  • 비네팅 맵광응답함수(CRF) 도 이때 함께 측정해두면 후처리에서 크게 유리 (7절 참조)

이후 조리개/포커스 링에 표식(마킹) 을 남기고, 세션마다 사진을 찍어 변화 여부를 기록하세요.


4. 측광 지표 (Metering Metric) — 논문 요약

4.1 현재 방식의 문제: 평균 밝기 (Mean Intensity)

목표: mean(I) = 128 이 되도록 노출 조절

문제: 프레임의 20%를 차지하는 하늘(DN 255)이 평균을 끌어올림 → 컨트롤러가 노출을 줄임 → 정작 특징점이 있는 지면/건물이 DN 30~50으로 뭉개짐. 관측하신 현상이 정확히 이것입니다.

더 근본적으로, 평균 밝기는 "이미지에 정보가 얼마나 있는가"와 아무 상관이 없습니다. 평균 128인 완전 균일한 회색 이미지는 특징점이 0개입니다.

4.2 즉시 적용 가능한 개선: ROI + 퍼센타일 측광

논문 구현 없이 오늘 바꿀 수 있는 것이고, 개선 폭이 가장 큽니다.

1) ROI 마스크: 프레임 상단 1/3 제외 (하늘) 또는 LiDAR 깊이로 원거리 영역 제외
2) 제어 목표:   ROI 내부의 p70 퍼센타일 → DN 130
3) 하드 제약:   ROI 내부의 p98 퍼센타일 < DN 245  (포화 방지)
4) 하늘이 포화되는 것은 허용 — SLAM에 하늘 특징점은 어차피 불필요

퍼센타일은 소수의 극단적으로 밝은 화소에 흔들리지 않으므로, 평균 대비 훨씬 안정적입니다.

4.3 Shim et al. — Gradient 기반 노출 제어

  • "Auto-adjusting camera exposure for outdoor robotics using gradient information", IROS 2014
  • "Gradient-based Camera Exposure Control for Outdoor Mobile Platforms", arXiv:1708.07338 / IEEE TCSVT

핵심 아이디어: 대부분의 영상처리 알고리즘(코너 검출, 디스크립터, 스테레오 매칭)이 로컬 gradient에 의존하므로, 밝기가 아니라 gradient 총량이 최대가 되는 노출을 찾자.

동작 방식:

  1. 현재 이미지 I에 여러 감마 값 γ를 적용해 "다른 노출로 찍었다면 어땠을지"를 합성 I_γ = 255 · (I/255)^γγ < 1이면 밝아진 이미지, γ > 1이면 어두운 이미지
  2. 각 합성 이미지에서 Sobel gradient 크기 맵을 계산
  3. 특별한 매핑 함수를 적용해 gradient를 합산:
    • 아주 작은 gradient(노이즈)는 임계값 δ 이하로 버림
    • 큰 gradient는 log 압축해서 몇 개의 강한 엣지가 점수를 독점하지 못하게 함
    • 결과적으로 "적당한 크기의 gradient가 많이 있는 상태" 가 최고점
  4. 점수가 최대인 γ*를 찾음
  5. γ* < 1 이면 노출을 올리고, γ* > 1 이면 내림

실무 제어 법칙(비례 제어, 상수는 튜닝):

Δstop = -k · log2(γ*)        k ∈ [0.5, 1.0]
t_next = t_curr · 2^(Δstop)

멀티카메라 확장 (우리 케이스에 직접 해당): 논문은 이 개념을 다중 카메라 시스템으로 확장해, 인접 카메라 간 밝기 일관성각 카메라의 적정 노출을 동시에 만족하는 제어 알고리즘을 제시합니다. 각 카메라가 독립 AE로 돌면 크로스 카메라 매칭과 파노라마 스티칭이 깨지는데, 이를 비용함수에 일관성 항으로 넣어 해결합니다.

장점: 광응답함수 캘리브레이션이 필요 없음 (감마 근사만 사용) → 구현 난이도 낮음 단점: 여러 γ를 sweep 해야 해서 계산량이 있음 (후처리 시스템이면 문제 없음)

4.4 Zhang, Forster, Scaramuzza — Active Exposure Control

  • "Active Exposure Control for Robust Visual Odometry in HDR Environments", ICRA 2017
  • 코드: github.com/uzh-rpg/active_camera_exposure_control (GPLv3)

Shim 대비 차이: 감마로 근사하는 대신 카메라의 실제 광응답함수(CRF)를 오프라인 캘리브레이션해 두고, 이를 이용해 "노출 시간이 t일 때의 이미지"를 해석적으로 정확히 예측합니다.

그 위에서 gradient 지표를 노출 시간에 대해 미분해서 gradient ascent로 최적점을 찾습니다. Sweep이 아니라 미분이므로 훨씬 빠릅니다.

논문은 SVO(Semi-direct Visual Odometry)를 가변 노출에 대응하도록 수정해 붙였고, 카메라 내장 AE와 고정 노출 양쪽 모두보다 좋은 결과를 보였습니다.

위 저장소에는 4가지 방법이 모두 구현되어 있어 바로 비교 가능합니다:

  • kMeanIntensity — 평균 밝기 (baseline, 현재 방식)
  • kPercentile — 퍼센타일 기반 (4.2절 방식)
  • kGradient — Zhang et al. gradient 미분 방식
  • kShim — Shim et al. sweep 방식

→ 실무 착수점으로 이 저장소를 그대로 쓰는 것을 추천합니다.

4.5 Kim, Cho, Kim — Bayesian Optimization

  • "Exposure control using Bayesian optimization based on entropy weighted image gradient", ICRA 2018

Gradient에 엔트로피 가중치를 곱한 지표를 쓰고, 국소 gradient ascent 대신 베이지안 최적화로 전역 탐색합니다. 목적함수가 다봉(multi-modal)일 때 국소 최적에 빠지지 않는 것이 장점이고, 적은 샘플로 수렴합니다.

4.6 최신 흐름 — Region-weighted / Motion-aware

  • "Region-weighted gradient and motion-aware camera exposure control for robust visual odometry" (2025)

Gradient 지표에 영역별 가중치(가까운 구조물 > 먼 배경)와 모션 블러 페널티를 추가합니다. IMU 각속도를 읽어서 "지금 빠르게 회전 중이니 노출을 길게 못 쓴다"를 제어에 반영하는 방식으로, 3.1절의 블러 계산을 실시간으로 하는 것에 해당합니다. VINS-Mono와 ORB-SLAM3 양쪽에서 정확도 향상을 보고합니다.

또한 BorealHDR 라는 다중 노출 스테레오 데이터셋(8.4 km, 50 궤적, LiDAR 기반 3D 맵 및 포즈 GT 포함)이 공개되어 있어, AE 알고리즘을 재현 가능하게 오프라인 벤치마크할 수 있습니다. 후처리 파이프라인이시니 이런 에뮬레이터 방식이 잘 맞습니다.

4.7 적용 우선순위

1단계 (즉시)   : ROI + 퍼센타일 측광으로 교체       ← 개선폭 최대, 비용 최소
2단계 (1~2주)  : uzh-rpg 저장소 kShim / kGradient 적용
3단계          : 3대 카메라 밝기 일관성 항 추가 (Shim 멀티카메라)
4단계          : IMU 각속도 연동 노출 상한 동적 조절

5. 노출 중심(Exposure-Midpoint) 타임스탬프

5.1 문제 정의

하드웨어 트리거는 노출의 시작을 결정합니다. 하지만 이미지가 실제로 표현하는 시각은 노출 구간의 중심입니다.

트리거 엣지 ────┬──────────────────────────────────>  시간
                │
                ├─ t_latency ─┤
                              ├──── t_exp ────┤
                              │       ↑       │
                        노출시작   유효시각   노출종료
                                (t_exp/2)

카메라 3대가 각자 AE로 돌면 t_exp가 서로 다릅니다. 같은 트리거 엣지를 받아도 유효 시각이 서로 어긋납니다.

카메라 A: t_exp = 3000 µs → 유효시각 = t_trig + Δ + 1500 µs
카메라 B: t_exp =  200 µs → 유효시각 = t_trig + Δ +  100 µs
                             차이 = 1400 µs = 1.4 ms

5.2 오차 크기 — 핸드헬드 보행 기준

병진 오차:

보행 1.4 m/s × 1.4 ms = 2.0 mm     ← 무시 가능

회전 오차 (이쪽이 지배적):

각속도 1.5 rad/s × 1.4 ms = 2.1 mrad = 0.12°
픽셀 환산: f_px 500 × 2.1 mrad = 1.05 px

→ 1 px 수준의 재투영 오차입니다. 일반적인 RANSAC 인라이어 임계값(1~2 px)과 같은 수준이라 무시하기 어렵습니다. 노출 차이가 최대(100 vs 5000 µs)일 때는 2.45 ms → 1.8 px까지 커집니다.

특히 핸드헬드로 몸을 돌릴 때 각속도가 순간적으로 2~3 rad/s까지 올라가고, 하필 그 순간이 실내→실외 전환처럼 노출이 크게 다른 상황과 겹치기 쉽습니다.

5.3 해결 절차

[A] 트리거→노출시작 지연 t_latency 를 1회 실측

대부분의 머신비전 카메라는 GPIO에 Strobe / ExposureActive 출력이 있습니다. 이 신호는 실제 센서 적분 구간 동안만 HIGH입니다.

1. 오실로스코프 CH1 = 트리거 입력, CH2 = ExposureActive 출력
2. 트리거 rising edge → ExposureActive rising edge 까지의 시간 = t_latency
3. ExposureActive의 HIGH 폭 = 실제 t_exp (설정값과 다를 수 있음 — 반드시 확인)
4. 카메라 3대 각각 측정 (개체차 있음)
5. 노출 시간을 100 µs / 1000 µs / 5000 µs로 바꿔가며 t_latency가 일정한지 확인

[B] 프레임별 실제 노출 시간을 로깅

GenICam Chunk Data를 활성화합니다:

ChunkModeActive = True
ChunkSelector = ExposureTime  → ChunkEnable = True
ChunkSelector = Gain          → ChunkEnable = True
ChunkSelector = Timestamp     → ChunkEnable = True
ChunkSelector = FrameID       → ChunkEnable = True

이러면 각 이미지에 실제 적용된 노출/게인이 이미지와 함께 원자적으로 붙어옵니다. AE는 명령과 실제 적용 사이에 1~2프레임 지연이 있으므로, 설정값이 아니라 chunk 값을 써야 합니다.

[C] 후처리에서 타임스탬프 보정

t_effective = t_trigger + t_latency + (t_exp_chunk / 2.0)

ROS 환경이라면 bag을 다시 쓰거나(header.stamp 재작성), 별도 CSV 사이드카를 만들어 SLAM 입력 시점에 적용합니다.

[D] 검증

보정 전/후로 다음을 비교:

  • 회전 구간에서의 재투영 오차 RMS
  • 카메라 간 특징점 매칭 인라이어 비율
  • LiDAR 포인트를 이미지에 투영했을 때의 엣지 정합도 (회전 구간에서 특히)

보정이 맞다면 회전이 빠른 구간에서 개선폭이 크게 나타나야 합니다. 정지 구간에서는 차이가 없어야 정상입니다.

5.4 대안 / 보완 전략

대안 1 — 노출 시간 동기화 (Master-Slave) 3대의 t_exp를 항상 동일하게 강제하고, 밝기 차이는 게인으로만 흡수합니다. 유효 시각이 구조적으로 일치하므로 보정이 불필요합니다.

  • 장점: 가장 확실, 후처리 단순
  • 단점: 태양을 정면으로 보는 카메라와 그늘을 보는 카메라의 노출 요구가 3~4 stop 차이날 때, 게인만으로는 2.7 stop(16 dB)밖에 못 메움

대안 2 — 노출 비율 제한 (절충안, 추천) 3대의 t_exp 비율을 예를 들어 최대 2:1 이내로 제한합니다. 그러면 최대 유효시각 차이가 t_max/4로 묶입니다.

t_max = 3000 µs → 최대 어긋남 750 µs → 회전 1.5 rad/s에서 0.56 px

여기에 [C]의 보정까지 하면 잔차는 무시할 수준이 됩니다.

대안 3 — TriggerDelay 사전 보정 카메라의 TriggerDelay 레지스터로 노출 시작을 미리 당겨서 중심을 맞추는 방법입니다. 다만 다음 프레임의 t_exp를 미리 알아야 하므로, AE가 천천히 변할 때만 근사적으로 동작합니다. 후처리 시스템이면 [C]가 더 정확하고 간단합니다.

5.5 LiDAR 쪽 체크리스트

  • LiDAR 포인트가 포인트 단위 타임스탬프를 갖는지 확인 (스캔 단위만 있으면 deskew 불가)
  • IMU로 모션 디스큐(motion compensation) 적용 여부 확인
  • 카메라와 LiDAR가 동일 시간 기준(PTP / PPS+NMEA / 공통 트리거 카운터)을 쓰는지 확인
  • LiDAR 스캔 구간의 어느 시점을 "스캔 시각"으로 잡을지 규약을 문서화 (스캔 시작? 중심?) — 카메라 중심시각 규약과 일관되게

6. 12-bit RAW 저장이란

6.1 무엇이 다른가

센서 ADC는 원래 10~12 bit입니다. 8-bit 출력을 선택하면 카메라가 내부에서 감마/LUT를 적용하고 8-bit로 잘라서 내보냅니다. 이때 버려진 정보는 영구히 복구 불가입니다.

8-bit 12-bit
밝기 계단 수 256 4096
카메라 내부 처리 감마/LUT 적용됨 선형 그대로
하늘 근처 계조 뭉개짐 살아있음
그림자 디테일 소실 후처리로 끌어올림 가능

"RAW"의 의미: 카메라가 감마, 화이트밸런스, 샤프닝, 디모자이킹(컬러의 경우)을 적용하지 않은 상태. 이 처리들을 후처리 PC에서 우리가 원하는 대로 수행합니다.

6.2 후처리 시스템에서의 실질적 가치

핵심은 "12→8 bit 변환 규칙을 언제든 다시 정할 수 있다" 는 것입니다.

[취득]  12-bit 선형 raw  →  디스크 저장 (한 번만 찍음)
                              │
[후처리]                      ├─→ 감마 2.2 → 8-bit → SLAM 시도 1
                              ├─→ CLAHE   → 8-bit → SLAM 시도 2
                              └─→ 로컬 톤매핑 → 8-bit → SLAM 시도 3

8-bit로 저장했다면, 결과가 나쁠 때 현장에 다시 나가야 합니다. 12-bit raw면 책상에서 재처리합니다. 이게 후처리 파이프라인의 가장 큰 이점입니다.

6.3 PixelFormat 선택

포맷 바이트/픽셀 비고
Mono8 1.0 현재 추정
Mono12Packed / Mono12p 1.5 12-bit를 압축 패킹 — 권장
Mono16 2.0 12-bit 데이터를 16-bit 컨테이너에 담음, 낭비
BayerRG12Packed 1.5 컬러 센서. 디모자이킹은 후처리에서
RGB8 3.0 카메라 내부 디모자이킹 — 피하세요

컬러 카메라라면 반드시 Bayer raw로 저장하세요. 카메라 내부 디모자이킹은 대역폭 3배에 품질도 나쁩니다.

6.4 대역폭/저장 용량 계산

초당 데이터량 = 카메라수 × 해상도 × 바이트/픽셀 × FPS

예시 (5 MP, 20 FPS, 3대):

Mono8       : 3 × 5.0e6 × 1.0 × 20 = 300 MB/s  → 1시간 1.08 TB
Mono12p     : 3 × 5.0e6 × 1.5 × 20 = 450 MB/s  → 1시간 1.62 TB

요구사항:

  • NVMe SSD 필수 (SATA SSD 550 MB/s는 여유가 없음, HDD는 불가)
  • 인터페이스 대역폭 확인: GigE 125 MB/s / 10GigE 1.25 GB/s / USB3 400 MB/s / CoaXPress
  • 무손실 압축 고려: 12-bit 선형 데이터는 zstd -1 이나 lz4 로 실시간 압축이 가능하고, 보통 1.5~2배 줄어듭니다. 손실 압축(JPEG)은 특징점 품질에 영향을 주므로 피하세요.

6.5 확인해야 할 함정

  • 프레임레이트 저하: 일부 센서는 12-bit에서 리드아웃이 느려져 최대 FPS가 떨어집니다. 데이터시트 확인 필수.
  • 대역폭 초과 시 프레임 드롭: 드롭이 생기면 하드웨어 동기가 무의미해집니다. FrameID 연속성을 반드시 로깅/검증하세요 (7절).
  • Gamma / LUT 비활성화 확인: Gamma = 1.0, LUTEnable = False. 이게 켜져 있으면 raw가 raw가 아닙니다.
  • BlackLevel 값 기록: 선형성 복원에 필요합니다.

6.6 후처리 파이프라인 권장 순서

12-bit 선형 raw
   ↓ BlackLevel 차감
   ↓ (컬러면) 디모자이킹
   ↓ 비네팅 보정 (플랫필드 맵)
   ↓ 노출/게인으로 정규화  ← photometric 일관성 확보
   ↓ 톤매핑: 감마 또는 CLAHE
8-bit mono → SLAM 프론트엔드

CLAHE 사용 시 주의: clipLimit, tileGridSize전 프레임에 걸쳐 고정하세요. 프레임마다 다르면 밝기 일관성이 깨져서 direct 방식이나 루프클로저에 악영향입니다.


7. 메타데이터 로깅 대상 정리

7.1 프레임 단위 (매 이미지마다)

항목 출처 왜 필요한가
frame_id (시퀀스 번호) Chunk FrameID 드롭 프레임 검출 — 가장 중요
trigger_count 트리거 보드 트리거와 프레임의 1:1 대응 검증
device_timestamp Chunk Timestamp 카메라 내부 클럭
host_timestamp 수신 PC 클럭 드리프트 추정용
exposure_time_us Chunk ExposureTime 타임스탬프 보정 + 광도 정규화
gain_db Chunk Gain 광도 정규화
black_level Chunk / 주기적 선형성 복원
device_temperature DeviceTemperature 암전류 변화, 캘리브레이션 열드리프트
pixel_format 설정 후처리 디코딩
wb_gains (컬러) Chunk 컬러 일관성
ae_state 컨트롤러 AE 수렴/포화 여부 디버깅
metering_score 컨트롤러 측광 지표 값 (튜닝 분석용)

7.2 세션 단위 (취득 1회마다, YAML 매니페스트)

session:
  id: 2026-08-13_park_001
  datetime_start: 2026-08-13T14:20:00+09:00
  environment: outdoor_sunny      # indoor / outdoor / mixed
  weather: clear
  platform: handheld
  operator: ...

cameras:
  - name: cam0
    model: ...
    serial: ...
    firmware: ...
    lens_model: ...
    lens_serial: ...
    aperture_fnumber: 4.0         # ★ 반드시 기록
    focus_setting: hyperfocal_marked
    nd_filter: none               # none / ND4 / ND8
    pixel_format: Mono12p
    resolution: [2448, 2048]
    roi: [0, 0, 2448, 2048]
    binning: 1
    gamma_enabled: false
    lut_enabled: false
    exposure_limits_us: [30, 3000]
    gain_limits_db: [0, 17]        # MV-CS016-10UC 실측 하드웨어 상한 (24 불가)
    ae_algorithm: shim_gradient_v2
    ae_roi_mask: masks/cam0_sky_exclude.png

sync:
  trigger_source: external_board
  trigger_freq_hz: 20
  trigger_latency_us:             # ★ 오실로스코프 실측값
    cam0: 12.4
    cam1: 12.1
    cam2: 12.8
  time_base: PTP                  # PTP / PPS+NMEA / trigger_counter

lidar:
  model: ...
  scan_rate_hz: 10
  per_point_timestamp: true
  sync_mode: PPS+NMEA
  deskew_applied: false           # 후처리에서 수행

imu:
  model: ...
  rate_hz: 200

calibration:
  intrinsics_file: calib/intrinsics_2026-08-10.yaml
  intrinsics_sha256: ...
  extrinsics_file: calib/extrinsics_2026-08-10.yaml
  extrinsics_sha256: ...
  calibrated_aperture: 4.0        # ★ 세션 조리개와 일치하는지 검증
  photometric_crf: calib/crf_cam0.txt
  vignetting_map: calib/vignette_cam0.png

storage:
  raw_path: /data/2026-08-13_park_001/
  compression: zstd-1

7.3 필수 검증 스크립트 (후처리 첫 단계)

취득 직후 자동으로 돌려야 하는 검사들:

[ ] FrameID 연속성 — 카메라 3대 모두 결번 없는가
[ ] 트리거 카운트 == 프레임 카운트
[ ] 3대 프레임 개수 일치
[ ] 노출 시간 히스토그램 — 상한/하한에 붙어있는 프레임 비율
      → 상한에 계속 붙어있으면 조리개가 너무 조여진 것
[ ] 포화 화소 비율 (DN > 4000 in 12-bit)
      → ROI 내에서 1% 넘으면 경고
[ ] 언더 화소 비율 (DN < 100 in 12-bit)
[ ] 카메라 간 노출 시간 비율 — 2:1 초과 프레임 수
[ ] device_timestamp vs host_timestamp 드리프트 추이
[ ] 온도 변화 폭 — 세션 중 10°C 이상 변하면 캘리브레이션 재검토

이 검사를 통과 못 하면 현장을 떠나기 전에 재취득하는 게 원칙입니다.


8. Claude Code 작업 지시용 태스크 목록

개발 PC에서 착수할 때 이 순서를 권장합니다.

Phase 1 — 진단 도구 (반나절)

  1. ev_coverage.py — 1.4절 EV 커버리지 계산기. 조리개/노출/게인 조합 입력 → 커버 EV 범위 출력, 목표 환경 EV 리스트와 비교해 부족분 표시
  2. blur_budget.py — 3.1절 블러 계산기. IMU 로그(rosbag/csv) 입력 → 각속도 퍼센타일 통계 → 목표 블러(px) 대비 t_blur_max 산출
  3. bag_qc.py — 7.3절 검증 스크립트 전체

Phase 2 — 광학 특성 실측 (1일, 현장 작업 포함)

  1. aperture_sweep_eval.py — 조리개별 촬영 이미지 폴더 입력 → ORB 특징점 수, 매칭 반복성, MTF50을 조리개별로 플롯
  2. gain_noise_eval.py — 게인별 SNR 및 특징점 품질 곡선 → g_work 결정
  3. 오실로스코프 측정으로 trigger_latency 3대 실측 (수작업, 결과를 YAML에 기록)

Phase 3 — 타임스탬프 보정 (1~2일)

  1. Chunk Data 활성화 (ExposureTime, Gain, Timestamp, FrameID) 및 로깅 경로 구현
  2. retimestamp.pyt_trig + t_latency + t_exp/2 로 타임스탬프 재작성. rosbag 재작성 또는 사이드카 CSV 생성
  3. 보정 전/후 재투영 오차 비교 리포트 생성 (회전 구간 별도 집계)

Phase 4 — 노출 제어 개선 (1~2주)

  1. ROI 마스크 생성 도구 (수동 폴리곤 또는 LiDAR 깊이 기반 자동)
  2. 퍼센타일 측광 컨트롤러 구현 및 교체 (4.2절)
  3. uzh-rpg/active_camera_exposure_control 통합, kShim/kGradient 비교 평가
  4. 3대 밝기 일관성 항 추가 (Shim 멀티카메라 방식)
  5. 노출 비율 2:1 제한 로직 추가 (5.4절 대안 2)

Phase 5 — 후처리 파이프라인 (1주)

  1. Mono12p 전환, 대역폭/드롭 검증
  2. 광응답함수(CRF) + 비네팅 맵 캘리브레이션 도구
  3. 12→8 bit 톤매핑 파이프라인 (감마 / CLAHE / 로컬 톤매핑 선택 가능하게, 파라미터는 세션 전체 고정)
  4. 노출·게인 기반 광도 정규화 적용

9. 참고문헌

  • I. Shim, J.-Y. Lee, I. S. Kweon, "Auto-adjusting camera exposure for outdoor robotics using gradient information," IROS 2014
  • I. Shim, T.-H. Oh, J.-Y. Lee, J. Choi, D.-G. Choi, I. S. Kweon, "Gradient-based Camera Exposure Control for Outdoor Mobile Platforms," arXiv:1708.07338 / IEEE TCSVT
  • Z. Zhang, C. Forster, D. Scaramuzza, "Active Exposure Control for Robust Visual Odometry in HDR Environments," ICRA 2017
  • J. Kim, Y. Cho, A. Kim, "Exposure control using Bayesian optimization based on entropy weighted image gradient," ICRA 2018
  • "Region-weighted gradient and motion-aware camera exposure control for robust visual odometry," 2025
  • BorealHDR — 다중 노출 스테레오 데이터셋 (AE 알고리즘 오프라인 재현 벤치마크용)
  • J. Engel, V. Usenko, D. Cremers, "A Photometrically Calibrated Benchmark For Monocular Visual Odometry" (TUM mono dataset — 광응답함수/비네팅 캘리브레이션 방법론)