docs: low echo detection + Placement CV threshold 문서 업데이트

- Low echo: VALLEY_STOP_RISE=50, EDGE_DIST_DECAY=0.12, edge boundary, score 공식
- Placement: CV threshold 방식 전체 설명 (CV 계산, SI/LR 판정, severity, relaxation)
- 기존 urineLen score 방식 → CV threshold 방식으로 전면 교체

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 10:30:41 +09:00
parent 4cdd69ef3d
commit fde4d0ad05
+103 -42
View File
@@ -231,7 +231,7 @@ UrinAI가 "소변 있다!" 하면, **정확히 어디서 어디까지**인지
| 상수 | 값 | 의미 |
|------|-----|------|
| `lowEchoAmp` | **1150** | denoised ≤ 이 값 = low-echo (iOS: 1600) |
| `lowEchoAmp` | **1150** | denoised ≤ 이 값 = low-echo |
| `lowMinLen` | 3 | low-echo span 최소 길이 |
| `mergeGapMax` | 3 | 인접 span 합치기 최대 gap |
| `peakSearchWin` | 20 | 벽 peak 탐색 범위 |
@@ -239,8 +239,10 @@ UrinAI가 "소변 있다!" 하면, **정확히 어디서 어디까지**인지
| `minPeakMargin` | 30 | peak 최소 높이 = low_mean + 30 |
| `minUrineLen` | 3 | 최소 소변 구간 길이 |
| `maxPeakCandidates` | 3 | 방향별 최대 후보 수 |
| `valleyStopRise` | **50** | valley 스캔 시 상승 early stop 임계 (기존 10) |
| `edgeDistDecay` | **0.12** | edge 거리 penalty 계수 (신규) |
### 알고리즘 (Prominence 기반, 2026-04-20 업데이트)
### 알고리즘 (Prominence + Edge Distance, 2026-04-24 업데이트)
```
1. 디노이징된 신호에서 1150 이하인 연속 구간 찾기
@@ -248,17 +250,20 @@ UrinAI가 "소변 있다!" 하면, **정확히 어디서 어디까지**인지
2. 인접한 구간 합치기 (gap ≤ 3칸이면 합침)
단, gap 안에 1155 이상인 peak이 있으면 합치지 않음
(그건 벽일 수 있으니까!)
3. 양쪽에서 벽 peak 후보 찾기 (±20칸 범위)
후보 중 prominence가 가장 큰 것을 선택
- edge 포함 boundary: left [leftLo:edge+1], right [edge-1:rightHi+1]
- 후보 중 score가 가장 높은 것을 선택
prominence란?
= peak 높이 - 옆 valley(골짜기) 깊이
= "이 peak이 주변보다 얼마나 튀어나왔는가"
score = prominence / (1 + 0.12 × distance_from_edge)
전벽(ant): peak 오른쪽(소변 방향) valley 기준
후벽(post): peak 왼쪽(소변 방향) valley 기준
prominence = peak 높이 - valley(소변 방향)
distance = |peak위치 - low-echo edge|
→ low-echo에 가까운 peak이 더 높은 점수 (먼 peak은 패널티)
valley 스캔 시 최소값에서 50 이상 올라가면 early stop
(기존 10에서 50으로 상향 → 더 깊은 valley까지 탐색)
4. 후벽이 80칸 넘으면 → 구간 뒤쪽 절반에서 다시 찾기
@@ -413,52 +418,108 @@ Android: estimateBladderVolume6ch(6채널)
| - | - | ✅ | ❌ | "센서를 **오른쪽**으로 옮기세요" (RIGHT) |
| ❌ | ❌ | ❌ | ❌ | "방광이 안 잡힙니다. 센서 접촉을 확인하세요" |
### 2단계: urineLen 기반 정밀 가이드 (4채널 이상 잡힌 후)
### 2단계: CV threshold 정밀 가이드 (center 3채널 이상 잡힌 후)
4채널 이상 잡히더라도, 방광 **중앙**에 정확히 위치해야 측정이 정확합니다.
각 채널의 소변 구간 길이(urineLen)를 비교하여 정밀 가이드를 제공합니다.
각 채널의 **cos-corrected depth(mm)**의 변동계수(CV)로 배치 품질을 판정합니다.
### CV (Coefficient of Variation) 계산
```
잘 맞은 경우 (대칭): 치우친 경우 (비대칭):
CH0 ████████████ (긴) CH0 ██████ (짧)
CH1 ████████████████(긴) CH1 ████████████ (긴)
CH2 ████████████████(긴) CH2 ██████████ (중간)
CH3 ████████████ (긴) CH3 ████ (짧)
→ Score: 90% → Score: 45%, "Move UP"
1. 각 center 채널(CH0~CH3)의 ant/post wall 위치를 mm로 변환:
depth_mm = (post_mm - ant_mm) × cos(beam_angle)
2. 유효 채널(≥3개)의 depth 평균과 표준편차 계산:
CV = std(depths) / mean(depths)
3. 판정:
CV ≤ 0.15 → PASS (Best Placement)
CV > 0.15 → FAIL → 방향 안내
```
**세로 판정:**
- CH0의 urineLen이 CH3보다 1.5배 이상 → "Move UP" (방광이 위쪽)
- CH3이 CH0보다 1.5배 이상 → "Move DOWN" (방광이 아래쪽)
**가로 판정:**
- CH4가 CH5보다 5칸 이상 길면 → "Move RIGHT" (왼쪽 치우침)
- CH5가 CH4보다 5칸 이상 길면 → "Move LEFT" (오른쪽 치우침)
### Placement Score 계산
### 왜 CV인가?
```
점수 = 세로대칭(30%) + 가로균형(30%) + 중앙크기(40%)
잘 맞은 경우 (중앙): 치우친 경우 (가장자리):
CH0: depth=45mm CH0: depth=15mm ← 짧음
CH1: depth=52mm CH1: depth=48mm
CH2: depth=50mm CH2: depth=42mm
CH3: depth=42mm CH3: depth=10mm ← 짧음
세로 대칭 = 1 - |CH0_len - CH3_len| / max(CH0_len, CH3_len)
가로 균형 = 1 - |CH4_len - CH5_len| / max(CH4_len, CH5_len)
중앙 크기 = avg(CH1_len, CH2_len) / 40 (40칸이면 만점)
Score ≥ 80%: "Perfect position! 🎯"
Score ≥ 70%: Lock (측정 시작 가능)
Score < 50%: 방향 가이드 표시
CV = 0.09 → PASS ✅ CV = 0.52 → FAIL ❌
weakest = CH3 → "MOVE UP"
```
## "위치 확인" 버튼을 누르면 무슨 일이?
중앙을 관통하면 모든 채널의 depth가 비슷 → CV 작음.
가장자리를 스치면 일부 채널이 짧음 → CV 큼.
### SI(세로) 방향 판정
```
1. 앱이 센서에 "maa" 명령 (6채널 전부 측정해!)
2. 각 채널마다:
- PiezoEchoAnalyzer로 벽 탐지 + urineLen 계산
- 실패 시 UrinAI.detect()로 fallback
3. 감지된 채널 = 초록 (urineLen 숫자 표시), 안 된 채널 = 빨강
4. 1단계 or 2단계 가이드 표시 + Placement Score
5. Score ≥ 70% 또는 4채널 이상 → "Start Measurement" 버튼 활성화
deviation[i] = (depth[i] - mean) / mean
weakest = 가장 deviation이 큰 채널
severity:
|dev| < 0.10 → slight
0.10 ≤ < 0.25 → moderate
|dev| ≥ 0.25 → severe
방향:
weakest가 CH0/CH1 (top) → MOVE DOWN
weakest가 CH2/CH3 (bottom) → MOVE UP
```
### LR(가로) 판정
```
depth_CH4 = raw_depth × cos(SI_angle) × cos(LR_angle)
depth_CH5 = raw_depth × cos(SI_angle) × cos(LR_angle)
lr_dev = (depth_CH4 - depth_CH5) / mean(depth_CH4, depth_CH5)
|lr_dev| ≤ 0.15 → PASS
lr_dev > 0 → MOVE RIGHT (왼쪽이 더 길음)
lr_dev < 0 → MOVE LEFT (오른쪽이 더 길음)
```
### 최종 판정
```
is_pass = CV ≤ cv_thr AND |lr_dev| ≤ lr_dev_thr
cv_thr = 0.15 + repeatCount × 0.02 (반복 시 완화)
lr_dev_thr = 0.15 (고정)
```
### Threshold Relaxation (반복 완화)
```
Scan 흐름:
1회차: CV_thr = 0.15
2회차: CV_thr = 0.17
3회차: CV_thr = 0.19
...
n회차: CV_thr = 0.15 + n × 0.02
→ Best Placement가 아닌 경우 반복 측정 동안 기준을 완화하여
사용자가 무한히 조절하지 않도록 함
```
## 연속 스캔 동작
```
"Start Scanning" 버튼 탭
↓
즉시 첫 스캔 (maa 6채널)
↓ 결과: CV, 방향, Score 표시
3초 대기
↓
자동 재스캔 (repeatCount 증가 → threshold 완화)
↓
... 반복 (사용자가 Stop 누를 때까지)
is_pass = true → Lock → "Start Measurement" 활성화
```
---