Files
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

270 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 질문지 답변
> `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` 옵션과 배선만 확인하면 됨 |