android: 修正增强档位说明(写反了)+ 分析页测温点沿用照片数值

上一提交把档位含义写成了「越高纳入越多弱对比区域」,方向是反的。按公式
amplitude = strength*(center-mean)/divisor, divisor = max(max-mean, mean-min, mapStrength),
mapStrength = strength*gain*2>>8 重新推导:
- 弱对比区域 divisor 被 mapStrength 兜住 → 幅度 = 128/gain,与档位无关;
- 强边缘 divisor 由对比度主导 → 幅度随档位线性增长;
- 门槛 mapStrength <= range*32 → 档位越高弱对比区域被整个排除(正是噪声所在处)。
即高档位是「更锐但更挑」。设置页文案与 DetailEnhance 文档已改正,
并加 theLevelRaisesTheContrastGateAndDropsWeakDetail 锁定
(range=60 窗口:8 档 mapStrength 1500 通过、16 档 3000 被拒)。
真机 HF 实测 0.365(关) / 0.438(6档) / 0.433(8档),与推导一致。

分析页测温点原先前先重新测量 NUC,于是照片上烧录的 Pt2 26.1℃ 与面板 25.9℃
不一致(与极值同一类漂移)。现在优先用文件记录的 tempMc——照片上印的就是它,
仅在记录缺失时才测量。真机确认照片/面板/极值三方数值完全一致。

真机(小米 22041211AC/Android12/MIUI,重启后冷启动):15.1fps 稳定、单帧 6.1~6.6ms、
6/8 档 9.9~10.3ms、拍照 extremes=2、分析页只有一个 min/max 且与照片重合、
底部面板可见(36.0/20.4/24.5)。101 项测试全绿。
This commit is contained in:
ZXCLI
2026-09-12 13:36:10 +08:00
parent f41668d5d1
commit e43e93025e
6 changed files with 96 additions and 13 deletions
@@ -44,6 +44,18 @@ package com.mag160c.thermal.core
* near-silent on flat areas). The divisor floor is what keeps flat, noisy regions * near-silent on flat areas). The divisor floor is what keeps flat, noisy regions
* quiet while edges still get pushed. * 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 * ## Validation status — READ THIS
* *
* The arithmetic above is a faithful transcription, and the structural properties * The arithmetic above is a faithful transcription, and the structural properties
@@ -102,8 +102,15 @@ class AnalyzeViewModel(
// which showed up on device as two "min" markers a few pixels apart. // which showed up on device as two "min" markers a few pixels apart.
if (recorded.minPos >= 0) mnPos = recorded.minPos if (recorded.minPos >= 0) mnPos = recorded.minPos
if (recorded.maxPos >= 0) mxPos = recorded.maxPos 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 -> 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) { withContext(Dispatchers.Main) {
_render.value = bmp _render.value = bmp
@@ -247,17 +247,21 @@ fun SettingsScreen(
text = { text = {
Column { Column {
Text( Text(
"加强局部细节(官方同款 7×7 局部映射)。档位沿用官方 SDK 自己的" + "加强局部细节(官方同款 7×7 局部映射)。档位越高:①对比度门槛越高," +
"定义:档位越高,纳入增强弱对比区域越多(边缘强度本身不变)," + "只有更强的细节会被增强弱对比区域(噪声所在处)被排除;" +
"画面更通透,弱细节里的噪声也会一起显现。", "②强边缘的增强幅度更大。所以高档位是「更锐但更挑」," +
"而不是「连噪声一起放大」。",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp), modifier = Modifier.padding(bottom = 8.dp),
) )
// Vendor levels: MAG_SetDetailEnhancement accepts 0..32 and the // Vendor levels: MAG_SetDetailEnhancement accepts 0..32 and the
// pipeline uses `level << 3`. Extra headroom is safe because the // pipeline uses `level << 3`. The level raises BOTH the contrast
// per-edge amplitude is level-independent (128/gain); the level // gate (a window qualifies only when its 7x7 range clears
// only decides which windows clear the contrast threshold. // 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( Column(
modifier = Modifier modifier = Modifier
.heightIn(max = 320.dp) .heightIn(max = 320.dp)
@@ -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 @Test
fun doesNotChangeTheOverallLevelOfASmoothImage() { fun doesNotChangeTheOverallLevelOfASmoothImage() {
// on a smooth ramp the mean and the centre are equal, so no delta should be // on a smooth ramp the mean and the centre are equal, so no delta should be
Binary file not shown.
+17 -4
View File
@@ -578,11 +578,24 @@ extremes **合并成一次 draw**(原来分两次调用,彼此看不见)
SetDetailEnhancement**jadx 全量搜索无此符号),所以官方实时画面用的是 SetDetailEnhancement**jadx 全量搜索无此符号),所以官方实时画面用的是
SDK 内部默认档——无法据此对齐具体档位,故本应用把它做成用户可见设置 SDK 内部默认档——无法据此对齐具体档位,故本应用把它做成用户可见设置
0/1/2/3/4/6/8 档),默认**关闭**(关闭时 `RenderPipelineTest` 逐位基线不受影响)。 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.4336/8 相近,符合"8 档门槛更高、
只增强更强边缘")。该行为由 `theLevelRaisesTheContrastGateAndDropsWeakDetail` 锁定
window range=608 档 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) 其他顺带修复(本轮真机复现) ### 8) 其他顺带修复(本轮真机复现)