diff --git a/app/src/main/java/com/medithings/vesiscan/managers/AlignmentAdvisorV2.kt b/app/src/main/java/com/medithings/vesiscan/managers/AlignmentAdvisorV2.kt index a5848ab..fa09917 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/AlignmentAdvisorV2.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/AlignmentAdvisorV2.kt @@ -568,7 +568,10 @@ class RollingAligner( } } lastAvg = avg - val dets = MethodDRunner.detectMultichannel(avg, methodDParams) + // 2026-07-06 (piezophantomtest ea3f15c): 정렬 위치 선택은 base 검출로 수행 + // (교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단). + // BV 산출은 별도 경로 (PiezoMonitoringView) 에서 applyCross=true 로 진행. + val dets = MethodDRunner.detectMultichannel(avg, methodDParams, applyCross = false) val summary = summarize(dets) val out = guide4.step(summary) lastSeenPhase = guide4.phase diff --git a/app/src/main/java/com/medithings/vesiscan/walldetect/MethodDRunner.kt b/app/src/main/java/com/medithings/vesiscan/walldetect/MethodDRunner.kt index d4bda61..e4a49f3 100644 --- a/app/src/main/java/com/medithings/vesiscan/walldetect/MethodDRunner.kt +++ b/app/src/main/java/com/medithings/vesiscan/walldetect/MethodDRunner.kt @@ -54,12 +54,19 @@ object MethodDRunner { /** * 6채널 (또는 N채널) raw 신호 → 채널별 MethodDResult? 리스트. * raw[ch] 길이가 다르면 그대로 처리 (각 채널 독립). + * + * @param applyCross false 면 교차채널 보정을 건너뛴 **base 검출**만 반환. + * 2026-07-06 이식 (piezophantomtest ea3f15c): + * 정렬 위치 선택 (`AlignGuide` / `select_alignment_position`) 은 base 검출로 수행해야 함 + * — 교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단. + * BV 산출은 applyCross=true 로 별도 수행. */ fun detectMultichannel( signals: List, params: MethodDParams = MethodDParams.DEFAULT, beamAnglesDeg: DoubleArray? = null, applyTgc: Boolean = true, + applyCross: Boolean = true, ): List { val angles = beamAnglesDeg ?: PiezoHW.degreeAll // 1) per-channel SG denoise (heavy + light) @@ -81,7 +88,9 @@ object MethodDRunner { ) } // 5) cross-channel 후처리 (Python `apply_cross_channel` 순서 고정) - applyCrossChannel(results, angles, lightTgc, heavyTgc, signals, params) + if (applyCross) { + applyCrossChannel(results, angles, lightTgc, heavyTgc, signals, params) + } return results } diff --git a/app/src/main/java/com/medithings/vesiscan/walldetect/algo/methodd/MethodDWallSelect.kt b/app/src/main/java/com/medithings/vesiscan/walldetect/algo/methodd/MethodDWallSelect.kt index f1ccaf6..5a2092c 100644 --- a/app/src/main/java/com/medithings/vesiscan/walldetect/algo/methodd/MethodDWallSelect.kt +++ b/app/src/main/java/com/medithings/vesiscan/walldetect/algo/methodd/MethodDWallSelect.kt @@ -22,6 +22,16 @@ import kotlin.math.pow object MethodDWallSelect { + // 경계 peak 구제 margin — 2026-07-06 이식 (piezophantomtest b252de0): + // 진짜 peak 이 탐색창 끝(post=hi) 에 걸리면 find_peaks_1d 가 바깥 샘플을 못 봐 + // local max 확정 실패 → shoulder 로 오분류(p1_1/v2 CH3). peak 확정용 view 만 + // 이 margin 만큼 바깥으로 넓히고 수용 범위는 [lo,hi] 유지 → farther peak 을 + // 새로 받지 않아 d_max 전역 확대 부작용 회피. + // post 전용 — ant 는 경계 확장이 벽을 surface 쪽으로 흔들어 채널 탈락시키는 + // 민감성이 있어 적용 X. + private const val BOUNDARY_PEAK_MARGIN: Int = 2 + + enum class Side { ANT, POST } enum class CType { PEAK, SHOULDER } @@ -96,20 +106,32 @@ object MethodDWallSelect { } if (hi <= lo) return WallResult(null, emptyList()) - val sub = DoubleArray(hi - lo + 1) { sig[lo + it] } + // 2026-07-06 (piezophantomtest b252de0): peak 확정용 view 를 경계 밖으로 margin 만큼 + // 확장 (post 전용). 수용 범위 [lo, hi] 는 아래서 유지. + val margin = if (side == Side.POST) BOUNDARY_PEAK_MARGIN else 0 + val vLo = lo + val vHi = minOf(n - 1, hi + margin) + val sub = DoubleArray(vHi - vLo + 1) { sig[vLo + it] } val relPeaks = PeakDetection.findPeaks1D(sub, 0, sub.size) val relShoulders = findShouldersLocal(sub) val candIdxType = mutableListOf>() - for (p in relPeaks) candIdxType += Pair(lo + p, CType.PEAK) - for (s in relShoulders) { - val idx = lo + s - if (candIdxType.any { it.second == CType.PEAK && abs(it.first - idx) <= 1 }) continue - candIdxType += Pair(idx, CType.SHOULDER) + for (p in relPeaks) candIdxType += Pair(vLo + p, CType.PEAK) + // 2026-07-06 (piezophantomtest b252de0): post 는 peak-only. + // shoulder(d2 변곡) 는 후벽을 깊은쪽으로 과확장 → BV 과대 (특히 30° 채널). + // ant 는 shoulder 유지 (near-field 전벽은 shoulder 로 잡히는 게 정상). + if (side != Side.POST) { + for (s in relShoulders) { + val idx = vLo + s + if (candIdxType.any { it.second == CType.PEAK && abs(it.first - idx) <= 1 }) continue + candIdxType += Pair(idx, CType.SHOULDER) + } } val scored = mutableListOf() for ((p, ctype) in candIdxType) { + // view 확장은 경계 peak 확정 전용 — 수용은 [lo, hi] 로 제한. + if (p !in lo..hi) continue val vIdx = adjacentValley(sig, p, edge) val prom = sig[p] - sig[vIdx] val gate = if (ctype == CType.PEAK) params.minPeakProm else params.minShoulderProm