03. 센싱과 비전 — 값이 있다는 것과 믿을 수 있다는 것은 다르다
출처: SR §6.3 (6.3.1 기능 범위 및 사용자 정보 / 6.3.2 센싱 정보 유효성 및 환경 판단 / 6.3.3 초음파 근접 위험 판단 / 6.3.4 오류·개인정보 및 데이터 유효성 / 6.3.5 엔진룸 CV 및 대상 동물 진입 판단) 관련 TBD: TBD-013(초음파 센서 개수·배치·근접 위험 거리), TBD-014(Vision 모델·신뢰도·FPS), TBD-015(ROA 판정 조건), TBD-022(센싱/Vision 데이터 최대 age), TBD-029~032(엔진룸 카메라 하드웨어·인식 기준·해제 시간·알림), TBD-038(센서/비전 오류 복구 판정 기준) 담당 노드: S32K144 Sensing Controller + Raspberry Pi 3 B Vision Module
0. 이 절의 주제는 "측정"이 아니라 "신뢰"다
§6.3.1은 측정 요구사항 7줄로 시작하지만, 나머지 네 개 소절(6.3.2~6.3.5)은 전부 **"이 값을 믿어도 되는가"**를 다룬다. 분량으로 봐도 신뢰 판정이 측정보다 네 배 많다.
센서 값 하나가 기능을 움직이려면 세 단계를 통과해야 한다: 측정 → 유효성 → 판정. 이 절 전체가 그 파이프라인의 명세다.
1. §6.3.1 — 기능 범위 및 사용자 정보
- 시스템은 외부 조도를 측정해야 한다.
- 시스템은 실내 온도를 측정해야 한다.
- 시스템은 실내 습도를 측정해야 한다.
- 시스템은 후방 물체와의 거리를 측정해야 한다.
- 시스템은 실내 카메라를 이용하여 탑승자 존재 여부를 판정해야 한다.
- 시스템은 실내 카메라를 이용하여 탑승자 인원 수를 판정해야 한다.
- 시스템은 센싱 정보의 유효 여부를 구분해야 한다.
- 시스템은 비전 판정 결과의 유효 여부를 구분해야 한다.
- HMI 는 유효한 센서 측정 값을 사용자에게 제공해야 한다.
- HMI 는 유효한 탑승자 판정 결과를 사용자에게 제공해야 한다.
- HMI 는 센서 오류와 통신 오류를 구분하여 표시해야 한다.
1-1. 측정 대상 5종과 그 소비처
| 측정 | 물리량 | 누가 쓰는가 |
|---|---|---|
| 외부 조도 | lux | §6.3.2 조명 점등 필요·소등 가능 판단 → Ambient Lighting(§6.4.3 조도 기반 밝기 조절) |
| 실내 온도 | °C | §6.3.2 공조 필요 판단 → Predictive Climate(§6.4.2) |
| 실내 습도 | %RH | §6.3.2 공조 필요 판단 → Window 기반 자동 환기(§6.4.2) |
| 후방 거리 | cm/m | §6.3.3 근접 위험 단계 → VSS 경고(§6.1.3), HMI 표시 |
| 실내 영상 | — | 탑승자 존재/인원 → 웰컴 차임(§6.1.1), ROA(§6.3.2), 자동 환기(§6.4.2) |
조도만 "외부"고 온습도는 "실내"다. 이유가 명확하다. 조도는 조명을 켤지 결정하므로 바깥이 밝은지가 중요하고, 온습도는 사람이 느끼는 환경이므로 실내가 중요하다. 이 비대칭이 센서 배치(TBD)를 결정한다.
1-2. "탑승자 존재"와 "탑승자 인원 수"는 별개 요구사항이다
한 문장으로 "시스템은 탑승자 존재 여부와 인원 수를 판정해야 한다"고 쓸 수도 있었는데 나눴다. §9의 분리 원칙이다.
실제로 두 판정은 난이도가 다르다.
| 판정 | 알고리즘 난이도 | 실패 양상 |
|---|---|---|
| 존재 여부 | 상대적으로 쉬움 — 사람 클래스가 하나라도 검출되면 됨 | 조명 변화, 가림에 강함 |
| 인원 수 | 어려움 — 겹침(occlusion), 부분 노출 처리 필요 | 뒷좌석 겹침 시 과소 계수 |
그래서 존재는 확실한데 인원 수는 틀릴 수 있다. 요구사항을 나눠 놓으면 "존재 판정은 통과, 인원 수 판정은 조건부"라는 검증 결과를 정확히 기록할 수 있다.
인원 수가 필요한 이유는 §6.3.2 마지막에 나온다.
"시스템은 등록 정원을 초과하는 인원을 감지한 경우 정원 초과 상태를 제공해야 한다."
1-3. HMI 요구사항 3줄의 의미
주어가 "시스템은"이 아니라 **"HMI 는"**이다. 표시 책임이 인터페이스에 명확히 있는 동작이기 때문이다(00편 §6의 주어 분류 참고).
핵심은 마지막 줄이다.
사용자 입장에서 두 오류는 다른 행동을 유발한다.
| 오류 | 사용자가 할 수 있는 일 |
|---|---|
| 센서 오류 | 센서가 가려졌나 확인, 정비소 방문 |
| 통신 오류 | 잠시 기다림, 재시동 |
같은 아이콘으로 뭉뚱그리면 사용자는 아무것도 판단할 수 없다.
2. §6.3.2 — 센싱 정보 유효성 및 환경 판단
- 시스템은 센싱 정보를 신뢰할 수 있기 전에는 해당 정보에 기반한 자동 기능을 수행하지 않아야 한다.
- 시스템은 비전 정보를 신뢰할 수 있기 전에는 해당 정보에 기반한 자동 기능을 수행하지 않아야 한다.
- 시스템은 특정 센서 오류가 발생하더라도 독립적으로 안전하게 사용할 수 있는 다른 센서 기능을 불필요하게 중단하지 않아야 한다.
- 시스템은 외부 조도가 정의된 기준 이하인 경우 조명 점등 필요 상태를 판단해야 한다.
- 시스템은 외부 조도가 정의된 기준 이상인 경우 조명 소등 가능 상태를 판단해야 한다.
- 시스템은 실내 온도가 정의된 기준을 초과한 경우 공조 필요 상태를 판단해야 한다.
- 시스템은 실내 습도가 정의된 기준을 초과한 경우 공조 필요 상태를 판단해야 한다.
- 시스템은 실내 고온과 유효한 탑승자 부재가 동시에 확인된 경우 실내 환경 개선 필요 여부를 판단해야 한다.
- 시스템은 실내 고온과 유효한 탑승자 존재가 동시에 확인된 경우 잔류 탑승자 위험 판단에 해당 정보를 사용해야 한다.
- 시스템은 차량 사용이 종료되고 도어가 잠긴 상태에서 실내 탑승자가 유효하게 감지되는 경우 잔류 탑승자 위험 상태를 제공해야 한다.
- 시스템은 등록 정원을 초과하는 인원을 감지한 경우 정원 초과 상태를 제공해야 한다.
2-1. 조도 판정이 두 문장인 이유 — 히스테리시스
시스템은 외부 조도가 정의된 기준 이하인 경우 조명 점등 필요 상태를 판단해야 한다.
시스템은 외부 조도가 정의된 기준 이상인 경우 조명 소등 가능 상태를 판단해야 한다.
"이하"와 "이상"이 **같은 기준값이면 경계에서 채터링(chattering)**이 발생한다. 조도가 기준값 근처에서 흔들리면 조명이 깜빡거린다. 터널 진입부, 가로수 그늘, 구름 이동에서 실제로 발생한다.
TBD-011이 이걸 명시적으로 다룬다: "조도 기준 Auto Light/Ambient 밝기 임계값 및 Hysteresis".
조도(lux)
↑
소등기준 ├───────────── ← 이 위로 올라가야 소등 (예: 120 lux)
│ ↑ │
│ │ ↓ ← 히스테리시스 밴드
점등기준 ├──────┴────── ← 이 아래로 내려가야 점등 (예: 80 lux)
│
└──────────────→ 시간
/* §6.3.2 + TBD-011 — 히스테리시스를 가진 조도 판정 */
typedef enum { LIGHT_OFF_OK, LIGHT_ON_NEEDED } light_need_t;
static light_need_t g_light_need = LIGHT_OFF_OK;
static void update_light_need(uint16_t lux, bool lux_valid)
{
/* §6.3.2 — 신뢰할 수 없으면 새 판단을 하지 않는다. 이전 상태 유지 */
if (!lux_valid) return;
if (g_light_need == LIGHT_OFF_OK) {
if (lux <= LUX_ON_THRESHOLD) g_light_need = LIGHT_ON_NEEDED;
} else {
if (lux >= LUX_OFF_THRESHOLD) g_light_need = LIGHT_OFF_OK; /* OFF > ON */
}
}
현재 상태에 따라 비교 기준이 바뀐다 — 이게 히스테리시스 구현의 전부다. 단순히 if (lux < TH) 하나로 쓰면 채터링을 막을 수 없다.
시간 기반 필터(디바운스)를 추가로 얹기도 한다. 히스테리시스는 크기 축, 디바운스는 시간 축에서 노이즈를 막으므로 둘은 상호 보완적이다.
함정 — 히스테리시스는 지연을 만든다 밴드를 넓게 잡으면 채터링은 사라지지만 반응이 늦어진다. 터널에 들어갔는데 조명이 3초 뒤에 켜지면 그것대로 문제다. TBD-011이 벤치 시험 항목인 이유.
2-2. 온도와 습도가 같은 결론으로 수렴한다
시스템은 실내 온도가 정의된 기준을 초과한 경우 공조 필요 상태를 판단해야 한다.
시스템은 실내 습도가 정의된 기준을 초과한 경우 공조 필요 상태를 판단해야 한다.
두 요구사항의 결과가 모두 **"공조 필요 상태"**다. 즉 OR 조건이다. 덥거나, 습하거나 — 둘 중 하나면 공조가 필요하다.
이건 구현에서 판단(judgment)과 측정(measurement)의 분리를 요구한다.
/* 측정값은 각각, 판단은 하나로 수렴 */
typedef struct {
int16_t temp_c10; /* 0.1 °C 단위 */
uint16_t humid_pct10; /* 0.1 %RH 단위 */
bool temp_valid;
bool humid_valid;
} cabin_env_t;
/* §6.3.2 — 공조 필요 판단 */
static bool climate_needed(const cabin_env_t *e)
{
bool need = false;
/* 각 센서는 독립적으로 판단에 기여 — §6.3.2 freedom from interference */
if (e->temp_valid && e->temp_c10 > TEMP_TH_C10) need = true;
if (e->humid_valid && e->humid_pct10 > HUMID_TH_PCT10) need = true;
return need;
}
습도 센서가 죽어도 온도 기반 판단은 살아남는다. 이게 §6.3.2의 세 번째 요구사항("특정 센서 오류가 발생하더라도 다른 센서 기능을 불필요하게 중단하지 않아야 한다")의 구체적 구현이다. if (!e->temp_valid || !e->humid_valid) return false;라고 쓰면 이 요구사항을 위반한다.
2-3. 실내 고온 × 탑승자 유무 = 완전히 다른 두 기능
이 부분이 §6.3.2에서 가장 중요하다.
실내 고온 + 탑승자 부재 → 실내 환경 개선 필요 여부 판단 (편의 기능)
실내 고온 + 탑승자 존재 → 잔류 탑승자 위험 판단에 사용 (안전 기능)
같은 온도 정보가 탑승자 유무에 따라 편의 기능이 되기도 하고 인명 안전 기능이 되기도 한다.
| 탑승자 부재 | 탑승자 존재 | |
|---|---|---|
| 판단 결과 | 실내 환경 개선 필요 | 잔류 탑승자 위험(ROA) |
| 대응 | Window 기반 자동 환기(§6.4.2), 선행 공조 | 비상 사이렌(§6.1.1), 원격 알림 |
| 성격 | 편의 — 사용자가 탈 때 쾌적하도록 | 안전 — 사람이 죽지 않도록 |
| 오판정의 대가 | 불필요한 창문 열림 | 인명 사고 |
그래서 탑승자 판정 정확도가 이 시스템에서 가장 중요한 인식 성능 지표가 된다. TBD-014(Vision 모델·신뢰도·FPS)와 TBD-015(ROA 판정 조건)가 여기 걸려 있다.
2-4. ROA — 잔류 탑승자 위험 상태
"시스템은 차량 사용이 종료되고 도어가 잠긴 상태에서 실내 탑승자가 유효하게 감지되는 경우 잔류 탑승자 위험 상태를 제공해야 한다."
조건이 셋(AND)이다.
차량 사용 종료 AND 도어 잠김 AND 탑승자 유효 감지 → ROA 위험 상태
각 조건이 왜 필요한가:
| 조건 | 없으면 무슨 일이 |
|---|---|
| 차량 사용 종료 | 운전 중에도 "잔류 탑승자"로 판정된다 (당연히 사람이 타고 있다) |
| 도어 잠김 | 문이 열려 있으면 스스로 나갈 수 있다. 갇힌 게 아니다 |
| 유효 감지 | 오검출(햇빛 얼룩, 인형)로 사이렌이 울리면 오경보 |
그리고 §6.1.1의 사이렌 요구사항과 조건이 다르다는 점을 주목해야 한다.
§6.3.2 ROA 위험 상태: 차량 사용 종료 + 도어 잠김 + 탑승자 유효 감지
§6.1.1 비상 사이렌: 차량 전원과 무관 + 실내 고온 + 잔류 탑승자 확인
§6.3.2는 "위험 상태를 만든다", §6.1.1은 "고온이 더해지면 사이렌을 낸다". 즉 2단계다.
위험 상태 제공과 사이렌 출력이 분리돼 있다는 게 설계적으로 중요하다. 잔류 탑승자가 있지만 아직 고온이 아니면 사이렌 없이 알림만 간다. 고온까지 겹치면 물리적 경보가 울린다. 위험 수준에 따라 대응이 단계적이다.
TBD-015: "ROA 판정 조건 — 전원/도어락/고온/탑승자 상태의 유지 시간 확정". 유지 시간이 핵심이다. 순간적인 오검출로 사이렌이 울리면 안 되므로, 조건이 일정 시간 지속되어야 한다.
3. §6.3.3 — 초음파 근접 위험 판단
- 시스템은 차량이 후진 기어 상태이고 후방 물체가 정의된 기준 거리 이내인 경우 근접 위험 상태를 생성해야 한다.
- 시스템은 후방 물체가 정의된 기준 거리 밖으로 이탈하거나 차량이 후진 기어 상태를 벗어난 경우 근접 위험 상태를 해제해야 한다.
- 시스템은 근접 거리를 VSS 경고에 사용할 수 있도록 제공해야 한다.
- 시스템은 근접 위험 단계를 HMI 및 VSS 경고에 사용할 수 있도록 제공해야 한다.
- 차량 내 HMI 는 후방 근접 위험 상태를 표시해야 한다.
- 차량 내 HMI 는 후방 근접 위험 상태가 유효한 경우 탑승자에게 경고해야 한다.
3-1. 생성 조건과 해제 조건이 대칭이 아니다
생성: 후진 기어 AND 기준 거리 이내
해제: 기준 거리 이탈 OR 후진 기어 이탈
생성은 AND, 해제는 OR. 드모르간 법칙상 이건 정확히 대칭이다 — ¬(A ∧ B) = ¬A ∨ ¬B. SR이 논리적으로 정합하게 쓰였다는 뜻이다.
이렇게 두 문장으로 쓴 이유는 §9의 분리 원칙이기도 하지만, 해제 조건을 명시하지 않으면 상태가 영원히 걸려 있는 버그가 실제로 자주 나기 때문이다. 상태를 만드는 코드는 누구나 쓰지만, 지우는 코드를 빼먹는다.
3-2. 초음파 거리 측정의 물리
초음파 센서는 **비행 시간(Time of Flight)**으로 거리를 잰다.
거리 = (음속 × 왕복시간) / 2
음속이 상수가 아니라는 게 함정이다. 공기 중 음속은 온도에 따라 변한다.
c(T) ≈ 331.3 + 0.606 × T [m/s], T는 섭씨
펌웨어가 음속을 20 °C 값(343.4 m/s)으로 고정해 두면, 실제 온도가 다를 때 측정 거리가 343.4 / c(T) 배만큼 어긋난다.
| 온도 | 실제 음속 c(T) | 343.4 / c(T) | 1 m 실제 거리의 측정값 |
|---|---|---|---|
| −20 °C | 319.2 m/s | 1.076 | 약 107.6 cm (+7.6 %, 멀게 나옴) |
| 0 °C | 331.3 m/s | 1.037 | 약 103.7 cm (+3.7 %) |
| 20 °C | 343.4 m/s | 1.000 | 100 cm (기준) |
| 40 °C | 355.5 m/s | 0.966 | 약 96.6 cm (−3.4 %, 가깝게 나옴) |
겨울 아침 −20 °C면 1 m 거리에서 7.6 cm가 어긋난다. 그것도 실제보다 멀게 나오는 방향이라 더 나쁘다 — 장애물이 실제로는 더 가까운데 경고가 늦게 뜬다. 근접 경고 단계 경계에 걸리면 한 단계가 통째로 밀린다.
/* 온도 보상 거리 계산 */
static uint16_t tof_to_mm(uint32_t tof_us, int16_t temp_c10)
{
/* c = 331.3 + 0.606·T [m/s] = 331300 + 606·T [mm/s]
temp_c10 은 0.1 °C 단위이므로 606·T = 60.6·temp_c10 ≈ (606·temp_c10)/10 */
int32_t c_mm_per_s = 331300 + (606 * (int32_t)temp_c10) / 10;
/* 왕복이므로 /2, us → s 이므로 /1e6 */
return (uint16_t)(((int64_t)c_mm_per_s * (int64_t)tof_us) / 2000000);
}
이 시스템은 실내 온도 센서를 이미 갖고 있다(§6.3.1). 외기온이 아니라 실내온이라 정확한 보상은 아니지만, 보상이 없는 것보다는 낫다. 이런 식으로 한 센서가 다른 센서의 정확도를 높이는 게 센서 융합의 가장 단순한 형태다.
3-3. 초음파의 한계 — 무엇을 못 보는가
| 실패 모드 | 원인 | 결과 |
|---|---|---|
| 흡음 물체 | 스펀지, 두꺼운 천, 눈 → 반사파가 약함 | 물체를 놓침 |
| 경사면 | 반사파가 센서로 안 돌아옴 (거울 반사) | 물체를 놓침 |
| 가는 물체 | 기둥, 봉 → 반사 단면적 부족 | 물체를 놓침 |
| 크로스토크 | 여러 센서를 동시 발신 → 남의 에코를 자기 것으로 오인 | 거리 오판 |
| 링잉(ringing) | 발신 직후 트랜스듀서 잔진동 | 최소 측정 거리 존재 (보통 20~30 cm) |
| 다중 반사 | 벽·바닥에서 여러 번 반사 | 실제보다 먼 거리 |
크로스토크는 소프트웨어로 반드시 해결해야 한다. TBD-013이 "초음파 센서 개수·배치 및 근접 위험 거리"를 확정 항목으로 두는데, 개수가 여럿이면 크로스토크가 즉시 문제가 된다.
해법은 **순차 발신(round-robin)**이다.
/* 센서를 하나씩 돌아가며 발신 — 크로스토크 회피 */
static uint8_t g_active_sensor;
void us_cycle_tick(void) /* 예: 60 ms 주기 */
{
us_stop_all();
us_trigger(g_active_sensor);
g_active_sensor = (g_active_sensor + 1u) % US_SENSOR_COUNT;
}
대가는 갱신 주기다. 센서 4개면 한 센서당 갱신 주기가 4배로 늘어난다. 최대 측정 거리 5 m이면 왕복 비행 시간이 약 29 ms이므로, 안전 여유를 두고 센서당 40~60 ms, 4개면 160~240 ms에 한 바퀴다. 후진 속도 5 km/h(≈1.4 m/s)면 그 사이 20~33 cm를 이동한다.
이 지연이 곧 TBD-013의 "근접 위험 거리"를 결정한다. 경고 거리는 "센서 갱신 지연 + 사용자 반응 시간 + 제동 거리"를 모두 흡수할 만큼 커야 한다.
3-4. 거리 vs 단계 — 두 가지를 모두 제공한다
시스템은 근접 거리를 VSS 경고에 사용할 수 있도록 제공해야 한다. ← 연속값
시스템은 근접 위험 단계를 HMI 및 VSS 경고에 사용할 수 있도록 제공해야 한다. ← 이산값
같은 측정에서 두 가지 표현을 뽑는다.
| 소비자 | 왜 | |
|---|---|---|
| 거리(연속값) | VSS | 경고음 간격을 거리에 비례해 연속적으로 줄이려면 원값이 필요 |
| 단계(이산값) | HMI + VSS | 화면에 바(bar)를 그리려면 단계가 필요. 히스테리시스를 단계 경계에 걸어 깜빡임 방지 |
TBD-013이 "근접 위험 단계 수(예: 5 단계) 포함"이라 하고, §6.1.3은 VSS 관점에서 2단계(주의/긴급)를 쓴다. 같은 데이터의 3가지 뷰가 존재하는 셈이다 — 원 거리, 5단계 HMI 표시, 2단계 VSS 음향. 이걸 한 변수로 뭉뚱그리면 어느 하나가 망가진다.
4. §6.3.4 — 오류·개인정보 및 데이터 유효성
이 소절은 데이터 유효성과 개인정보라는 성격이 다른 두 주제를 담고 있다.
4-1. 데이터 유효성
- 시스템은 센서 입력이 유효 범위를 벗어난 경우 해당 센서 정보를 정상 정보로 사용하지 않아야 한다.
- 시스템은 센서 정보가 정의된 갱신 기준을 만족하지 못하는 경우 해당 센서 정보를 정상 정보로 사용하지 않아야 한다.
- 시스템은 센서 오류의 복구 조건이 충족되기 전에는 해당 센서 정보를 정상 정보로 간주하지 않아야 한다.
- 시스템은 카메라 연결 상태가 인식 기준을 만족하지 못하는 경우 비전 결과를 정상 정보로 사용하지 않아야 한다.
- 시스템은 카메라 영상 품질이 인식 기준을 만족하지 못하는 경우 비전 결과를 정상 정보로 사용하지 않아야 한다.
- 시스템은 비전 오류의 복구 조건이 충족되기 전에는 탑승자 상태를 정상으로 확정하지 않아야 한다.
- 시스템은 센서 또는 비전 정보를 신뢰할 수 없는 경우 해당 정보에 기반한 새로운 자동 기능을 수행하지 않아야 한다.
- HMI 는 통신 오류 동안 마지막 정상 센서 값을 현재 정상 값으로 표시하지 않아야 한다.
유효성 판정의 세 축이 여기 다 나온다.
| 축 | 무엇을 잡는가 | 예 |
|---|---|---|
| 범위 | 물리적으로 불가능한 값 | 실내 온도 −80 °C, 습도 150 %, 거리 0 mm |
| 신선도(age) | 갱신이 멈춘 값 | 5초 전 온도를 현재 온도로 쓰기 (TBD-022) |
| 복구 이력 | 방금 고장났다 살아난 센서 | 1회 정상값으로 바로 신뢰하지 않기 (TBD-038) |
세 번째 축이 자주 빠진다. 센서가 간헐적으로 고장 나면 값이 정상/이상을 오간다. 정상값 하나가 오는 순간 바로 복구로 판정하면, 기능이 켜졌다 꺼졌다를 반복한다.
TBD-038이 정확히 이걸 다룬다: "센서/비전 오류 복구 판정 기준 — 연속 정상값 수신 횟수, 유지 시간 등 확정 (TBD-022 데이터 age 와 별개)".
"TBD-022와 별개"라는 단서가 중요하다. 오류 진입 판정과 복구 판정은 비대칭이어야 한다.
typedef struct {
bool healthy;
uint8_t ok_streak; /* 연속 정상 수신 횟수 */
uint32_t last_update_ms;
} sensor_health_t;
#define RECOVER_OK_STREAK 5u /* TBD-038 — 연속 5회 정상이어야 복구 */
#define MAX_AGE_MS 500u /* TBD-022 — 이보다 오래되면 신뢰 불가 */
static void sensor_feed(sensor_health_t *h, int32_t raw, int32_t lo, int32_t hi)
{
bool in_range = (raw >= lo && raw <= hi);
if (!in_range) {
h->healthy = false; /* 진입은 즉시 — 1회 이상값으로 바로 불신 */
h->ok_streak = 0;
return;
}
h->last_update_ms = hal_now_ms();
if (h->ok_streak < 255u) h->ok_streak++;
/* 복구는 보수적으로 — 연속 N회 정상이어야 (TBD-038) */
if (!h->healthy && h->ok_streak >= RECOVER_OK_STREAK) h->healthy = true;
}
static bool sensor_trusted(const sensor_health_t *h)
{
if (!h->healthy) return false;
/* 신선도 — 갱신이 멈췄으면 값이 정상 범위여도 신뢰 불가 (TBD-022) */
return (hal_now_ms() - h->last_update_ms) <= MAX_AGE_MS;
}
진입은 1회, 복구는 N회. 이 비대칭이 안전 설계의 기본이다. 위험 쪽으로는 민감하게, 안전 쪽으로는 둔감하게.
4-2. 비전 유효성은 두 겹이다
카메라 연결 상태가 인식 기준을 만족하지 못하는 경우 → 비전 결과 사용 금지
카메라 영상 품질이 인식 기준을 만족하지 못하는 경우 → 비전 결과 사용 금지
연결과 품질을 나눴다.
| 감지 방법 | 실패 예 | |
|---|---|---|
| 연결 | 프레임 수신 여부, FPS, 타임아웃 | 케이블 빠짐, RPi 다운, CSI 링크 끊김 |
| 품질 | 밝기 히스토그램, 블러 지표(라플라시안 분산), 노출 포화 비율 | 렌즈 오염, 역광, 야간 저조도, 김서림 |
연결은 되는데 품질이 나쁜 경우가 실제로 훨씬 흔하고 위험하다. 프레임은 계속 오는데 새까만 화면이면, 검출기는 "사람 없음"을 자신 있게 출력한다. 오검출이 아니라 미검출이고, ROA에서 미검출은 인명 사고다.
# Vision Module (RPi) — 품질 게이트
import cv2, numpy as np
def frame_quality_ok(frame) -> tuple[bool, str]:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mean = float(gray.mean())
if mean < 25.0: return False, "too_dark" # 야간·렌즈 가림
if mean > 230.0: return False, "too_bright" # 역광·노출 포화
# 라플라시안 분산 = 초점/블러 지표. 낮을수록 흐리다.
blur = cv2.Laplacian(gray, cv2.CV_64F).var()
if blur < 60.0: return False, "blurred" # 김서림·오염·흔들림
# 포화 화소 비율 — 부분 역광 검출
if float((gray > 250).mean()) > 0.30: return False, "saturated"
return True, "ok"
품질 게이트를 통과하지 못한 프레임의 추론 결과는 버려야지, 낮은 신뢰도로 사용하면 안 된다. SR이 "정상 정보로 사용하지 않아야 한다"고 쓴 이유다.
4-3. 개인정보
- 시스템은 실내 카메라 원본 영상을 탑승자 인식 목적 범위를 벗어나 상시 외부 전송하지 않아야 한다.
- 시스템은 비전 정보를 공유할 때 기능 수행에 필요한 최소한의 인식 결과만 제공해야 한다.
- 시스템은 엔진룸 카메라 영상을 대상 동물 진입 판정 목적 범위를 벗어나 외부로 전송하지 않아야 한다.
- 시스템은 엔진룸 카메라 영상을 상시 저장하지 않아야 한다.
세 가지 원칙이 요구사항으로 들어와 있다.
| 원칙 | 해당 요구사항 |
|---|---|
| 목적 제한 | "인식 목적 범위를 벗어나 … 전송하지 않아야 한다" |
| 데이터 최소화 | "기능 수행에 필요한 최소한의 인식 결과만 제공" |
| 보관 최소화 | "엔진룸 카메라 영상을 상시 저장하지 않아야 한다" |
이게 아키텍처에 직접적인 제약을 준다: 영상은 Raspberry Pi를 벗어나지 않는다.
CAN으로 나가는 건 이런 것들뿐이다.
/* 비전 → 차량 네트워크로 나가는 전부 (§6.3.4 최소화 원칙) */
typedef struct {
uint8_t occupant_present : 1; /* 탑승자 존재 */
uint8_t occupant_count : 4; /* 인원 수 (0~15) */
uint8_t animal_present : 1; /* 엔진룸 대상 동물 */
uint8_t vision_valid : 1; /* 유효성 */
uint8_t reserved : 1;
uint8_t confidence; /* 신뢰도 0~255 */
uint16_t age_ms; /* 이 결과의 나이 (TBD-022) */
} vision_result_t; /* 4 바이트 — 영상 대비 수십만 분의 1 */
이 구조체에 이미지가 없다는 게 요구사항 충족의 증거다. 코드 리뷰에서 확인할 수 있는 형태로 개인정보 요구사항이 구현에 반영된 셈이다.
함정 — "상시"라는 단어 "상시 외부 전송하지 않아야 한다", "상시 저장하지 않아야 한다". 완전 금지가 아니라 상시 금지다. 즉 디버깅·검증 목적의 일시적 저장/전송은 배제하지 않는다. 다만 그것이 기본 동작이 되어서는 안 된다. 실무에서는 개발 빌드와 양산 빌드를 컴파일 타임 플래그로 가르고, 양산 빌드에서는 영상 저장 코드가 아예 링크되지 않게 한다.
5. §6.3.5 — 엔진룸 CV 및 대상 동물 진입 판단
- 시스템은 엔진룸 카메라 영상을 이용하여 대상 동물의 진입 여부를 판정해야 한다.
- 시스템은 정의된 트리거 조건 또는 주기에 따라 엔진룸 대상 동물 진입 판정을 수행해야 한다.
- 시스템은 대상 동물 진입 판정의 신뢰도가 정의된 기준 미만인 경우 해당 결과를 확정 판단에 사용하지 않아야 한다.
- 시스템은 엔진룸 카메라 영상 품질이 인식 기준을 만족하지 못하는 경우 해당 결과를 확정 판단에 사용하지 않아야 한다.
- 시스템은 대상 동물 진입이 유효하게 확인된 경우 엔진룸 대상 동물 진입 상태를 생성해야 한다.
- 시스템은 엔진룸 내부에서 더 이상 대상 동물이 감지되지 않는 상태가 정의된 시간 이상 유지되는 경우 엔진룸 대상 동물 진입 상태를 해제해야 한다.
- 시스템은 엔진룸 대상 동물 진입 상태를 HMI, VSS 경고 및 모바일 인터페이스에 사용할 수 있도록 제공해야 한다.
- 차량 내 HMI 는 엔진룸 대상 동물 진입 상태를 표시해야 한다.
- 시스템은 엔진룸 대상 동물 진입 상태가 발생한 경우 권한이 있는 원격 사용자에게 즉시 알려야 한다.
- 모바일 인터페이스는 권한이 있는 원격 사용자가 엔진룸 대상 동물 진입 상태를 조회할 수 있도록 해야 한다.
5-1. 이 기능이 왜 존재하는가
겨울철 고양이가 따뜻한 엔진룸에 들어가 있다가 시동 시 팬 벨트에 말려 죽는 사고가 실제로 빈번하다. 한국에서는 "보닛 노크" 캠페인으로 알려져 있다. 이 기능은 그걸 자동화한 것이다.
5-2. 상시 추론이 아니라 트리거/주기 기반이다
"시스템은 정의된 트리거 조건 또는 주기에 따라 엔진룸 대상 동물 진입 판정을 수행해야 한다."
Raspberry Pi 3 B에서 CNN 추론을 상시 돌리면 전력과 발열이 감당이 안 된다. 차량이 주차된 상태에서 배터리로 며칠을 버텨야 하므로 더욱 그렇다.
TBD-036이 이걸 확정 항목으로 둔다: "엔진룸 CV 판정 트리거 조건 — 시동 시도 감지/도어 개방/정의된 주기 중 확정, 배터리 소모 및 자원 점유 최소화 관점 검토".
가능한 트리거들:
| 트리거 | 장점 | 단점 |
|---|---|---|
| 시동 시도 감지 | 가장 중요한 순간에 정확히 검사. 전력 최소 | 이미 늦을 수 있다 — 시동 걸리기 전에 판정을 끝내야 함 |
| 도어 개방 | 사용자가 차에 접근한 시점 = 곧 시동 | 도어를 안 열고 원격 시동하면 놓침 |
| 정의된 주기 | 진입 시점을 실시간에 가깝게 포착 | 배터리 소모. 주차 며칠이면 방전 위험 |
| 모션 트리거(PIR 등) | 저전력으로 상시 감시, 움직임 있을 때만 추론 | 부품 추가 |
현실적인 조합은 **시동 시도 + 도어 개방 + 저빈도 주기(예: 30분)**다. 배터리를 지키면서 결정적 순간을 놓치지 않는다.
5-3. 신뢰도 게이트 — 두 겹
신뢰도가 정의된 기준 미만 → 확정 판단에 사용 금지
영상 품질이 기준 미달 → 확정 판단에 사용 금지
§6.3.4의 비전 유효성과 같은 구조인데, 여기는 **모델 출력 신뢰도(confidence)**가 추가됐다.
TBD-030: "대상 동물 인식 모델/신뢰도 기준 — 유사 소동물(너구리 등) 오탐 처리 포함 벤치 시험 후 확정".
**"유사 소동물 오탐"**이 명시된 게 실무적이다. 고양이 검출기는 너구리·족제비·개를 고양이로 오분류하기 쉽다. 하지만 생각해 보면 — 오분류가 문제인가?
| 실제 | 판정 | 결과 |
|---|---|---|
| 고양이 | 고양이 | ✓ 정확 |
| 너구리 | 고양이 | 경고 발생 → 사용자가 보닛 열어 확인 → 동물이 실제로 있었으므로 유용 |
| 없음 | 고양이 | 오경보 → 사용자 신뢰 하락 |
| 고양이 | 없음 | 미검출 → 사고 |
즉 동물 종 구분 오류는 상대적으로 무해하고, 존재/부재 오류가 치명적이다. 그래서 신뢰도 기준은 "고양이일 확률"보다 "동물류가 있을 확률"로 잡는 게 안전하다. TBD-030이 벤치 시험을 요구하는 이유가 이 임계값 설정이다.
# 신뢰도 게이트 — 종 구분보다 존재 여부에 보수적으로
ANIMAL_CLASSES = {"cat", "dog", "raccoon", "small_animal"}
CONF_TH = 0.55 # TBD-030 — 벤치 시험으로 확정
def animal_present(detections) -> tuple[bool, float]:
ok, why = frame_quality_ok(frame)
if not ok:
return False, 0.0 # §6.3.5 — 품질 미달은 확정 판단에 사용 금지
best = 0.0
for d in detections:
if d.label in ANIMAL_CLASSES:
best = max(best, d.score)
return (best >= CONF_TH), best
5-4. 생성과 해제의 비대칭 — 다시
생성: 대상 동물 진입이 "유효하게 확인된 경우" → 즉시
해제: "더 이상 감지되지 않는 상태가 정의된 시간 이상 유지되는 경우" → 시간 조건
진입은 즉시, 해제는 지연. §6.3.4의 오류 복구 비대칭과 같은 철학이다.
왜 필요한가? 고양이는 엔진룸 안에서 움직인다. 카메라 사각지대로 들어가면 한두 프레임 검출이 끊긴다. 즉시 해제하면 "있음 → 없음 → 있음"이 반복되고, 사용자는 경고가 깜빡거리는 걸 보게 된다. 더 나쁜 건, 사용자가 "없음"을 보고 시동을 거는 순간이다.
TBD-031: "엔진룸 대상 동물 진입 상태 해제 유지 시간 — 오탐(그림자, 낙엽) 방지 기준 통합 확정".
/* §6.3.5 — 진입 즉시 생성, 해제는 지연 */
#define ANIMAL_CLEAR_HOLD_MS 30000u /* TBD-031 */
static bool g_animal_state;
static uint32_t g_last_seen_ms;
static void animal_update(bool detected_now, bool result_valid)
{
if (!result_valid) return; /* §6.3.5 — 확정 판단에 사용 금지 */
if (detected_now) {
g_last_seen_ms = hal_now_ms();
g_animal_state = true; /* 즉시 생성 */
return;
}
/* 미검출이 유지 시간을 넘겨야 해제 */
if (g_animal_state && (hal_now_ms() - g_last_seen_ms) >= ANIMAL_CLEAR_HOLD_MS) {
g_animal_state = false;
}
}
5-5. 알림 경로가 네 갈래다
엔진룸 대상 동물 진입 상태
├─ HMI 표시 (§6.3.5 "차량 내 HMI 는 … 표시해야 한다")
├─ VSS 경고 (§6.1.1 "고양이 존재가 유효하게 확인된 경우 알림 및 주의 음향")
├─ 원격 즉시 알림 (§6.3.5 "권한이 있는 원격 사용자에게 즉시 알려야 한다")
└─ 모바일 조회 (§6.3.5 "원격 사용자가 조회할 수 있도록 해야 한다")
"즉시 알려야 한다"(push)와 "조회할 수 있도록"(pull)이 별개 요구사항이다. 푸시는 사용자가 앱을 안 켜도 도달해야 하고, 조회는 사용자가 확인하고 싶을 때 볼 수 있어야 한다. 둘은 구현이 완전히 다르다.
TBD-032: "엔진룸 대상 동물 진입 시 원격 사용자 알림 방식 — Push 알림/SMS 등 모바일 인터페이스 구현 방식 및 이벤트 발생 대비 알림 전달 목표 시간 확정".
그런데 §7.1이 "원격 사용자 기능과 BLE Digital Key 는 프로토타입의 로컬 네트워크/BLE 환경에서 검증한다"고 했다. BLE 범위 밖에서는 푸시가 도달할 수 없다. 프로토타입에서는 로컬 Wi-Fi 안에서만 검증하고, 실제 푸시(FCM 등)는 범위 밖이다. 이 한계를 알고 시험 설계를 해야 한다.
TBD-034도 관련이 있다: "엔진룸 대상 동물 진입 상태 Ambient Lighting 우선순위 등급 — Warning/Fault/Emergency 중 적용 등급을 별도 티어 여부 확정". 즉 이 상태를 조명으로도 표시할지, 표시한다면 어느 등급으로 볼지가 미정이다. 06편에서 다룬다.
5-6. 비전 지연이 다른 기능을 막으면 안 된다
§6.3.4의 마지막 요구사항이 여기와 연결된다.
"시스템은 엔진룸 대상 동물 진입 판정 처리가 지연되거나 실패하더라도 실내 탑승자 인식 기능의 정상 동작을 방해하지 않아야 한다."
Raspberry Pi 3 B는 코어가 4개뿐이고, 두 개의 카메라 스트림에서 각각 추론을 돌려야 한다. 엔진룸 추론이 CPU를 다 먹으면 실내 탑승자 인식이 굶는다 — 그리고 실내 인식은 ROA(인명 안전)에 쓰인다.
우선순위가 명확하다: 실내 탑승자 인식이 엔진룸 판정보다 중요하다.
import threading, queue
# 실내 인식 — 높은 우선순위, 전용 스레드, 주기 보장
# 엔진룸 판정 — 낮은 우선순위, 트리거 기반, 실내 스레드를 굶기지 않음
occupancy_q = queue.Queue(maxsize=2) # 최신 프레임만 — 밀리면 버린다
engine_q = queue.Queue(maxsize=1)
def occupancy_worker(): # 상시 동작 (§6.3.1)
while True:
frame = occupancy_q.get()
publish_occupancy(infer_occupancy(frame))
def engine_worker(): # 트리거/주기 (§6.3.5)
while True:
frame = engine_q.get()
publish_animal(infer_animal(frame))
# maxsize 를 작게 두는 게 핵심 — 큐가 쌓이면 오래된 프레임을 처리하게 되고,
# 그건 TBD-022 의 max age 를 위반한다. 밀리면 버리는 게 맞다.
큐를 크게 잡으면 안 되는 이유 큐가 길면 프레임이 쌓이고, 추론 결과가 나올 때쯤 그 프레임은 이미 수 초 전 장면이다. §6.3.4는 "정의된 갱신 기준을 만족하지 못하는 경우 정상 정보로 사용하지 않아야 한다"고 하므로, 그 결과는 어차피 버려진다. 처리하고 버릴 바에는 처음부터 받지 않는 게 낫다. 실시간 시스템에서 큐 깊이는 처리량이 아니라 지연 예산으로 정한다.
6. 한 줄 요약
| 절 | 한 줄 |
|---|---|
| §6.3.1 | 조도(외부)·온도·습도(실내)·후방거리·실내영상. 탑승자 존재와 인원 수는 별개 요구사항 |
| §6.3.2 | 조도 판정은 히스테리시스. 온·습도는 OR로 공조 필요. 고온+부재=편의 / 고온+존재=ROA 안전 |
| §6.3.3 | 후진 AND 기준거리 → 위험 생성, 이탈 OR 기어해제 → 해제. 거리(연속)와 단계(이산)를 모두 제공 |
| §6.3.4 | 범위·신선도(TBD-022)·복구이력(TBD-038) 세 축. 영상은 RPi를 벗어나지 않는다 |
| §6.3.5 | 트리거/주기 기반 추론. 진입 즉시·해제 지연. HMI·VSS·푸시·조회 네 경로. 실내 인식을 방해 금지 |
설계 판단 체크포인트
구현에 들어가기 전에 팀이 답을 갖고 있어야 하는 것들이다. 답이 안 나오면 그 자리가 곧 설계 문의 또는 TBD 후보다.
-
조도 판정에 히스테리시스가 없으면 무슨 일이 일어나나? 그리고 히스테리시스만으로 부족한 경우는? 기준값 근처에서 조명이 채터링한다. 히스테리시스는 크기 축 노이즈를 막지만, 밴드를 크게 넘나드는 저주파 변동(구름 이동)은 못 막는다. 그때는 시간 축 필터(이동 평균, 디바운스)를 얹는다. TBD-011이 임계값과 히스테리시스를 함께 확정 항목으로 둔 것은 두 축을 같이 튜닝해야 하기 때문이다.
-
초음파 센서가 4개다. 순차 발신 대신 동시 발신하면 무엇이 좋고 무엇이 나빠지나? 갱신 주기가 4배 빨라진다. 대신 크로스토크로 A센서가 B센서의 에코를 받아 잘못된 거리를 낸다. 절충안은 물리적으로 멀리 떨어진 센서끼리 그룹을 지어 동시 발신하거나, 센서별로 다른 주파수/코드를 쓰는 것이다. 후자는 트랜스듀서 비용이 오른다.
-
비전 미검출(false negative)과 오검출(false positive) 중 ROA에서 더 위험한 것은? 임계값을 어느 쪽으로 기울여야 하나? 미검출이 압도적으로 위험하다 — 아이가 차 안에 있는데 없다고 판정하면 사망 사고다. 오검출은 불필요한 사이렌으로 끝난다. 따라서 탑승자 존재 판정의 임계값은 낮게(민감하게) 잡아야 한다. 다만 §6.1.1의 사이렌은 고온 조건과 AND이고, TBD-015가 유지 시간을 두므로, 낮은 임계값의 오경보를 상위 조건이 걸러 준다. 단일 임계값이 아니라 조건 체인 전체로 균형을 맞추는 설계다.
-
§6.3.4의 "상시 저장하지 않아야 한다"를 지키면서도 사고 원인 분석을 하려면? 판정 결과(구조체)와 메타데이터(타임스탬프, 신뢰도, 품질 지표)는 저장해도 개인정보 요구사항에 걸리지 않는다. 영상 자체가 아니라 판정 근거의 요약을 남기는 것이다. 필요하면 이벤트 발생 시점 전후로 한정된 짧은 구간만, 사용자 동의 하에 저장하는 정책을 별도로 정의해야 한다 — 이건 SR 범위 밖이고 개인정보 정책 문서의 몫이다.
-
엔진룸 판정을 "시동 시도 감지"에서만 하면 무엇이 문제인가? 시동 시도를 감지한 시점에 이미 스타터가 돌기 시작할 수 있다. 추론에 수백 ms가 걸리므로 판정이 끝나기 전에 엔진이 돈다. 그래서 TBD-036이 "시동 시도 감지/도어 개방/정의된 주기 중 확정"으로 세 가지를 열어 둔 것이고, 실제로는 도어 개방 시점에 미리 판정해 두고 시동 시점에는 캐시된 결과를 쓰는 설계가 현실적이다. 다만 그 캐시가 TBD-022의 max age를 넘으면 안 된다.
-
비전 결과 큐를 길게 잡으면 처리량이 올라가는데, 왜 짧게 잡아야 하나? 큐가 길면 오래된 프레임을 처리하게 되고, 그 결과는 §6.3.4의 갱신 기준(TBD-022)을 위반해 어차피 버려진다. 처리 자원만 낭비하고 지연은 늘어난다. 실시간 시스템의 큐 깊이는 처리량이 아니라 허용 지연으로 결정한다. 밀리면 오래된 프레임을 버리는 게(drop-oldest) 정답이다.