fix(methodd): merge_close_spans prominence semantic — Python 1:1
Python `merge_close_spans(gap_peak_min_prom=X)` 는 gap 내 peak 가 양쪽 endpoint valley (max(sg[prev_e], sg[s])) 대비 상대 높이가 X 초과일 때 병합을 스킵한다. Kotlin 은 대신 absolute (peak > low_amp + X) 기준을 써서, valley 가 low_amp 보다 훨씬 낮은 경우 병합 스킵 임계가 Python 보다 높아지는 잠재 divergence 있었음. - SpanUtils.mergeCloseSpans 에 gapPeakMinProm 파라미터 추가, Python 우선순위 (prom 이 thr 을 override) 그대로 이식. - Legacy DetectLumenFirst 는 여전히 gapPeakThr (absolute) 사용 — peakCheckMinGap default 를 1 로 유지해 하위호환 (Python default 는 3). - MethodDSpan 은 신규 gapPeakMinProm 파라미터로 호출 전환. - SpanUtilsMergeCloseSpansTest 5 케이스 (prom split/merge, gap=0, absolute compat, 우선순위) 로 semantic 검증. Golden test 33 케이스 회귀 없음 (기존 케이스에선 두 semantic 이 동일 결과였음 확인). Method D wall 검출 semantic 이 Python 1:1 도달. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -29,26 +29,35 @@ object SpanUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage 1 merge — width-bounded amplitude-aware merge. Closes gaps
|
||||
* of width ≤ maxGap when the gap-peak amplitude is below gapPeakThr
|
||||
* (typically T + 50 ADC). Compatible with V2 / Otsu / scalar paths.
|
||||
* Stage 1 merge — width-bounded amplitude-aware merge.
|
||||
*
|
||||
* Note: gap-peak should be sampled from the ORIGINAL `sg` (not the
|
||||
* median-filtered envelope) because the running median can clip a
|
||||
* real wall peak to its plateau-median value, which on borderline
|
||||
* cases drops below T + GAP_PEAK_MARGIN and would erroneously merge
|
||||
* across the wall.
|
||||
* Peak guard (병합 skip 조건) — Python `merge_close_spans` 와 동일한 우선순위:
|
||||
* 1) [gapPeakMinProm] (prominence): `gap` 내 peak_amp 가 양쪽 valley
|
||||
* (sg[prev_e], sg[s]) 중 큰 쪽보다 이만큼 높으면 병합 skip.
|
||||
* `gap > 0` 인 모든 gap 에 적용. `sg` 필수. (Method D 표준)
|
||||
* 2) [gapPeakThr] (absolute): gap 내 peak_amp 가 절대 임계 초과 시 skip
|
||||
* (`gap >= peakCheckMinGap`). 둘 다 주면 (1) 만 사용 — Python 일치.
|
||||
* legacy V4 / DetectLumenFirst 경로 호환.
|
||||
*
|
||||
* gap-peak 는 원본 `sg` 에서 샘플 — median-filtered envelope 은 wall peak 을
|
||||
* plateau median 값으로 clip 해 borderline 케이스에서 잘못 merge 시킬 수 있음.
|
||||
*/
|
||||
fun mergeCloseSpans(
|
||||
spans: List<Span>,
|
||||
maxGap: Int,
|
||||
sg: DoubleArray? = null,
|
||||
gapPeakThr: Double? = null
|
||||
gapPeakThr: Double? = null,
|
||||
gapPeakMinProm: Double? = null,
|
||||
// Python default = 3 (Method D). Kotlin legacy DetectLumenFirst 호출자는
|
||||
// 기존 gap>=1 시맨틱에 의존 → 하위호환 위해 default = 1. Prom 모드는 항상
|
||||
// gap > 0 이라 이 값 미사용.
|
||||
peakCheckMinGap: Int = 1,
|
||||
): List<Span> {
|
||||
if (spans.isEmpty()) return emptyList()
|
||||
val ordered = spans.sortedBy { it.start }
|
||||
val merged = mutableListOf(ordered[0])
|
||||
val usePeak = sg != null && gapPeakThr != null
|
||||
val useProm = sg != null && gapPeakMinProm != null
|
||||
val useThr = sg != null && gapPeakThr != null && !useProm
|
||||
for (k in 1 until ordered.size) {
|
||||
val (s, e) = ordered[k]
|
||||
val last = merged.last()
|
||||
@@ -56,7 +65,14 @@ object SpanUtils {
|
||||
val gap = s - pe - 1
|
||||
if (gap <= maxGap) {
|
||||
var skip = false
|
||||
if (usePeak && gap >= 1) {
|
||||
if (useProm && gap > 0) {
|
||||
var mx = Double.NEGATIVE_INFINITY
|
||||
for (i in (pe + 1) until s) if (sg!![i] > mx) mx = sg[i]
|
||||
if (mx > Double.NEGATIVE_INFINITY) {
|
||||
val valley = maxOf(sg!![pe], sg[s])
|
||||
if ((mx - valley) > gapPeakMinProm!!) skip = true
|
||||
}
|
||||
} else if (useThr && gap >= peakCheckMinGap) {
|
||||
var mx = Double.NEGATIVE_INFINITY
|
||||
for (i in (pe + 1) until s) if (sg!![i] > mx) mx = sg[i]
|
||||
if (mx > gapPeakThr!!) skip = true
|
||||
|
||||
@@ -52,11 +52,14 @@ object MethodDSpan {
|
||||
val mask = BooleanArray(sg.size) { sg[it] <= lowAmp }
|
||||
val rawSpans = SpanUtils.contiguousTrueSpans(mask)
|
||||
.filter { (it.end - it.start + 1) >= params.lowMinLen }
|
||||
// Python 1:1: prominence 시맨틱 사용 (peak - max(sg[prev_e], sg[s]) > min_prom).
|
||||
// 이전 코드는 absolute threshold (peak > low_amp + min_prom) 를 사용해
|
||||
// valley 가 low_amp 보다 훨씬 낮은 경우 병합 스킵 임계가 Python 보다 높았음.
|
||||
val spans = SpanUtils.mergeCloseSpans(
|
||||
rawSpans,
|
||||
maxGap = params.mergeGapMax,
|
||||
sg = sg,
|
||||
gapPeakThr = lowAmp + params.gapPeakMinProm,
|
||||
gapPeakMinProm = params.gapPeakMinProm,
|
||||
)
|
||||
if (spans.isEmpty()) return null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user