diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/core/DetailEnhance.kt b/android/app/src/main/kotlin/com/mag160c/thermal/core/DetailEnhance.kt index 293a65e..7da2119 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/core/DetailEnhance.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/core/DetailEnhance.kt @@ -44,6 +44,18 @@ package com.mag160c.thermal.core * near-silent on flat areas). The divisor floor is what keeps flat, noisy regions * quiet while edges still get pushed. * + * ## What the strength level actually controls + * + * Raising the level does two things at once: + * 1. it RAISES the contrast gate — a window is enhanced only when its 7x7 range + * clears `mapStrength / 32`, so low-contrast areas (where sensor noise lives) + * are excluded entirely at high levels; and + * 2. it RAISES the amplitude on strong edges, where the local contrast itself + * dominates the divisor. + * Where the contrast is small enough that the divisor floor applies, the amplitude + * works out to `strength / mapStrength` = `128 / gain` — independent of the level. + * That is why turning the level up sharpens the picture instead of boiling it. + * * ## Validation status — READ THIS * * The arithmetic above is a faithful transcription, and the structural properties diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewModel.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewModel.kt index 3e2209f..67c6dbb 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewModel.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewModel.kt @@ -102,8 +102,15 @@ class AnalyzeViewModel( // which showed up on device as two "min" markers a few pixels apart. if (recorded.minPos >= 0) mnPos = recorded.minPos if (recorded.maxPos >= 0) mxPos = recorded.maxPos + // Each probe keeps the temperature the CAPTURE recorded, because that is + // the number burned into the photo next to the marker. Re-measuring the + // NUC block gives a slightly different value (the sensor drifts between + // capture and reload), which showed on device as the photo saying + // "Pt2 26.1" while the panel said 25.9 for the same point. Only probes + // that somehow lack a recorded value are measured. val loaded = (parsed?.probes.orEmpty()).map { p -> - Probe(p.x, p.y, p.label, measureSensor(p.x, p.y) ?: (p.tempMc / 1000f)) + Probe(p.x, p.y, p.label, (p.tempMc / 1000f).takeIf { p.tempMc != 0 } + ?: measureSensor(p.x, p.y) ?: 0f) } withContext(Dispatchers.Main) { _render.value = bmp diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt index bbc6246..25d31e0 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/settings/SettingsScreen.kt @@ -247,17 +247,21 @@ fun SettingsScreen( text = { Column { Text( - "加强局部细节(官方同款 7×7 局部映射)。档位沿用官方 SDK 自己的" + - "定义:档位越高,纳入增强的弱对比区域越多(边缘强度本身不变)," + - "画面更通透,弱细节里的噪声也会一起显现。", + "加强局部细节(官方同款 7×7 局部映射)。档位越高:①对比度门槛越高," + + "只有更强的细节会被增强,弱对比区域(噪声所在处)被排除;" + + "②强边缘的增强幅度更大。所以高档位是「更锐但更挑」," + + "而不是「连噪声一起放大」。", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 8.dp), ) // Vendor levels: MAG_SetDetailEnhancement accepts 0..32 and the - // pipeline uses `level << 3`. Extra headroom is safe because the - // per-edge amplitude is level-independent (128/gain); the level - // only decides which windows clear the contrast threshold. + // pipeline uses `level << 3`. The level raises BOTH the contrast + // gate (a window qualifies only when its 7x7 range clears + // mapStrength/32) and the amplitude on strong edges (where the + // local contrast dominates the divisor). On flat, noisy areas the + // divisor floor holds the amplitude at 128/gain regardless of + // level, which is why cranking it up sharpens instead of boiling. Column( modifier = Modifier .heightIn(max = 320.dp) diff --git a/android/app/src/test/kotlin/com/mag160c/thermal/core/DetailEnhanceTest.kt b/android/app/src/test/kotlin/com/mag160c/thermal/core/DetailEnhanceTest.kt index f90e6ad..160b933 100644 --- a/android/app/src/test/kotlin/com/mag160c/thermal/core/DetailEnhanceTest.kt +++ b/android/app/src/test/kotlin/com/mag160c/thermal/core/DetailEnhanceTest.kt @@ -184,6 +184,53 @@ class DetailEnhanceTest { ) } + /** + * What the level knob really does (this is the sentence the settings dialog + * shows the user, so it is pinned here): + * - the level raises the contrast GATE: a window is enhanced only when its 7x7 + * range clears `mapStrength / 32` (= level * gain * 2 / 256 / 32), so weak + * detail — where sensor noise lives — is dropped at high levels; and + * - where a window IS gated in, the divisor floor keeps its amplitude near + * `128/gain` of the deviation, so it does not grow with the level. + * + * Window used below: 15 samples at 7200 and one at 7260 (range 60), centre 7260, + * gain 3000. + * level 8 (strength 64 -> mapStrength 1500): gate 1500 <= 60*32 = 1920 passes + * level 16 (strength 128 -> mapStrength 3000): gate 3000 <= 1920 FAILS -> dropped + */ + @Test + fun theLevelRaisesTheContrastGateAndDropsWeakDetail() { + val size = 16 + fun window(): IntArray { + val src = IntArray(size * size) + for (dy in 0 until 4) for (dx in 0 until 4) src[(dy * 2) * size + dx * 2] = 7200 + src[0] = 7260 + src[3 * size + 3] = 7260 + return src + } + fun changeAt(strength: Int): Int { + val gray = ByteArray(size * size) { 128.toByte() } + DetailEnhance(size, size).enhance(window(), gray, strength = strength, gain = 3000) + return (gray[3 * size + 3].toInt() and 0xFF) - 128 + } + assertTrue( + "level 8 (mapStrength 1500) must act on a range-60 window", + changeAt(64) != 0, + ) + assertEquals( + "level 16 (mapStrength 3000) must drop the same window: its contrast " + + "is below the gate", + 0, + changeAt(128), + ) + // and the amplitude is small wherever it does apply: the divisor floor caps + // it near 128/gain, so higher levels cannot make flat areas boil + assertTrue( + "amplitude must stay small, was ${changeAt(64)}", + kotlin.math.abs(changeAt(64)) <= 4, + ) + } + @Test fun doesNotChangeTheOverallLevelOfASmoothImage() { // on a smooth ramp the mean and the centre are equal, so no delta should be diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index 85fd340..91a8bdf 100644 --- a/build-artifacts/mag160c-app-debug.apk +++ b/build-artifacts/mag160c-app-debug.apk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16ed9057c541c6146a0afcef0c081a9c8e8733d93d6d6ed15c14f219f7ba9029 -size 12663188 +oid sha256:4ca5458de77631e86ec824b8050342d0a349a19657d7b4bc1a2219b26bfacdf4 +size 12741424 diff --git a/docs/android_app/session_state.md b/docs/android_app/session_state.md index aef47bd..83168de 100644 --- a/docs/android_app/session_state.md +++ b/docs/android_app/session_state.md @@ -578,11 +578,24 @@ extremes **合并成一次 draw**(原来分两次调用,彼此看不见) SetDetailEnhancement**(jadx 全量搜索无此符号),所以官方实时画面用的是 SDK 内部默认档——无法据此对齐具体档位,故本应用把它做成用户可见设置 (0/1/2/3/4/6/8 档),默认**关闭**(关闭时 `RenderPipelineTest` 逐位基线不受影响)。 -一个有用的推论:每档的边缘强度其实与档位无关(化简为 `128/gain`), -档位只决定哪些窗口通过对比度阈值——所以调高档位是"纳入更多弱对比区域", -而不是"把边缘推得更狠"。 -实机实测:级别 2 下仍 15.1fps,单帧绘制 11.3ms(滤波器约 +5ms,帧预算 66ms 内)。 +**档位到底控制什么(推导 + 真机 HF 实测,曾一度写反)**: +`amplitude = strength*(center-mean)/divisor`,`divisor = max(max-mean, mean-min, mapStrength)`, +`mapStrength = strength*gain*2>>8`(gain=1000 → 每档 62.5)。由此: +- 弱对比区域:divisor 被 mapStrength 兜住 → 幅度化简为 `128/gain`,**与档位无关**; +- 强边缘:divisor 由局部对比度主导 → 幅度**随档位线性增长**; +- 门槛 `mapStrength <= range*32` → 档位越高,**弱对比区域被整个排除**(噪声所在处)。 +所以高档位是"更锐但更挑",不是"连噪声一起放大"(设置页文案曾写反,已改)。 +真机 HF 能量实测:关 0.365、6 档 0.438、8 档 0.433(6/8 相近,符合"8 档门槛更高、 +只增强更强边缘")。该行为由 `theLevelRaisesTheContrastGateAndDropsWeakDetail` 锁定 +(window range=60:8 档 mapStrength 1500 通过、16 档 3000 被拒)。 + +**另一个一致性修复**:分析页原先把测温点拿去**重新测量** NUC,导致照片上烧录的 +`Pt2 26.1℃` 与面板显示的 25.9℃ 不一致(同极值那类漂移)。现在优先采用文件里 +记录的 `tempMc`(照片上印的就是它),仅在缺失时才测量。 + +实机实测:级别 2 下仍 15.1fps,单帧绘制 11.3ms(滤波器约 +5ms,帧预算 66ms 内); +6/8 档 9.9~10.3ms,同样不掉帧。 ### 8) 其他顺带修复(本轮真机复现)