Files
VesiscanClinicalAndroid/app/src/main/java/com/example/medilightv2android/walldetect/DetectionSanity.kt
T
dw.jang 82634c2107 feat: Method C (V4.1) 대규모 업데이트 — 2-pass pipeline + ChordConsensus
새 알고리즘 모듈 (7개):
- ImpulseReject: 2-pass Hampel filter (lumen 내 impulse 제거)
- MedianFilter: running-median pre-filter (window=7, speckle 제거)
- MorphClose: morphological closing on lumen mask (disabled)
- StaLta: STA/LTA impulse detector (Allen 1978, wall edge vs reverberation)
- ChordConsensus: Tukey MAD outlier + Fischler-Bolles pair test
- DetectionSanity: detection sanity checks
- SweepStabilizer: sweep temporal stability

변경된 모듈 (9개):
- V41Detector: 2-pass pipeline (ImpulseReject.detectWithLumenClean),
  BModeScore V41_WEIGHTS (wamp+stalta), ChordConsensus filtering
- BvEstimation: ChordConsensus filter → trusted channel only
- DetectLumenFirst: running-median pre-filter, V41_PARAMS,
  mergeGapMax=5, Kremkau half-amplitude ant fallback
- AnatomicalGate: antDepthMin 22→10mm, strict threshold 제거
- BModeScore: V41_WEIGHTS (far=0.15, dark=0.10, wamp=0.45, stalta=0.20)
- WallSelect: MAX_PEAK_CANDIDATES_POST=64
- SpanUtils, Otsu, BvDispatchResult 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 17:01:55 +09:00

93 lines
3.2 KiB
Kotlin

/*
* Copyright (c) 2026 Medithings Co., Ltd.
* Author: Charles KWON OhJun <charleskwon@medithings.co.kr>
* Project: CharlesKWONsLaw — wall-detect live compare
*
* Stability check on V41 detection outputs. Complements SweepStabilizer:
* the stabilizer guards the INPUT, this guards the OUTPUT.
*
* Per-channel rules (relative to the previous live frame):
* • antIdx jump > maxIndexJump → unstable
* • postIdx jump > maxIndexJump → unstable
* • |chord change| / chord > maxChordRatio → unstable
*
* "unstable" is a flag for the diagnostic log, NOT a hard reject — V41's
* own anatomical gate already drops genuinely-bad detections. This layer
* just surfaces "this channel's reading just jumped, double-check".
*/
package com.example.medilightv2android.walldetect
import com.example.medilightv2android.walldetect.dto.ChannelResult
import kotlin.math.abs
class DetectionSanity(
private val maxIndexJump: Int = 8,
private val maxChordRatio: Double = 0.20,
private val channels: Int = 6,
) {
private val lastAnt = IntArray(channels) { -1 }
private val lastPost = IntArray(channels) { -1 }
data class ChannelReport(
val channel: Int,
val antJump: Int?,
val postJump: Int?,
val chordChangeRatio: Double?,
val unstable: Boolean,
)
fun reset() {
for (i in lastAnt.indices) { lastAnt[i] = -1; lastPost[i] = -1 }
}
fun check(perChannel: List<ChannelResult>): List<ChannelReport> {
val reports = mutableListOf<ChannelReport>()
for ((ch, cr) in perChannel.withIndex()) {
val ant = cr.antIdx
val post = cr.postIdx
val prevA = lastAnt.getOrElse(ch) { -1 }
val prevP = lastPost.getOrElse(ch) { -1 }
var antJump: Int? = null
var postJump: Int? = null
var chordRatio: Double? = null
var unstable = false
if (ant != null && prevA >= 0) {
val jump = abs(ant - prevA)
antJump = jump
if (jump > maxIndexJump) unstable = true
}
if (post != null && prevP >= 0) {
val jump = abs(post - prevP)
postJump = jump
if (jump > maxIndexJump) unstable = true
}
if (ant != null && post != null && prevA >= 0 && prevP >= 0) {
val curChord = (post - ant).toDouble()
val prevChord = (prevP - prevA).toDouble()
if (prevChord > 0.0) {
val r = abs(curChord - prevChord) / prevChord
chordRatio = r
if (r > maxChordRatio) unstable = true
}
}
reports += ChannelReport(
channel = ch,
antJump = antJump,
postJump = postJump,
chordChangeRatio = chordRatio,
unstable = unstable,
)
// Update state ONLY when we have a valid current detection,
// otherwise comparison against -1 sentinel resumes after gap.
if (ant != null) lastAnt[ch] = ant
if (post != null) lastPost[ch] = post
}
return reports
}
}