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
* 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
@@ -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
@@ -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)
@@ -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