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>
This commit is contained in:
+269
@@ -0,0 +1,269 @@
|
||||
# 질문지 답변
|
||||
|
||||
> `question_1.md` 5개 질문에 대한 답변. `camera_exposure_design_notes.md`의 일반론을 이 저장소의 실제 구현(`hik_camera_ros2_driver/src/hik_camera_node.cpp`)과 실제 하드웨어 데이터시트(`resource/`에 첨부된 카메라·렌즈 매뉴얼)로 검증했다.
|
||||
>
|
||||
> 사용한 근거 자료:
|
||||
> - 카메라: `resource/MVCS01610UMUC USB3.0 Area Scan Camera_datasheet_20250826 1.pdf` (MV-CS016-10UC, 실제 장착 모델)
|
||||
> - 렌즈: `resource/VM0420MP5 en.pdf` (ZLKC VM0420MP5, 4mm F2.0)
|
||||
> - 드라이버 소스: `src/hik_camera_ros2_driver/src/hik_camera_node.cpp`
|
||||
> - 현재 설정값: `src/hik_camera_ros2_driver/config/camera_params_cam{1,2,3}.yaml`
|
||||
> - 실측 캘리브레이션: `src/hik_camera_ros2_driver/config/JUL8_calib.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 노출 하한 100 µs가 맞는가
|
||||
|
||||
**결론: 더 낮출 수 있다. 하드웨어 스펙상 표준 모드에서 15 µs까지 가능하다.**
|
||||
|
||||
카메라 데이터시트(`MV-CS016-10UM/UC`) 사양표:
|
||||
|
||||
| 모드 | 범위 |
|
||||
|---|---|
|
||||
| UltraShort exposure mode | 1 µs ~ 14 µs |
|
||||
| **Standard exposure mode** | **15 µs ~ 10 sec** |
|
||||
|
||||
현재 3대 모두 `exposure_auto_min: 100.0` (µs)로 설정되어 있다 (`camera_params_cam1/2/3.yaml`). 이는 하드웨어가 지원하는 Standard 모드 하한(15 µs)보다 **6.7배 높은 값**이며, 별도 모드 전환 없이 그냥 파라미터 값만 낮추면 되는 영역이다.
|
||||
|
||||
```
|
||||
현재 하한 100 µs → 데이터시트 표준모드 하한 15 µs
|
||||
Δstop = log2(100/15) = 2.74 stop 추가 확보 가능
|
||||
```
|
||||
|
||||
design note 1.3절의 "8.30 stop 예산, 여유 사실상 0" 진단에 그대로 대입하면:
|
||||
|
||||
```
|
||||
기존: 노출 100→5000 µs (5.64 stop) + 게인 0→16 dB (2.66 stop) = 8.30 stop
|
||||
개선: 노출 15→3000 µs (7.64 stop, 상한은 3.1절 블러 계산으로 재설정) + 게인 동일
|
||||
= 5.64 → 7.64 stop 로 노출 쪽에서만 2 stop 여유 증가
|
||||
```
|
||||
|
||||
코드상 이 값의 실제 상한/하한은 카메라에서 직접 쿼리한다:
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:176, 186-191
|
||||
MV_CC_GetFloatValue(camera_handle_, "ExposureTime", &f_value);
|
||||
param_desc.integer_range[0].from_value = static_cast<int64_t>(f_value.fMin);
|
||||
param_desc.integer_range[0].to_value = static_cast<int64_t>(f_value.fMax);
|
||||
exposure_auto_min_ = this->declare_parameter("exposure_auto_min", 100.0, param_desc);
|
||||
```
|
||||
|
||||
즉 ROS 파라미터 허용 범위 자체가 `f_value.fMin`이므로, **100 µs보다 낮은 값을 넣어도 카메라가 거부하지 않을 가능성이 높다** (실제 fMin이 15 µs 근방이라면). 배포 전 개체별 실측을 위해 `f_value.fMin`/`fMax`를 한 줄 로그로 찍어 3대 각각 확인하는 것을 권장한다 (데이터시트 값은 스펙 상 보증치이고, 개체 편차가 있을 수 있음).
|
||||
|
||||
**권장 조치:** `exposure_auto_min`을 15~30 µs 수준으로 낮춰서 3대 모두 재테스트. 코드 수정 없이 yaml/launch 파라미터 변경만으로 가능.
|
||||
|
||||
---
|
||||
|
||||
## 2. 게인 상한이 16 dB 근처가 맞는가
|
||||
|
||||
**결론: 맞다. 그리고 이건 여유를 둔 값이 아니라 하드웨어 한계에 거의 붙은 값이다. 24 dB는 이 카메라에서 물리적으로 불가능하다.**
|
||||
|
||||
데이터시트 사양표:
|
||||
|
||||
```
|
||||
Gain: 0 dB to 17 dB
|
||||
```
|
||||
|
||||
`camera_params_cam1/2/3.yaml`에 이미 실측으로 남겨둔 주석과 정확히 일치한다:
|
||||
|
||||
```yaml
|
||||
gain: 12.0 # Range: 0.0 ~ 16.9, Unit: dB (cam1, cam3)
|
||||
gain: 15.0 # Range: 0.0 ~ 16.9, Unit: dB (cam2)
|
||||
```
|
||||
|
||||
이 범위는 코드가 실행 시점에 카메라로부터 직접 읽어온 값이다 (`hik_camera_node.cpp:201-203`, `MV_CC_GetFloatValue(camera_handle_, "Gain", &f_value)` → `f_value.fMax`). 즉 16.9 dB는 추측이 아니라 3대 카메라 모두에서 동일하게 확인된 **아날로그 게인 스테이지의 실제 상한**이다.
|
||||
|
||||
design note가 "여유"로 제시했던 `g_max = 24 dB`는 이 센서(Sony IMX273)에서 낼 수 없는 값이다. 정정하면:
|
||||
|
||||
| 항목 | design note 가정 | 실측 |
|
||||
|---|---|---|
|
||||
| 게인 상한 | 24 dB (3.99 stop) | **16.9 dB (2.81 stop)** |
|
||||
| f/4 + 게인상한 커버 다크사이드 EV | 7.65 (직사광~밝은실내) | **약 8.8~8.9** (밝은 실내 초반까지) |
|
||||
|
||||
즉 design note 1.4절의 "f/4 + 게인 24 dB → EV 7.65까지 커버" 시나리오는 이 카메라로는 실현 불가능하고, 실제로는 **조리개를 더 열거나(f/2.8) 노출 상한을 늘리는 쪽(블러 허용 한도 내에서)** 으로만 어두운 쪽 stop을 추가로 확보할 수 있다. design note가 "현재" 계산에 썼던 16 dB 자체는 이미 실측치에 근접한 값이라 그 부분 계산은 유효하다 — 틀린 건 "더 열 수 있다"는 가정(24 dB) 쪽이다.
|
||||
|
||||
**실무적으로 더 급한 문제:** cam2는 지금 `gain: 15.0`으로, 하드웨어 상한(16.9)까지 겨우 **1.9 dB(0.3 stop)** 남아 있다. 실내 진입 시 게인으로 흡수할 여유가 사실상 없는 카메라가 이미 1대 있다는 뜻이므로, 조리개/노출 쪽 대책(1번 항목, 3.1절 블러 계산)이 게인보다 먼저 손봐야 할 우선순위다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 12-bit raw + 프레임별 메타데이터 저장이 가능한가
|
||||
|
||||
두 가지를 나눠서 봐야 한다: **카메라/SDK가 지원하는가**(가능)와 **지금 이 드라이버가 실제로 그렇게 발행하는가**(불가능, 코드 수정 필요).
|
||||
|
||||
### 3.1 "12-bit raw"가 정확히 뭘 의미하는가
|
||||
|
||||
이 카메라(IMX273)는 ADC를 8/10/12-bit로 선택해서 읽을 수 있다. 지금 3대 모두:
|
||||
|
||||
```yaml
|
||||
adc_bit_depth: "Bits_8" # 8-bit = 256단계로 양자화해서 센서에서 내보냄
|
||||
pixel_format: "BayerRG8" # 컬러 베이어 모자이크, 아직 디모자이킹 전
|
||||
```
|
||||
|
||||
`Bits_12`로 바꾸면 센서 ADC가 4096단계로 픽셀값을 내보낸다. "raw"는 여기에 추가로 **디모자이킹(컬러 보간)·감마·화이트밸런스를 카메라/드라이버가 적용하지 않은 상태**를 뜻한다 — 이 처리들을 나중에 PC에서 원하는 방식으로(톤매핑 알고리즘을 여러 번 바꿔가며) 다시 할 수 있다는 게 핵심 가치다 (design note 6.1~6.2절 참조).
|
||||
|
||||
데이터시트 기준 이 카메라(UC, 컬러)가 지원하는 raw 포맷:
|
||||
|
||||
```
|
||||
Mono 8/10/12, Bayer RG 8/10/10Packed/12/12Packed, YUV422Packed, RGB8, BGR8
|
||||
```
|
||||
|
||||
`BayerRG12Packed`가 명시적으로 지원된다. **여기까지는 카메라 하드웨어 레벨에서 가능하다.**
|
||||
|
||||
### 3.2 그런데 지금 드라이버는 무조건 8-bit RGB로 변환해서 내보낸다
|
||||
|
||||
`hik_camera_node.cpp`를 보면 목적 포맷이 초기화 시점에 고정되어 있다:
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:132-134 (initializeCamera, 파라미터 선언보다 먼저 실행됨)
|
||||
convert_param_.nWidth = img_info_.nWidthValue;
|
||||
convert_param_.nHeight = img_info_.nHeightValue;
|
||||
convert_param_.enDstPixelType = PixelType_Gvsp_RGB8_Packed; // ← 항상 이걸로 변환
|
||||
```
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:499-505, 511 (captureLoop, 매 프레임)
|
||||
convert_param_.pSrcData = out_frame.pBufAddr;
|
||||
convert_param_.enSrcPixelType = out_frame.stFrameInfo.enPixelType;
|
||||
MV_CC_ConvertPixelType(camera_handle_, &convert_param_); // Bayer/Mono → RGB8로 변환
|
||||
...
|
||||
image_msg_.encoding = "rgb8";
|
||||
image_msg_.step = out_frame.stFrameInfo.nWidth * 3; // 3 byte/px 고정
|
||||
```
|
||||
|
||||
`pixel_format`/`adc_bit_depth` 파라미터는 **센서가 내보내는 원본(변환의 소스)만** 바꿀 뿐, 최종적으로 ROS 토픽·bag에 실리는 이미지는 `adc_bit_depth: Bits_12`로 설정해도 **항상 8-bit 3채널 RGB로 양자화·디모자이킹된 후**의 데이터다. 즉:
|
||||
|
||||
**지금 이 드라이버로는 코드를 고치지 않는 한 12-bit raw가 bag에 담기지 않는다.** 이게 design note 6.1절이 걱정하는 "정보 손실"이 실제로 지금 일어나고 있는 지점이다 (카메라가 아니라 ROS 드라이버 단에서).
|
||||
|
||||
**필요한 수정 (개략):** `pixel_format`이 12-bit raw 계열일 때는 `MV_CC_ConvertPixelType` 변환을 건너뛰고 SDK가 준 원본 버퍼를 그대로 발행. ROS `sensor_msgs/Image`에는 12-bit로 압축 패킹된 인코딩이 표준으로 없으므로, 실무적으로는 12-bit 값을 16-bit 컨테이너에 담는 `mono16` / `bayer_rggb16` 인코딩(값은 하위 12비트만 사용, 상위 4비트는 0)으로 발행하는 것이 cv_bridge/rqt_image_view 등과 바로 호환되어 가장 무난하다.
|
||||
|
||||
### 3.3 프레임별 메타데이터 — 사실 이미 대부분 손에 들어와 있다
|
||||
|
||||
design note 5.3-[B]는 "GenICam Chunk Data(`ChunkModeActive`)를 켜야 프레임별 노출/게인이 붙어온다"고 가정하는데, **이 SDK는 그럴 필요가 없다.** 캡처 루프가 이미 매 프레임 받아오는 `MV_FRAME_OUT_INFO_EX` 구조체(`out_frame.stFrameInfo`) 안에 이미 다음 필드가 들어있다 (`hikSDK/include/CameraParams.h:182-231`):
|
||||
|
||||
```c
|
||||
float fGain; // 이 프레임에 실제 적용된 게인
|
||||
float fExposureTime; // 이 프레임에 실제 적용된 노출시간
|
||||
unsigned int nFrameNum; // 프레임 번호 — 드롭 검출용
|
||||
unsigned int nAverageBrightness;
|
||||
unsigned int nRed, nGreen, nBlue; // 화이트밸런스
|
||||
unsigned int nTriggerIndex;
|
||||
unsigned int nLostPacket;
|
||||
```
|
||||
|
||||
그런데 현재 `captureLoop()`(`hik_camera_node.cpp:482-576`)는 이 구조체에서 `nDevTimeStampHigh/Low`, `nWidth`, `nHeight`, `nFrameLen`, `enPixelType`만 읽고 **나머지는 그냥 버린다.**
|
||||
|
||||
이게 특히 중요한 이유: cam1/cam2/cam3 전부 `exposure_auto: true`(Continuous AE)로 돌고 있다. 즉 프레임마다 실제 노출시간이 계속 바뀌는데, **그 실제값이 지금 bag 어디에도 남지 않는다** (3초 주기 콘솔 INFO 로그로만 잠깐 찍힘). design note가 강조하는 "AE는 명령과 실제 적용 사이 1~2프레임 지연이 있으므로 설정값이 아니라 실측값을 써야 한다"는 문제를 지금 데이터로는 사후에 풀 수 없다는 뜻이다.
|
||||
|
||||
**→ "카메라 설정만이라도 저장 가능한가"에 대한 답: 가능하고, 오히려 design note가 가정한 것보다 훨씬 쉬운 작업이다.** GenICam Chunk 기능을 새로 켤 필요 없이, 이미 파싱되어 들어오는 `fExposureTime`/`fGain`/`nFrameNum` 등을 읽어서 별도 토픽(작은 커스텀 메시지)이나 CSV 사이드카로 흘려보내기만 하면 된다. 드라이버에 몇 줄 추가하는 수준의 저위험 변경.
|
||||
|
||||
### 3.4 용량 재계산 — 12-bit raw가 오히려 더 작아질 수 있다
|
||||
|
||||
지금 실측: 1440×1080 해상도(`JUL8_calib.md` 캘리브레이션 결과와 일치), 3대, 트리거는 LiDAR(mid360, 10 Hz)와 맞춘 것으로 보이며 `acquisition_frame_rate` 기본값도 10 Hz — 이를 가정해서 계산한다 (STM32 PWM 실제 주파수로 재확인 권장).
|
||||
|
||||
| 포맷 | byte/px | 프레임당 (3대) | 11분(660s) 카메라 데이터량 |
|
||||
|---|---|---|---|
|
||||
| **현재: RGB8Packed** (디모자이킹 후 발행) | 3.0 | 14.0 MB | **≈ 92.4 GB** |
|
||||
| BayerRG12Packed (raw, packed) | 1.5 | 7.0 MB | ≈ 46.2 GB |
|
||||
| Mono16/bayer_rggb16 (raw, ROS 표준 인코딩) | 2.0 | 9.3 MB | ≈ 61.6 GB |
|
||||
|
||||
(계산: 1440×1080 = 1,555,200 px × byte/px × 3대 × 10 Hz × 660 s)
|
||||
|
||||
현재 92.4 GB는 사용자가 보고한 "11분에 약 100 GB"와 mid360(10 Hz 포인트클라우드)·IMU(200 Hz) 오버헤드를 더하면 거의 일치한다 — 10 Hz 가정이 대체로 맞는 것으로 보인다.
|
||||
|
||||
**반직관적이지만 중요한 결론:** 지금 드라이버는 베이어 원본(1 byte/px)을 강제로 디모자이킹해서 RGB8(3 byte/px)로 3배 부풀린 뒤 저장하고 있다. **12-bit raw로 전환하면 정보량(256→4096단계)은 늘지만, 파일 크기는 오히려 지금보다 33~67% 줄어든다.** zstd/lz4 무손실 압축(design note 6.4절)까지 적용하면 상위 4비트가 항상 0인 Mono16 컨테이너는 더 잘 압축된다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 측광 지표 개선 — "하늘 있으면 바닥이 어두워지는" 현상
|
||||
|
||||
design note 4.1절이 정확히 이 증상을 설명한다. 실제 구현을 보면 왜 그런지 명확하다.
|
||||
|
||||
현재 AE는 카메라 온보드 Continuous AE를 그대로 쓴다:
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:326-328
|
||||
MV_CC_SetEnumValue(camera_handle_, "ExposureAuto", 2); // Continuous
|
||||
MV_CC_SetIntValue(camera_handle_, "AutoTargetBrightness", ...);
|
||||
```
|
||||
|
||||
컨트롤 변수는 스칼라 하나(`AutoTargetBrightness`: cam1=110, cam2=128, cam3=145)뿐이고, 이건 **프레임 전체의 평균 밝기를 이 값에 맞추는** 방식이다 — design note 4.1절이 지적하는 "하늘(DN 255)이 평균을 끌어올려서 지면이 뭉개지는" 문제와 정확히 같은 구조.
|
||||
|
||||
SDK 헤더를 뒤져봐도 이 카메라가 "AE 통계 계산에만 쓰는 별도 관심영역(ROI)"을 지원한다는 근거는 없다. 찾은 AOI 관련 함수는:
|
||||
|
||||
```
|
||||
MV_CC_GetAOIoffsetX/Y, MV_CC_SetAOIoffsetX/Y
|
||||
```
|
||||
|
||||
이건 **촬영 자체를 크롭하는 AOI**(센서에서 어느 영역을 읽어올지)이지, "화각은 그대로 두고 AE 계산에서만 하늘을 빼는" 기능이 아니다. (다만 이 SDK는 상당수 GenICam 기능을 전용 C 함수 없이 문자열 노드명으로 설정하는 방식이라 — 코드의 `MV_CC_SetEnumValueByString(handle, "TriggerMode", ...)` 패턴처럼 — 카메라 자체가 별도 노드로 AE ROI를 지원할 가능성을 100% 배제할 순 없다. Hikvision MVS 프로그램의 Feature Tree에서 한 번 확인해볼 가치는 있음.)
|
||||
|
||||
**→ 실무적 결론: 카메라 온보드 AE로는 ROI 측광을 구현하기 어렵다. 대신 온보드 AE를 끄고 외부 루프로 대체하는 게 맞는 방향이고, 이 드라이버는 그걸 위한 훅을 이미 갖고 있다.**
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:601-620 (dynamicParametersCallback)
|
||||
if (name == "gain") { ... }
|
||||
else if (name == "exposure_time") { ... } // exposure_auto=false일 때 즉시 반영됨
|
||||
```
|
||||
|
||||
`exposure_auto: false`로 두고 `exposure_time`/`gain`은 런타임에 `ros2 param set`으로 바꿀 수 있다. 즉 design note 4.2절(ROI + 퍼센타일)이나 4.3~4.4절(gradient 기반)의 노출 컨트롤러를 **드라이버 코드를 건드리지 않고, `/camX/image`를 구독해서 지표를 계산하고 파라미터 서비스로 `exposure_time`/`gain`을 되쓰는 별도의 작은 ROS 2 노드**로 구현할 수 있다. design note 4.7절의 "1단계: ROI+퍼센타일 교체"가 정확히 이 형태로 바로 착수 가능하다.
|
||||
|
||||
**권장 순서:** design note 4.7절과 동일 — (1) ROI+퍼센타일 외부 노드부터 (개선폭 대비 구현 비용 최소), (2) 여유 있으면 `uzh-rpg/active_camera_exposure_control`의 `kShim`/`kGradient` 비교 평가.
|
||||
|
||||
---
|
||||
|
||||
## 5. 회전 오차 — "나무를 둘러싼 오차"가 맞는가, GPIO 절차가 가능한가
|
||||
|
||||
### 5.1 "나무 둘레 오차" 해석이 맞는가
|
||||
|
||||
**메커니즘 이해는 맞다. 다만 나무에 한정된 현상이 아니라, 회전 중 시야에 들어오는 모든 정지된 근거리 물체에 공통으로 적용되는 일반적 현상이다.**
|
||||
|
||||
design note 5.2절의 핵심은: 카메라 3대가 각자 다른 노출시간을 쓰면(AE가 독립적으로 도니까) 같은 트리거 엣지를 받아도 "유효 촬영 시각"(노출 중심)이 카메라마다 어긋난다. 회전 중에는 이 시간차(최대 ~1.4~2.45 ms)만큼 실제로는 카메라가 서로 다른 자세에서 그 장면을 봤는데, SLAM 파이프라인은 "같은 시각에 찍었다"고 가정하고 삼각측량/재투영을 한다 → 그 오차가 픽셀 단위로 나타난다.
|
||||
|
||||
이건 나무라는 물체 자체의 속성이 아니라 **"회전 중 시야에 잡힌 정지 물체 전반"**에 생기는 오차다. 다만 나무가 유독 눈에 잘 띄는 이유는 있다:
|
||||
|
||||
1. **특징점 밀도** — 가지·잎·수피 텍스처는 ORB/FAST가 조밀하게 코너를 잡는 대상이라, 오차가 "몇 개 점이 튀는" 수준이 아니라 나무 윤곽 전체에 걸쳐 조직적으로 어긋나 보인다(design note 2.2절의 코너 검출기 논의와 같은 맥락).
|
||||
2. **근거리 물체일수록 오차가 커짐** — 3.1절 블러 공식의 `f_px · v⊥ · t_exp / Z` 항을 보면 알 수 있듯, 같은 각속도라도 물체까지의 거리 `Z`가 가까울수록(=시차/파랄랙스가 클수록) 타임스탬프 오차가 만들어내는 겉보기 픽셀 이동량이 커진다. 나무는 대개 배경보다 가까운 전경 물체라 이 효과가 두드러진다.
|
||||
|
||||
즉 "나무를 둘러싼 오차"라는 관찰은 정확하고, 일반화하면 "핸즈헬드로 몸을 돌릴 때 시야 안의 가까운 텍스처가 풍부한 정지 물체 주변에서 특히 두드러지는 재투영 오차"다.
|
||||
|
||||
### 5.2 5.3절 GPIO 절차가 지금 카메라에서 가능한가
|
||||
|
||||
**하드웨어적으로는 가능성이 높다. 필요한 출력 라인이 물리적으로 비어 있다.**
|
||||
|
||||
데이터시트 전기 사양:
|
||||
|
||||
```
|
||||
Digital I/O: 6-pin P7 connector
|
||||
- opto-isolated input × 1 (Line 0)
|
||||
- opto-isolated output × 1 (Line 1)
|
||||
- bi-directional non-isolated I/O × 1 (Line 2)
|
||||
```
|
||||
|
||||
지금 시스템은 Line0만 쓰고 있다 (STM32 PWM → LINE0, 트리거 입력):
|
||||
|
||||
```cpp
|
||||
// hik_camera_node.cpp:157-158
|
||||
MV_CC_SetEnumValue(camera_handle_, "TriggerMode", 1);
|
||||
MV_CC_SetEnumValue(camera_handle_, "TriggerSource", MV_TRIGGER_SOURCE_LINE0);
|
||||
```
|
||||
|
||||
즉 **Line1(전용 출력 라인)이 3대 카메라 모두 현재 미사용 상태로 남아 있다.** design note 5.3-[A]가 요구하는 "ExposureActive/Strobe 신호를 오실로스코프로 측정" 절차에 쓸 출력 핀이 물리적으로는 준비되어 있다는 뜻이다.
|
||||
|
||||
다만 두 가지는 별도로 확인해야 한다 (코드나 데이터시트만으로는 100% 확답 불가):
|
||||
|
||||
1. **GenICam `LineSource`에 `ExposureActive`/`Strobe`가 옵션으로 있는가** — 이 SDK는 `ADCBitDepth`/`PixelFormat`/`TriggerMode`처럼 `MV_CC_SetEnumValueByString()`로 문자열 노드명을 직접 설정하는 방식이라, 헤더 파일만 봐서는 이 특정 라인 소스 옵션 목록이 드러나지 않는다. Hikvision MVS 프로그램으로 카메라 1대에 연결해서 Feature Tree → Digital IO Control → `LineSelector=Line1` → `LineMode`(Output인지) → `LineSource` 드롭다운에 `ExposureActive` 또는 `Strobe`가 있는지 확인하는 게 가장 빠르다.
|
||||
2. **실제 배선(6-pin 케이블)이 Line1을 브레이크아웃했는가** — 트리거 입력용으로 Line0만 배선하고 Line1은 커넥터 안에 묶여있지 않을 수 있다. 물리적으로 핀이 나와 있는지 확인 필요.
|
||||
|
||||
두 조건이 맞으면 design note 5.3-[A]의 오실로스코프 절차(CH1=트리거 입력, CH2=Line1 출력)를 그대로 3대에 적용할 수 있고, 나아가 확인된 노드명을 알면 이미 코드에 있는 것과 동일한 패턴(`MV_CC_SetEnumValueByString`)으로 자동화도 가능하다.
|
||||
|
||||
**권장 조치 순서:** (1) MVS로 Line1의 `LineSource` 옵션 확인 → (2) 물리 배선 확인 → (3) 가능하면 오실로스코프로 `t_latency` 3대 실측 (design note 5.3-[A]) → (4) 5.3-[C]의 `t_effective = t_trigger + t_latency + fExposureTime/2` 보정 적용 (이때 `fExposureTime`은 3번 항목에서 다룬, 이미 SDK가 프레임마다 주는 실측 노출시간을 써야 함 — 설정값이 아니라).
|
||||
|
||||
---
|
||||
|
||||
## 요약
|
||||
|
||||
| 질문 | 결론 |
|
||||
|---|---|
|
||||
| 1. 노출 하한 100 µs | 데이터시트상 15 µs까지 가능. 파라미터만 낮추면 됨 (코드 수정 불필요), 2.74 stop 추가 확보 |
|
||||
| 2. 게인 상한 16 dB | 정확함. 하드웨어 실측 상한 16.9 dB(스펙 17 dB)로, 이미 한계에 근접. 24 dB는 이 카메라로 불가능 |
|
||||
| 3. 12-bit raw + 메타데이터 | 카메라/SDK는 지원하나, 드라이버가 `RGB8Packed`로 강제 변환하는 코드(`enDstPixelType`)가 있어 지금은 실제로 저장 안 됨 — 코드 수정 필요. 메타데이터(`fExposureTime`/`fGain`/`nFrameNum`)는 이미 SDK가 매 프레임 주고 있어 추가 활성화 없이 곧바로 로깅 가능. 12-bit raw 전환 시 용량은 오히려 현재보다 33~67% 감소 예상 |
|
||||
| 4. 측광 지표 | 현재는 온보드 전체평균 AE라 하늘에 취약함이 확인됨. 카메라 자체에 AE 전용 ROI 기능은 안 보임 → `exposure_auto:false` + 외부 노드가 이미 지원되는 `exposure_time`/`gain` 파라미터를 실시간으로 되쓰는 방식으로 ROI+퍼센타일 측광 구현 가능 (드라이버 코드 변경 불필요) |
|
||||
| 5. 회전 재투영 오차 | "나무 둘레 오차" 관찰은 맞음 — 나무 특정 문제가 아니라 회전 중 근거리 텍스처 물체 전반의 일반 현상. GPIO 절차는 Line1(opto-isolated 출력)이 물리적으로 비어 있어 가능성 높음, MVS로 `LineSource` 옵션과 배선만 확인하면 됨 |
|
||||
@@ -0,0 +1,773 @@
|
||||
# 멀티카메라 + 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.6~1.3 ms, "보통 2~3 ms"는 광각 렌즈 가정 — 3.1절 참조)** |
|
||||
| 게인 상한 | 16 dB | **16.9 dB가 하드웨어 실측 상한(더 못 엶)**, 실사용 목표는 12 dB |
|
||||
| 측광 | 전체 평균 밝기 | **ROI + 퍼센타일**, 이후 gradient 기반 |
|
||||
| 타임스탬프 | 트리거 시각 | **트리거 + 지연 + 노출시간/2** |
|
||||
| 저장 | 8-bit | **12-bit raw + 프레임별 메타데이터** |
|
||||
|
||||
핵심 진단 세 가지:
|
||||
|
||||
1. **f/8~f/11은 회절 한계를 2~3배 넘어섰습니다.** 빛도 잃고 해상력도 잃는 이중 손해입니다.
|
||||
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, 100~5000 µs, 0~16 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 5~8)에 4~6 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 7~8) 하단조차 안정적으로 커버하지 못하고, "어두운 실내"(EV 5~6)는 이 조합으로는 원천적으로 불가능합니다. 어두운 쪽 stop을 더 벌려면 게인이 아니라 **더 밝은 렌즈(더 낮은 F No.) 또는 더 긴 노출 상한(3.1절의 실제 블러 한계 재계산 필요 — 아래 참조)** 쪽에서 찾아야 합니다.
|
||||
|
||||
이 계산을 스프레드시트나 짧은 파이썬 스크립트로 만들어 두면, 렌즈를 바꾸거나 상한을 조정할 때마다 즉시 검증할 수 있습니다.
|
||||
|
||||
```python
|
||||
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), 요약표의 "보통 2~3 ms"는 과도한 값입니다 — 실제로는 **0.6~1.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] 후처리에서 타임스탬프 보정**
|
||||
|
||||
```python
|
||||
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 매니페스트)
|
||||
|
||||
```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일, 현장 작업 포함)
|
||||
4. `aperture_sweep_eval.py` — 조리개별 촬영 이미지 폴더 입력 → ORB 특징점 수, 매칭 반복성, MTF50을 조리개별로 플롯
|
||||
5. `gain_noise_eval.py` — 게인별 SNR 및 특징점 품질 곡선 → `g_work` 결정
|
||||
6. 오실로스코프 측정으로 `trigger_latency` 3대 실측 (수작업, 결과를 YAML에 기록)
|
||||
|
||||
### Phase 3 — 타임스탬프 보정 (1~2일)
|
||||
7. Chunk Data 활성화 (`ExposureTime`, `Gain`, `Timestamp`, `FrameID`) 및 로깅 경로 구현
|
||||
8. `retimestamp.py` — `t_trig + t_latency + t_exp/2` 로 타임스탬프 재작성. rosbag 재작성 또는 사이드카 CSV 생성
|
||||
9. 보정 전/후 재투영 오차 비교 리포트 생성 (회전 구간 별도 집계)
|
||||
|
||||
### Phase 4 — 노출 제어 개선 (1~2주)
|
||||
10. ROI 마스크 생성 도구 (수동 폴리곤 또는 LiDAR 깊이 기반 자동)
|
||||
11. 퍼센타일 측광 컨트롤러 구현 및 교체 (4.2절)
|
||||
12. `uzh-rpg/active_camera_exposure_control` 통합, `kShim`/`kGradient` 비교 평가
|
||||
13. 3대 밝기 일관성 항 추가 (Shim 멀티카메라 방식)
|
||||
14. 노출 비율 2:1 제한 로직 추가 (5.4절 대안 2)
|
||||
|
||||
### Phase 5 — 후처리 파이프라인 (1주)
|
||||
15. `Mono12p` 전환, 대역폭/드롭 검증
|
||||
16. 광응답함수(CRF) + 비네팅 맵 캘리브레이션 도구
|
||||
17. 12→8 bit 톤매핑 파이프라인 (감마 / CLAHE / 로컬 톤매핑 선택 가능하게, 파라미터는 세션 전체 고정)
|
||||
18. 노출·게인 기반 광도 정규화 적용
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- 코드: https://github.com/uzh-rpg/active_camera_exposure_control
|
||||
- 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 — 광응답함수/비네팅 캘리브레이션 방법론)
|
||||
@@ -0,0 +1,27 @@
|
||||
# 질문지
|
||||
|
||||
> 대상 카메라: **MV-CS016-10UC** (Sony IMX273, 1/2.9", 3.45 µm, global shutter, 1440×1080) — `resource/`에 첨부된 데이터시트 및 `hik_camera_ros2_driver` 코드/설정 실측 주석 기준으로 아래 질문들을 갱신함. 상세 답변은 `answer_1.md` 참조.
|
||||
|
||||
## 1. 노출 하한 값을 100 µs보다 낮출 수 있는가
|
||||
데이터시트 기준 Standard exposure mode 하한은 15 µs다 (UltraShort 모드는 1~14 µs로 별도 모드). 현재 bag 녹화 시 3대 모두 `exposure_auto_min: 100.0`으로 설정되어 있는데, 15 µs 근방까지 낮추면 stop 계산법상 `log2(100/15) ≈ 2.74 stop`의 여유가 추가로 생긴다. 코드가 이미 쿼리하는 개체별 `ExposureTime` 노드의 실제 fMin 값(데이터시트는 스펙 상 보증치이며 개체 편차 가능)을 로그로 한 번 확인하고, `exposure_auto_min`을 실제 하한 근처로 낮춰 3대 모두 재검증이 필요한지 확인한다.
|
||||
|
||||
## 2. 게인 상한치를 16 dB보다 더 열 수 있는가
|
||||
데이터시트 기준 게인 범위는 0~17 dB이며, `camera_params_cam1/2/3.yaml`의 실측 주석에도 "Range: 0.0 ~ 16.9 dB"로 이미 확인되어 있다. 즉 현재 16 dB 설정은 여유를 둔 보수적인 값이 아니라 **하드웨어 상한(16.9 dB)에 거의 붙은 값**이며, 24 dB 등으로 더 여는 것은 이 카메라에서 물리적으로 불가능하다. cam2는 현재 15 dB로, 상한까지 1.9 dB(0.3 stop)밖에 남지 않은 상태다. 실내 진입 시 필요한 다크사이드 stop 여유를 게인이 아니라 조리개(f/2.8 등) 또는 노출 상한(3.1절 블러 계산 기준) 쪽에서 확보하는 방향으로 조정할지 결정한다.
|
||||
|
||||
## 3. 저장 시 12-bit raw 및 프레임별 메타데이터를 저장할 수 있는가
|
||||
현재 11분 촬영 시 3대 카메라(RGB8Packed) + mid360(10 Hz) + IMU(200 Hz) bag 용량이 100 GB 근방이며, 10 Hz 트리거 가정으로 역산한 카메라 데이터량(≈92 GB)과 대체로 맞아떨어진다.
|
||||
|
||||
드라이버(`hik_camera_node.cpp`)가 `convert_param_.enDstPixelType`을 `RGB8Packed`로 고정해 매 프레임 강제 변환·발행하므로, `pixel_format`/`adc_bit_depth` 파라미터를 12-bit로 바꿔도 **실제로는 12-bit raw가 bag에 담기지 않는다**는 것이 코드로 확인되었다. 이 강제 변환 로직을 없애고 raw 패스스루(예: `mono16`/`bayer_rggb16` 인코딩)로 바꾸는 작업을 언제 반영할지, 그리고 이때 예상 용량(현재보다 33~67% 감소 예상 — BayerRG12Packed 1.5 byte/px 또는 Mono16 2 byte/px vs 현재 RGB8 3 byte/px)이 실측으로도 맞는지 확인한다.
|
||||
|
||||
프레임별 메타데이터(노출시간/게인/프레임번호)는 SDK가 `MV_FRAME_OUT_INFO_EX` 구조체(`fExposureTime`, `fGain`, `nFrameNum`, `nAverageBrightness` 등)로 이미 매 프레임 제공하고 있는데, 캡처 루프가 이를 읽지 않고 버리고 있다는 것도 확인되었다 (GenICam `ChunkModeActive`를 새로 켤 필요 없이 바로 쓸 수 있음). 3대 모두 `exposure_auto: true`라 실제 노출값이 프레임마다 바뀌는데 지금은 bag에서 사후 확인이 불가능한 상태이므로, 최소한 실제 노출/게인 값만이라도 별도 토픽이나 CSV로 로깅하는 작업을 언제 반영할지 정한다.
|
||||
|
||||
## 4. 측광 지표 관련 논문/정리 내용으로 현재 측광 지표를 개선할 수 있는가
|
||||
현재 하늘이 포함된 이미지에서 바닥이 어두워지는 경향이 눈으로도 확인된다. 원인은 카메라 온보드 Continuous AE가 `AutoTargetBrightness` 단일 스칼라(전체 프레임 평균 밝기 기준)로만 동작하기 때문으로 확인되었다. SDK 헤더상 AE 계산에만 쓰는 별도 ROI 기능은 확인되지 않으며(`MV_CC_Set/GetAOIoffsetX/Y`는 촬영 크롭용), 이 부분은 Hikvision MVS Feature Tree에서 한 번 더 확인이 필요하다.
|
||||
|
||||
드라이버는 `exposure_auto: false` + 런타임 파라미터(`exposure_time`/`gain`, 이미 `dynamicParametersCallback`으로 지원됨)를 통해 외부 노드가 노출을 대신 제어할 수 있는 구조를 이미 갖고 있다. ROI+퍼센타일 측광(설계노트 4.2절)을 이 구조 위에 별도 ROS 2 노드로 구현하는 작업을 언제 착수할지 정한다.
|
||||
|
||||
## 5. 회전 시 재투영 오차와 GPIO 관련 절차 사용 가능 여부
|
||||
핸즈헬드 장비를 회전하며 녹화 시 나무 등 근거리 물체 주변에 재투영 오차가 나타나는 현상은, 나무 자체의 문제가 아니라 **회전 중 카메라 간 노출 중심 타임스탬프 어긋남이 근거리·고밀도 특징점 물체에서 특히 두드러지는 일반적 현상**으로 확인되었다 (근거: 5.2절 블러 공식의 `v⊥/Z` 항 — 거리가 가까울수록 겉보기 오차가 커짐). 이 진단이 맞다면, 다음을 확인해서 5.3-[A] GPIO 오실로스코프 절차를 실제로 진행할지 정한다:
|
||||
|
||||
- 데이터시트 확인 결과 카메라 I/O는 Line0(opto-isolated 입력, 현재 트리거용으로 사용 중), Line1(opto-isolated 출력, **현재 미사용**), Line2(양방향 비절연)로 구성되어 있어 출력 라인 자체는 물리적으로 비어 있다.
|
||||
- 다만 Hikvision MVS Feature Tree에서 Line1의 `LineSource`에 `ExposureActive`/`Strobe` 옵션이 실제로 존재하는지, 그리고 현재 6-pin 케이블이 Line1을 실제로 브레이크아웃했는지는 아직 미확인 — 이 두 가지를 확인해야 절차 진행 여부를 최종 결정할 수 있다.
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user