748ccbd796
D-P (legacy bcbc7b4) 로직 진단:
· Halir-Flusser ellipse fit · adaptive_large_bladder_relax · b_si_floor 미탑재.
· 300ml phantom 시연 값 = 130~150ml (원리상 큰 방광 처리 불가).
· 사용자 관찰 250ml 는 METHOD_D (Halir + adaptive) 의 결과 · D-P 로는 재현 불가.
조치:
· default = METHOD_D 복귀. 250ml 스케일 즉시 회복.
· D-P 는 dev-mode BV chip 에 "D-P" 로 남김 (phantom 소볼륨 실험 · 로직 대조용).
· Legacy fork 파일 (PiezoBVEstimatorLegacyDP.kt) 유지 · 삭제 안 함.
· DPS 1.968 은 유지 (사용자 별도 지시).
빌드: BUILD SUCCESSFUL 2s.
225 lines
13 KiB
Kotlin
225 lines
13 KiB
Kotlin
package com.medithings.vesiscan.managers
|
||
|
||
/**
|
||
* Green Zone 전체 파이프라인의 공유 상수 — Single Source of Truth.
|
||
*
|
||
* UrinAI, PeakDetection, Scoring 모두 이 상수를 참조.
|
||
* 파일 간 임계값 불일치 방지.
|
||
*
|
||
* 원본: xBench/GreenZoneConstants.kt (Charles KWON)
|
||
* 포팅일: 2026-04-17
|
||
*
|
||
* ⚠ 미세 조정 시 이 파일만 수정하면 전체 파이프라인에 반영됨.
|
||
* 각 상수의 의미와 영향 범위를 아래 주석 참고.
|
||
*/
|
||
enum class DetectionMethod { METHOD_A, METHOD_B, METHOD_C, METHOD_D }
|
||
enum class PlacementGuideMode { SIMPLE, BOUNDARY, SWEEP }
|
||
/**
|
||
* BV 계산 방법.
|
||
* - FRUSTUM : legacy analyzer walls + estimateBladderVolume6ch (Tanaka frustum)
|
||
* - V41 : Method C 의 SphereFit (methodCBvMl 직접 사용)
|
||
* - METHOD_D : Method D walls (cross-channel ant tie-break + OS-CFAR) + estimateBladderVolume6ch
|
||
* → Python `for_app_share` 임상 검증 pipeline 와 1:1 정렬 (2026-06-30).
|
||
*
|
||
* 사용 지침 (phantom 측정 vs 임상):
|
||
* - 구형 phantom (QC/calibration) : V41 (SphereFit) 권장. METHOD_D 는 lr_ratio < 1.0
|
||
* 영역에서 BV underestimate 가능 (대칭 phantom 검증 시 -38% 오차 사례).
|
||
* - 임상 인체 측정 : METHOD_D 권장. 인체 방광은 비대칭이므로 lr_ratio
|
||
* [0.5, 1.5] 자유 범위가 anatomically 올바름. Python algorithm = appshare 팀
|
||
* reference 동작과 정확 일치 (lr floor / shrinkage 의도적으로 없음).
|
||
* - METHOD_D 에 phantom-style lr floor (1.0) 를 추가하지 말 것. 사용자 결정
|
||
* 2026-06-30: "phantom QC 는 V41 로, 인체는 Python 1:1".
|
||
*/
|
||
// METHOD_D_P: 2026-08-11 신설. Python parity fix (Halir-Flusser ellipse fit ·
|
||
// adaptive_large_bladder_relax · b_si_floor · subsample refined 등) 도입 이전
|
||
// (bcbc7b4 · 2026-07-02) 시점 BV 로직 격리 fork. phantom (구 가정) 시연 재현용.
|
||
// demo-final default. 인체 (타원 방광) 정확도 필요 시 METHOD_D 사용.
|
||
enum class BvMethod { FRUSTUM, V41, METHOD_D, METHOD_D_P }
|
||
|
||
/** Sensor alignment 알고리즘 — V1=기존 (computePlacementGuide), V2=신규 (alignment.py 포팅). */
|
||
enum class AlignmentAlgo { V1, V2 }
|
||
|
||
object GreenZoneConstants {
|
||
|
||
@Volatile var postMaxIdx: Int = 80
|
||
|
||
// 2026-08-04: default detectionMethod METHOD_C → METHOD_D 로 변경했었음.
|
||
// 2026-08-10 ROLLBACK (demo-final 전용 · 사용자 요청): 8/4 이전 METHOD_C 로 복귀.
|
||
// METHOD_D 는 dev-mode 에서 수동 전환 가능. cloud-mvp 는 METHOD_D 유지.
|
||
@Volatile var detectionMethod: DetectionMethod = DetectionMethod.METHOD_C
|
||
// 2026-08-04: V41 → METHOD_D 로 변경 · 2026-08-10 ROLLBACK V41 로 복귀.
|
||
// 2026-08-10 RE-ADJUST (사용자 요청): demo-final default 최종 조합 =
|
||
// detectionMethod = METHOD_C (walls 검출 · phantom-검증 파이프라인)
|
||
// bvMethod = METHOD_D (BV 계산 · Python parity + adaptive_large_bladder_relax + b_si_floor)
|
||
// Method D BV 는 estimateBv(walls) wrapper 로 라우팅 (PiezoMonitoringView).
|
||
// 2026-08-11 (rev): default → METHOD_D 로 복귀.
|
||
// METHOD_D_P (legacy Frustum+cap) 는 Halir-Flusser · adaptive_large_bladder_relax
|
||
// 미탑재라 큰 방광 (~300ml phantom) 에서 원리상 값이 150 대까지 낮게 나옴.
|
||
// 사용자 관찰 250ml 는 METHOD_D (Halir+adaptive 적용) 결과. dev 토글에는 D-P 남김.
|
||
@Volatile var bvMethod: BvMethod = BvMethod.METHOD_D
|
||
|
||
/**
|
||
* lr_ratio 강제 override (algorithm 팀 최신 표준: piezophantomtest 4830e7c).
|
||
* - null : computed (기존 Python `compute_lr_ratio` 알고리즘)
|
||
* - Double : 강제 값 (algorithm 팀 default = 1.0)
|
||
*
|
||
* Phantom 실측 검증: computed lr=0.68 → BV -38%. lr=1.0 fixed → -1.6% (APP 일치).
|
||
* 6채널 hardware LR 분해능 한계로 computed lr 신뢰도 낮음. Dev toggle 전환 가능.
|
||
*/
|
||
@Volatile var lrRatioOverride: Double? = 1.0
|
||
|
||
@Volatile var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE
|
||
@Volatile var alignmentAlgo: AlignmentAlgo = AlignmentAlgo.V1 // default V1 (일반 사용자). V2 (Method D) 는 clinical alignment session 전용.
|
||
|
||
/** Posture pitch 임계값 (deg). |pitch| > 이 값 → LYING, ≤ 이 값 → NON_LYING. dev 모드에서 조정 가능. */
|
||
@Volatile var pitchLyingThreshold: Float = 25f
|
||
|
||
/** Posture acc magnitude gate min (g). 이 값 미만이면 classifier UNKNOWN. */
|
||
@Volatile var postureAccMagMin: Float = 0.8f
|
||
/** Posture acc magnitude gate max (g). 이 값 초과면 classifier UNKNOWN. */
|
||
@Volatile var postureAccMagMax: Float = 1.2f
|
||
|
||
/**
|
||
* Posture polling 간격 (ms). PiezoMonitoring idle 폴링 주기.
|
||
* 이력:
|
||
* 6/30 : mtb (1Hz × 15sample) → msp (5Hz × 1sample) — piezo 무음 확보, 주기 200ms
|
||
* (walking window 1.5초 안에 7-8 샘플 확보용)
|
||
* 7/03 : msp → mim (1Hz × 15sample, 무음) — piezo 무음 유지 + 시계열 15샘플 확보.
|
||
* 단일 폴링당 300ms 시계열 얻으므로 주기 1000ms 로 완화 (BLE 왕복 5배 감소).
|
||
* dev 모드에서 조정 가능.
|
||
*/
|
||
@Volatile var posturePollIntervalMs: Long = 1000L
|
||
|
||
// ── Walking detector 임계값 (flowchart 4-2 / 5-2) ─────────────────
|
||
/** Walking 인정 최소 pitch (deg). |pitch| > 이 값 이라야 walking 후보. */
|
||
@Volatile var walkingMinPitchDeg: Float = 45f
|
||
/** Walking 시작 — gyro 이동평균 (wmean) 임계 (dps). 실측: stand-up + 자세조정
|
||
* motion 이 5~10 dps 지속. 진짜 보행은 50+ dps. 10 으로 완화하여 짧은 보행도 잡음. */
|
||
@Volatile var walkingStartWmean: Float = 10f
|
||
/** Walking 시작 — gyro 이동표준편차 (wstd) 임계 (dps). */
|
||
@Volatile var walkingStartWstd: Float = 2f
|
||
/** Walking 시작 — 두 임계 모두 만족한 채 지속해야 하는 최소 시간 (ms).
|
||
* 실측: stand-up motion 이 3-5초 지속 → 4초는 아슬아슬. 대신 walkingStartWmean=10dps
|
||
* 가 stand-up (5-10dps) 을 이미 걸러줌 → 4초로 UX 반응성 확보 (사용자 요청 7/03).
|
||
* 실제 보행은 지속되므로 4초 이상 자동 만족. */
|
||
@Volatile var walkingStartHoldMs: Long = 4000L
|
||
/** Walking 유지 — gyro wmean 임계 (dps). 시작보다 낮음. */
|
||
@Volatile var walkingKeepWmean: Float = 2f
|
||
/** Walking 유지 — wmean 임계 미달 후 종료까지 grace 시간 (ms). 미만 지속이 이 값 넘으면 walking 종료. */
|
||
@Volatile var walkingKeepHoldMs: Long = 2000L
|
||
/** Walking detector sliding window (ms). 이 시간 분량 sample 평균/표준편차. */
|
||
@Volatile var walkingWindowMs: Long = 1500L
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 신호 범위
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** raw ADC 중 사용할 샘플 수.
|
||
* Kotlin 원본: 90. VesiScan maa 응답은 100 samples 전송하지만 끝부분 10개는
|
||
* CRC/noise 가능성 → 앞 90개만 사용.
|
||
* 영향: UrinAI 탐색 범위, PeakDetection 탐색 범위 */
|
||
const val useSamples: Int = 90
|
||
|
||
/** 초기 ringdown 스킵 (센서 근접 반사 무시).
|
||
* idx 0~2는 피에조 소자 직접 반사 → 항상 높은 값 → 액체로 오판 방지.
|
||
* 영향: UrinAI.fw 탐색 시작점, PeakDetection.fw 탐색 시작점 */
|
||
const val ringSkip: Int = 3
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 액체(소변) 판정 임계 — 12-bit ADC (0~4095)
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** Low-echo detection 임계값 (denoised 신호 기준).
|
||
* denoised ≤ 이 값 → low-echo (소변 후보).
|
||
* UrinAI의 liquidThrLoose(raw 기준)와 다름 — 여기는 TVD+SG 적용 후 신호 기준.
|
||
* Python 원본: 1150 (low_echo_detection_method_b.py LOW_ECHO_AMP, 6ch 버전)
|
||
* 영향: PiezoEchoAnalyzer → low-echo span 탐지, 벽 찾기의 기반 */
|
||
@Volatile var lowEchoAmp: Float = 1250f
|
||
|
||
/** 구조 이진화 임계값: raw < 이 값 → 액체(liquid), ≥ → 조직(tissue).
|
||
* 낮출수록 엄격 (더 확실한 액체만 인정), 높일수록 관대.
|
||
* Kotlin 원본: 1400.
|
||
* 영향: UrinAI.binarize → fw/bw 탐색의 기반 */
|
||
const val liquidThrLoose: Float = 1400f
|
||
|
||
/** 순수 액체 확인용 엄격 임계값: raw < 이 값 → "확실한 소변".
|
||
* liquidThrLoose보다 낮아야 함.
|
||
* 이 기준으로 연속 액체 구간(liquidRun) 측정.
|
||
* Kotlin 원본: 1100. VesiScan 실측에서 팬텀 low-echo가 1100~1400 범위.
|
||
* 1100→run=0, 1300→run=2 (미달). 1400으로 상향.
|
||
* liquidThrLoose(1400)와 동일하게 설정 — 팬텀에서는 loose/strict 구분 불필요.
|
||
* ⚠ 인체 측정 시 loose > strict 으로 재분리 필요할 수 있음
|
||
* 영향: UrinAI.maxRun 계산 → 최종 판정의 핵심 */
|
||
const val liquidThrStrict: Float = 1400f
|
||
|
||
/** 최소 연속 순수 액체 길이.
|
||
* maxRun ≥ 이 값이어야 "소변 있음" 판정.
|
||
* 소방광(50mL) 대응: chord≈24 중 순수 액체 5+ 필요.
|
||
* Kotlin 원본: 5.
|
||
* 영향: UrinAI 최종 판정 (detected 조건 1/3) */
|
||
const val minLiquidRun: Int = 5
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 벽 검출 범위
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** bw - fw 최소 (chord 최소 길이).
|
||
* 소방광 한계: 15 → 30mL(chord≈20) 이상 검출 가능.
|
||
* 50mL(chord≈24): margin 9 ✓
|
||
* 30mL(chord≈20): margin 5 ⚠
|
||
* 25mL 이하: 검출 불가 (임상적으로 PVR 50mL+ 의미 있는 범위)
|
||
* Kotlin 원본: 15.
|
||
* 영향: UrinAI 최종 판정 (detected 조건 2/3), PeakDetection bw 탐색 하한 */
|
||
const val minChord: Int = 15
|
||
|
||
/** bw - fw 최대 (이론적 상한).
|
||
* USE_SAMPLES(90)가 실질 제한.
|
||
* Kotlin 원본: 80.
|
||
* 영향: PeakDetection bw 탐색 상한 */
|
||
const val maxChord: Int = 80
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 품질
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** (wallPeak - lumenFloor) / wallPeak 최소.
|
||
* 벽과 소변 사이 대비가 이 값 이상이어야 유효.
|
||
* 높일수록 엄격 (선명한 벽 요구), 낮출수록 관대.
|
||
* Kotlin 원본: 0.15.
|
||
* 영향: UrinAI 최종 판정 (detected 조건 3/3) */
|
||
const val contrastMin: Float = 0.15f
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// Green Zone Finder
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** score ≥ 이 값 → center lock (Green Zone 확정).
|
||
* Scoring.THR_EXCELLENT(75)보다 낮아 빠른 UX 확보.
|
||
* Kotlin 원본: 70.
|
||
* 영향: Placement Guide에서 Good 판정 기준 */
|
||
const val lockThreshold: Int = 70
|
||
|
||
/** 최대 기억 측정 수 (히스토리).
|
||
* Kotlin 원본: 20. */
|
||
const val maxHistory: Int = 20
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 물리 상수 (교정용)
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
/** ADC 샘플링 레이트 (Hz). VesiScan HW 사양. */
|
||
const val fs: Double = 400_000.0
|
||
|
||
/** 기준 조직 음속 (m/s). */
|
||
const val cRef: Double = 1540.0
|
||
|
||
/** 교정용 팬텀 직경 (mm). V=530mL 구. */
|
||
const val dPhantomMm: Double = 100.406
|
||
|
||
/** 팬텀 용적 (mL). */
|
||
const val vPhantomMl: Double = 530.0
|
||
|
||
/** 팬텀 반지름 (mm). */
|
||
const val rPhantomMm: Double = 50.203
|
||
}
|