441f249410
레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.
· post_tie_lock 후벽 상위 두 후보의 score 가 구조적으로 붙는 구간(전 데이터
5773 결정 중 격차<10% 가 17.9%)에서 직전 trace 선택을 유지한다.
방향을 강제하지 않는다 — 깊은-peak 고정·인접 골 tie-break 은
전부 계통 편향을 낳았고, tie 밴드에서는 정의상 두 후보의 증거가
같기 때문이다. 상태기를 넘길 때만 동작하며, 레퍼런스도 streaming
strict pass 에서만 건다. streaming.py 이식 전까지 호출부는 없다.
· _sibeam_cap_bounds 검출 채널이 정확히 2개일 때 미검출 이웃 빔까지의 SI 거리를
cap 상한으로. 2개뿐이면 끝 채널이 서로의 이웃이라 기존 top/bottom
clamp 가 성립하지 않아 cap 이 무제한 외삽된다. supine 은 건너뛴다.
· F83 적도-밖 bottom cap 연속 보간(실리콘·supine). 전제인
mirror_bottom_cap 도 함께 옮겼다.
좌표계가 두 개라는 것을 확인했다
레퍼런스 cross_channel._z 는 sample_to_ap_depth 를 인자 없이 불러 교차채널 좌표가
언제나 모듈 기본값(1.936/6.85)으로 떨어진다. BV 는 프리셋 값을 명시로 넘긴다.
abs 는 두 값이 같아 표가 안 나지만 실리콘(1.897/7.651)에서는 갈린다 — 실측
f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 439.88 vs 416.95 mL 이 됐다.
is_deep 판정이 34.209 > 34.042 처럼 아슬아슬한 자리라 작은 좌표 차가 선택을 뒤집는다.
의도인지 누락인지는 알고리즘팀 확인이 필요하다. 확인 전까지 레퍼런스와 같은 값을
내는 쪽을 택했고, 기존 경로는 프리셋 값을 그대로 쓴다.
missing_outside 의 null 과 빈 집합을 구분했다
null = 계산 안 함 → cap clamp 를 언제나 상한으로. 빈 집합 = 계산했고 '방광 밖'
채널이 없다 → 하한 쪽으로. ifEmpty { null } 로 뭉개면 "미검출이지만 span 은 있다"는
가장 흔한 경우에 si_floor 가 통째로 죽는다.
검증
· 레퍼런스 대조 6종 전부 불일치 0 — tie 1568행(lock 24건) · 실데이터 BV 784행
(nch=2 6건) · sibeam 120행(상한 102건) · 합성 BV 640행(mirror 226건) ·
F83 blend 440행 · F83 apply 12행. 실데이터가 미러·nch=2 에 거의 안 닿아
그 경로는 합성 입력으로 레퍼런스 함수를 직접 호출한 기대값으로 덮었다.
· 기존 경로 무변경 — 44 cycle × CCC on/off 덤프가 HEAD 와 88행 전부 일치.
· 전체 36개 통과 · demoDebug APK 빌드 성공.
LegacyEquivalenceDumpTest 가 PiezoHW.activePreset 을 명시로 고정하게 했다. 전역
가변 상태라 다른 테스트가 먼저 돌면 덤프가 통째로 달라진다 — 실제로 한 번 착시를
만들었다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
9.7 KiB
Markdown
156 lines
9.7 KiB
Markdown
# 알고리즘 경로 두 갈래 (`AlgoMode`)
|
||
|
||
demo-final 은 팬텀 시연과 임상 계측이 함께 도는 브랜치다. 여기에 신 저장소
|
||
(`vesiscan_pre_product/vesiscan_android_user`)가 레퍼런스
|
||
(`piezo-phantom-test/vesiscan_test`)와 1:1 로 맞춘 검출·BV 단계들을 옮기되,
|
||
**기본 동작은 그대로 두고** dev 패널에서만 켤 수 있게 했다.
|
||
|
||
- 코드: [`managers/AlgoMode.kt`](../app/src/main/java/com/medithings/vesiscan/managers/AlgoMode.kt)
|
||
- UI: 계측 화면 dev 패널 → **알고리즘** 스위치 (기본 꺼짐)
|
||
- 상태는 프로세스 수명 동안만 유지된다. 앱을 다시 켜면 기존 경로로 돌아간다.
|
||
|
||
## "기존" 이 가리키는 것
|
||
|
||
`ebddc52`(검출·BV 레퍼런스 정합) 와 `20a48ad`(실리콘 cap 억제) 가 들어간 **현재
|
||
demo-final HEAD** 다. 2026-09-03 임상이 이 코드 위에서 돌았으므로, 그 값을 움직이는
|
||
변경은 기본 경로에 넣지 않는다.
|
||
|
||
따라서 아래 항목들은 **양쪽 공통**이며 모드와 무관하다:
|
||
|
||
| 항목 | 값 | 들어간 커밋 |
|
||
|---|---|---|
|
||
| `d_max` · `inward_walk_win` | 15 · 4 | `ebddc52` |
|
||
| prominence 기준 | span 바닥 (`ANT_FLOOR_REF`) | `ebddc52` |
|
||
| cap 축 붕괴 폴백 | `CAP_AXIS_RATIO_MAX` 0.85 / `CAP_AXIS_DMAX_MAX` 100 | `ebddc52` |
|
||
| `ELLIPSE_CAP_R_MAX` · `TOP_CAP_EDGE_K` | 0.92 · 0.6 | `ebddc52` |
|
||
| `cap_d_adaptive` (실리콘) | ("aspect", 80.0, 2.0) | `20a48ad` |
|
||
| dps · delay | 프리셋 유도 (v\*=1.936/6.85, r\*=1.897/7.651) | 기존 |
|
||
|
||
## 스위치를 켜면 바뀌는 것
|
||
|
||
부분 적용은 없다. 단계들이 서로를 전제하므로 섞으면 어느 쪽도 아닌 값이 나온다.
|
||
|
||
### 1. 교차채널 보정 (CCC) — 3단계 → 7단계
|
||
|
||
| | 기존 | 레퍼런스 |
|
||
|---|---|---|
|
||
| 순서 | `ant_tiebreak` → NTV(Rule A+B) → `inward_post` | ①.9 `envelope_research` → ① `ant_tiebreak` → ①.7 Λ-dip → ①.8 `edge_post_recovery` → ①.95 `lumen_return_recovery` → ② NTV(**Rule A 만**) → ③ `fp_channel_gate` |
|
||
| `inward_post` | 있음 | **없음** |
|
||
| NTV Rule B | 있음 | 없음 |
|
||
|
||
레퍼런스 쪽 데이터에서 각 단계가 결과를 바꾼 횟수(파이썬 ablation, 1512 케이스):
|
||
`ant_tiebreak` 158 · `edge_post_recovery` 73 · Λ-dip 7 · `fp_gate` 4 · 나머지 0.
|
||
|
||
### 2. 검출 파이프라인 — 4개 단계 추가
|
||
|
||
| 단계 | 하는 일 | 왜 |
|
||
|---|---|---|
|
||
| 2 `scale_recovery` | 검출 **실패** 채널만 SG5 로 재탐색 | window 를 전역으로 바꾸면 모든 벽이 함께 밀려 부피가 통째로 이동한다. 실패 채널만 건드려 커버리지(nch)만 얻는다. |
|
||
| 4 `missing_outside` | span 조차 없는 채널 = 진짜 방광 밖 | **CCC 앞**에서 계산해야 한다. CCC 가 복구한 채널은 방광 밖이 아니다. |
|
||
| 5b `channel_confidence` | 약한 채널을 CCC **복구 단계에서만** 숨김 | 판정(NTV·FP) 전에 되돌린다. 끝까지 숨기면 CCC 가 그 채널을 FP 로 기각할 기회를 잃어 오히려 살아남는다. |
|
||
| 6 `CONF_DEMOTE` | CCC 후 여전히 약한 채널을 단면에서 제외 | 남는 채널이 3 미만이 되면 아무것도 하지 않는다. |
|
||
|
||
신뢰도는 **비-TGC** `min(ant_amp, post_amp) / lumen_min` 을 trace 최고값으로 나눈 값이다.
|
||
TGC 로 재면 원거리 이득이 약한 채널을 정상 채널에 붙여 놓는다(CH1↔CH3 간격 TGC 0.05 vs
|
||
비-TGC 0.21).
|
||
|
||
### 3. BV
|
||
|
||
- **`si_floor` / `missing_outside`** — 극 cap clamp 의 방향이 관측 증거로 갈린다.
|
||
미검출 채널에 span 이 **없으면** 방광이 그 빔에 없다는 뜻이라 **상한**(종전),
|
||
span 이 **있으면** 방광이 거기까지는 온다는 뜻이라 **하한**이다. 외삽을 관측으로
|
||
바꾸는 것이다. `SI_FLOOR_WALK` 는 span 이 이어지는 가장 바깥 채널까지 걸어간다.
|
||
- **bottom cap 제한 신설** — top 만 있고 bottom 은 제약이 전혀 없었다. sitting 에서
|
||
CH3(치골)이 소실될 때 bottom cap 이 과외삽된다.
|
||
- **중앙 채널 보간 은행가 반올림** — 레퍼런스는 `int(round((int(prev)+int(next))/2.0))`
|
||
이다. 보간 채널은 subsample 정밀도를 가질 근거가 없다(이웃 둘의 정수 위치뿐).
|
||
실측 f01 tr11: 이 차이로 CH2 단면이 2971.0 vs 2788.2 mm² 로 갈렸다.
|
||
- **`adaptive_large_bladder_relax` 제거** — 레퍼런스에서 빠진 완화다. 남아 있는 동안
|
||
`b_si` 를 0.85·`a_ap` 로 바닥 처리해 cap 축 비를 정확히 0.85 로 만들었고, 그래서 바로
|
||
다음의 cap 축 폴백(`< 0.85`)이 **경계에서 발동하지 않았다**.
|
||
실측 PHANTOM mean: Python 160.03 mL vs 완화가 살아 있는 코틀린 232.49 mL.
|
||
|
||
폴백 재계산에는 `missing_outside`·`si_floor` 를 **넘기지 않는다** — 레퍼런스
|
||
`runners.estimate_bv` 의 폴백 호출이 그 인자를 빼고 부르기 때문이다.
|
||
실측 f04 cyc20: 넘기면 249.95 mL, 안 넘기면 278.43 mL(= 레퍼런스 값).
|
||
|
||
## 검증
|
||
|
||
| 무엇을 | 어떻게 | 결과 |
|
||
|---|---|---|
|
||
| 기존 경로 무변경 | `LegacyEquivalenceDumpTest` 가 `align_cm{0,1}.csv` 44 cycle × CCC on/off 를 검출→BV 로 돌려 덤프. HEAD 코드와 작업트리 코드의 덤프를 대조. | **88 행 전부 일치** |
|
||
| 토글 배선 | `AlgoModeSwitchTest` — 켜면 값이 달라지고 `missing_outside` 가 산출되며, 끄면 기존 값이 정확히 재현되는지 | 44 cycle 중 **40 개가 갈림**, 끈 뒤 완전 복귀 |
|
||
| 정렬 판정 | 현행 레퍼런스 `AnchorGuide(ver='v1')` 를 `data123` 3 세션에 돌려 대조 | nch·ch3·hit **전부 일치** (0cm 3/X/2·11, 1cm 4/O/8·11, 3cm 3/O/11·11) |
|
||
| 전체 | `:app:testDemoDebugUnitTest` | **30개 전부 통과** |
|
||
|
||
`LegacyEquivalenceDumpTest` 는 의도적으로 `AlgoMode` 를 참조하지 않는다 — HEAD 에는 그
|
||
타입이 없어서, 참조하면 대조 자체가 불가능해진다.
|
||
|
||
## 이번에 함께 고친 것
|
||
|
||
- **테스트 픽스처 경로** — 저장소가 갈리면서 `c:/Projects/medilightv2android/data123` 가
|
||
없어졌다. 5개 테스트가 `FileNotFoundException` 으로 조용히 실패하고 있었다.
|
||
`c:/Projects/vesiscan-design-archive/data123` 로 옮겼다.
|
||
- **`CenterAlignerValidationTest` 기대값** — 위 픽스처가 깨져 있어 `ebddc52` 이후 이
|
||
테스트가 한 번도 돌지 않았다. 다시 돌리니 hit 7→8 · 10→11 로 어긋났는데, **현행
|
||
레퍼런스를 같은 세션에 돌려 보니 8 · 11 이었다** — 코틀린이 맞고 기대값이 낡은
|
||
것이다(구 `v3_python_reference.py` 기준, 현재 저장소에 없음). 기대값을 현행 레퍼런스
|
||
산출값으로 갱신했다.
|
||
`bv_cv` 는 현행 레퍼런스가 더 이상 내지 않는 값이라 대조할 수단이 없다. "Python 과
|
||
bit-exact" 라던 종전 주석은 근거가 없어져 걷어내고, 성격을 **회귀 고정**으로 바꿔
|
||
적었다.
|
||
|
||
## 나중에 추가된 3건 (2026-09-03)
|
||
|
||
`post_tie_lock` · `_sibeam_cap_bounds` · F83 을 마저 옮겼다. 셋 다 레퍼런스 경로에서만
|
||
의미가 있다.
|
||
|
||
| 항목 | 어디 | 기존 경로에 미치는 영향 |
|
||
|---|---|---|
|
||
| `post_tie_lock` | `walldetect/PostTieLock.kt` | 없음 — 상태기를 **넘길 때만** 동작하고, 넘기는 호출부가 없다 |
|
||
| `_sibeam_cap_bounds` | `PiezoBVEstimator.kt` | 없음 — `AlgoMode.reference` 로 막았다 |
|
||
| 미러 + F83 | `PiezoBVF83.kt` | 없음 — `mirrorBottomCap` 을 켜는 호출부가 F83 뿐이다 |
|
||
|
||
`post_tie_lock` 은 후벽 상위 두 후보의 score 가 구조적으로 붙는 구간에서 **직전 trace 의
|
||
선택을 유지**한다(방향을 강제하지 않는다 — 그렇게 한 앞선 시도들은 전부 계통 편향을
|
||
낳았다). 레퍼런스도 streaming 경로의 strict pass 에서만 걸고 정렬에서는 쓰지 않으므로,
|
||
`streaming.py` 를 옮기기 전까지는 클래스와 배선만 자리를 잡아 둔 상태다.
|
||
|
||
### 좌표계가 두 개다 — 레퍼런스를 그대로 따랐다
|
||
|
||
레퍼런스 `cross_channel._z` 는 `sample_to_ap_depth(idx, angle)` 를 **인자 없이** 불러
|
||
교차채널 좌표가 언제나 모듈 기본값(1.936 / 6.85)으로 떨어진다. 반면 BV 는 프리셋 값을
|
||
명시로 넘긴다. abs(vr\*)는 두 값이 같아 표가 안 나지만 **실리콘(r\*: 1.897/7.651)에서는
|
||
갈린다** — 실측 f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 부피가 439.88 vs
|
||
416.95 mL 이 됐다. `is_deep` 판정이 34.209 > 34.042 처럼 아슬아슬하게 걸리는 자리라
|
||
작은 좌표 차이가 그대로 선택을 뒤집는다.
|
||
|
||
의도인지 누락인지 **알고리즘팀 확인이 필요하다.** 확인 전까지는 레퍼런스와 같은 값을
|
||
내는 쪽을 택했고, **기존 경로는 프리셋 값을 그대로 쓴다** — 오늘 임상이 그 위에서 돌았다.
|
||
|
||
### 대조 결과
|
||
|
||
`ThreeFeatureParityTest` 가 모드 ON 상태에서 레퍼런스 산출 기대값과 대조한다.
|
||
실데이터는 abs 인체 0~4cm 라 미러·nch=2 에 거의 안 닿아, 그 경로는 합성 입력으로
|
||
**레퍼런스 함수를 직접 호출한** 기대값으로 덮었다.
|
||
|
||
| 대조 | 행 | 실제로 밟은 경로 |
|
||
|---|---|---|
|
||
| tie lock | 1568 | lock 발동 24 건 |
|
||
| 실데이터 BV | 784 | nch=2 6 건 |
|
||
| sibeam | 120 | 상한 산출 102 건 |
|
||
| 합성 BV | 640 | `mirror sphere` 226 건 |
|
||
| F83 blend | 440 | weight 0→1 |
|
||
| F83 apply | 12 | sub-window 재검출 |
|
||
|
||
전부 불일치 0.
|
||
|
||
## 아직 안 옮긴 것
|
||
|
||
`streaming.py`(연속 측정 상태기). `post_tie_lock` 의 자연스러운 소비처라 함께 설계해야
|
||
한다.
|
||
|
||
레퍼런스 저장소의 `library/analysis/abdomen.py` 가 커밋되어 있지 않아
|
||
`pipeline_dump.py` · `compare.py` 가 그대로는 돌지 않는다. 이번 대조는 스크래치 사본에
|
||
호출 시 즉시 실패하는 스텁을 놓아 우회했다(정렬·검출·BV 경로는 이 모듈을 쓰지 않는다).
|