# 질문지 답변

> `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` 옵션과 배선만 확인하면 됨 |
