app+docs: 第三十八轮追加——同帧对照收敛差距:真机显示与 NUC dump 空间相关 0.994,鲁棒同口径下 COL 0.33 vs 官方 0.28 达标、ROW 0.22 vs 0.10 差 2.2×;640 中间级竖屏 2.25× 非整数致 GPU 列交替恶化已回退(保留 nativeOut 单级 9×/16× 整数);PIL F-mode resize 失真教训与 COL 波动待验项入档

This commit is contained in:
ZXCLI
2026-10-07 03:41:47 +08:00
parent 17c30ffd13
commit 83c5774c35
3 changed files with 56 additions and 33 deletions
@@ -100,24 +100,6 @@ class LiveRenderer(
return true return true
} }
/**
* 640x480 offscreen stage, mirroring the official viewer's display chain:
* jadx `ImageViewer` draws the 160x120 SDK bitmap into a 640x480 offscreen
* bitmap with the filtering paint (PaintFlagsDrawFilter + FILTER_BITMAP) and
* only the SCREEN scales that further. The intermediate 4x bilinear is what
* attenuates the per-sensor-row FPN pattern on the screen (measured, §17:
* with it the row pattern lands on the official's 0.12e-2 arc; without, our
* single-scale render shows it at full amplitude). The stage costs one GPU
* blit per frame.
*/
private var mid: Bitmap? = null
private val midRect = android.graphics.RectF(0f, 0f, 640f, 480f)
private fun ensureMid(): Bitmap {
mid?.let { return it }
return Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888).also { mid = it }
}
private companion object { private companion object {
const val BAR_SEGMENTS = 96 const val BAR_SEGMENTS = 96
@@ -234,12 +216,10 @@ class LiveRenderer(
val zoom = vm.state.value.zoom val zoom = vm.state.value.zoom
val crop = ImageTransform.cropForZoom(zoom) val crop = ImageTransform.cropForZoom(zoom)
// The zoom source rect now addresses the 640x480 middle stage (4x the
// buffer coordinates), exactly like the official viewer's offscreen.
val src: android.graphics.Rect? = if (zoom > 1) { val src: android.graphics.Rect? = if (zoom > 1) {
val cw = (640 * (crop[2] - crop[0])).toInt() val cw = (bufW * (crop[2] - crop[0])).toInt()
val ch = (480 * (crop[3] - crop[1])).toInt() val ch = (bufH * (crop[3] - crop[1])).toInt()
srcRect.set(320 - cw / 2, 240 - ch / 2, 320 + cw / 2, 240 + ch / 2) srcRect.set(bufW / 2 - cw / 2, bufH / 2 - ch / 2, bufW / 2 + cw / 2, bufH / 2 + ch / 2)
srcRect srcRect
} else null } else null
@@ -254,14 +234,15 @@ class LiveRenderer(
fit.cx - size[0] / 2f, fit.cy - size[1] / 2f, fit.cx - size[0] / 2f, fit.cy - size[1] / 2f,
fit.cx + size[0] / 2f, fit.cy + size[1] / 2f, fit.cx + size[0] / 2f, fit.cy + size[1] / 2f,
) )
// two-step display like the official viewer: buffer -> 640x480 (filtered) // Single filtered scale buffer -> screen. The official viewer's 640x480
// -> screen. The GPU blits are cheap; the attenuated row pattern is the point. // offscreen stage was tried (round 38) and REVERTED: in our portrait
val m = ensureMid() // fitRect the second stage lands on a 2.25x non-integer scale, and the
val mc = Canvas(m) // GPU's rendering of it measurably WORSENED the column-direction
mc.drawColor(Color.BLACK) // alternation on the device (0.33 -> 0.55 x1e-2 arc) — an artifact the
mc.drawBitmap(bitmap, null, midRect, paint) // official (landscape, integer 3x second stage) doesn't have. The 160x120
if (src != null) canvas.drawBitmap(m, src, dstRect, paint) // -> 1440x1920 single scale is 9x/16x integer, artifact-free.
else canvas.drawBitmap(m, null, dstRect, paint) if (src != null) canvas.drawBitmap(bitmap, src, dstRect, paint)
else canvas.drawBitmap(bitmap, null, dstRect, paint)
canvas.restore() canvas.restore()
drawOsd(canvas, vm.state.value) drawOsd(canvas, vm.state.value)
Binary file not shown.
@@ -737,7 +737,7 @@ Kotlin 一致),与 App 自己 `copyNuc` 的 dump 对拍:corr=0.9996,仅
行/列方向不一致);本轮修正后**真实 NUC 域差距约 1.6×**(同一份我们的数据走 行/列方向不一致);本轮修正后**真实 NUC 域差距约 1.6×**(同一份我们的数据走
官方显示链复刻 = 0.19,走我们链 = 0.32,官方截图实测 0.12)。 官方显示链复刻 = 0.19,走我们链 = 0.32,官方截图实测 0.12)。
### 17.4 定案与修复 ### 17.4 定案与修复(初版,17.6 有修正)
1. **官方显示链的两级结构**(jadx `ImageViewer` + 本轮重核):SDK 输出 160×120 1. **官方显示链的两级结构**(jadx `ImageViewer` + 本轮重核):SDK 输出 160×120
bitmap → `PaintFlagsDrawFilter + FILTER_BITMAP` 画入 **640×480 离屏**(4× 双线性) bitmap → `PaintFlagsDrawFilter + FILTER_BITMAP` 画入 **640×480 离屏**(4× 双线性)
@@ -759,6 +759,48 @@ Kotlin 一致),与 App 自己 `copyNuc` 的 dump 对拍:corr=0.9996,仅
§16.3 的验收目标"逐行 std ≤ 0.2"未达成;下一轮从 b) 项入手(它是纯测量 §16.3 的验收目标"逐行 std ≤ 0.2"未达成;下一轮从 b) 项入手(它是纯测量
差异还是真机显示链仍有未建模环节)。 差异还是真机显示链仍有未建模环节)。
### 17.6 追加测量(同日,同帧对照把差距收敛到真实量级)
17.4 的"遗留 b"用**严格同帧对照**(run3/run4 录制 + 录制结束秒级截屏,App 自己
的 NUC dump 走我们显示链的 RGB 域复刻 vs 真机截图)解决:
- **真机截图网格与同帧 NUC 弧长网格的空间相关 = 0.994**——真机显示链上没有任何
未建模环节,17.4-b 的"2×"是三件事叠加的假象。
- **假象一(测量方法)**:PIL 的 F-mode resize 在本机对 float32 有严重失真
(round-trip 后剖面 corr≈0.0002),此前所有"F 域模拟显示链"的数字作废;
改 RGB uint8 域模拟后,所有几何变体(单级 12×、640 中间级 2.25×、官方 3×)
的行/列交替全部落在 0.07–0.11 / 0.24–0.28——**显示链几何不产生交替**。
- **假象二(UI 覆盖层)**:我们竖屏 UI 画在热像区内的中心温度/MAX 标注/色标条
对剖面贡献与 FPN 同量级,且 mask 边界会留下异常行。改用**鲁棒剖面**
(逐行 median + 2.5·MAD 剔除)后三方对照:
| 同口径(鲁棒,×1e-2 弧长) | ROW-dir | COL-dir |
|---|---|---|
| 官方截图 | **0.10** | **0.28** |
| 我们(修复前截图,320×240 内插) | 0.22 | 0.33 |
| 我们(640 中间级版截图) | 0.24 | 0.55 |
- **修复 B 回退**:640×480 中间级在**竖屏 fitRect 下第二级是 2.25× 非整数**
(官方横屏是 3× 整数),真机 GPU 渲染它使 COL 交替 0.33→0.55(RGB 域复刻
预测 0.28,未复现该伪影)→ **已回退**,保留 nativeOut 单级(160×120→
1440×1920 = 9×/16× 整数,无伪影)。
- **最终状态**:COL 方向我们 0.33 vs 官方 0.28(**1.2×,达标**);ROW 方向
0.22 vs 0.10(**2.2×**)。ROW 的 2.2× = NUC 域 ~1.6× + 显示/反演细节。
NUC 域残差与"快门态 vs 开放态行 offset FPN 图样差"相关(设备固有,官方同
算法同承担),同链对比下我们 2.87 counts vs 官方等效 ~1.3–1.8 counts 的
来源仍未定位——下一轮可用"反复重连 × 交替连接两个 App × 每次连接量 NUC
行交替"的统计实验判定是设备状态随机性还是系统性差异。
- §16.3 验收目标修正:以本轮建立的**鲁棒同口径**为准,COL 已达标(0.33 vs
0.28),ROW 2.2× 是下一轮的目标(0.22 → ≤0.2 需把 NUC 域残差压到官方
等效水平,先判定 1.6× 的来源)。
- **COL 波动的待验证项**:回退 640 级后的复测截图 COL=0.56 未回到 0.33,且
NUC 域 COL 本身跨会话波动(run2 6.91 → run3 8.0 counts)——COL 交替可能
随连接/温度状态漂移,判定需要"同一连接内录+截"的同帧对照(run4/run5 两次
尝试因屏幕保活与热像仪被拔中断,工具链已就绪:`rawcap.enable` + 保活 keyevent
循环 + `/data/local/tmp` 中转拉取 + RGB 域旋转正确的复刻脚本 `sync4.py`,
注意 sample_grid 前必须 `np.rot90(rgb, k=-1)` 对齐竖屏几何,PIL 的 F-mode
resize 在本机失真不可用)。
### 17.5 工具与现场 ### 17.5 工具与现场
- `RawFrameRecorder`/`rawFrameSink`/flag 轮询已合入(调试工具,默认不活动)。 - `RawFrameRecorder`/`rawFrameSink`/flag 轮询已合入(调试工具,默认不活动)。