From 3510bdc7740a8fa60c88f99bf29bddbab92166fb Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Tue, 6 Oct 2026 04:47:36 +0800 Subject: [PATCH] =?UTF-8?q?app+docs:=20=E5=8F=82=E8=80=83=E5=B8=A7?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=8F=82=E6=95=B0=E5=8C=96(baselineFrames,?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A44)=E2=80=94=E2=80=94=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E5=BB=B6=E9=95=BF=E5=88=B08=E4=BC=9A=E6=8A=8A=E5=BC=80?= =?UTF-8?q?=E5=BF=AB=E9=97=A8=E5=B8=A7=E6=94=B6=E8=BF=9B=E5=8F=82=E8=80=83?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E4=B8=8D=E7=A8=B3=E5=AE=9A=EF=BC=9B=E7=BB=93?= =?UTF-8?q?=E8=AE=BA=EF=BC=9A=E9=9C=80=E5=BF=AB=E9=97=A8=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=E7=9A=84=E5=8F=82=E8=80=83=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mag160c/thermal/core/RenderPipeline.kt | 22 +++++++++++++---- .../com/mag160c/thermal/usb/IrSession.kt | 5 ++++ build-artifacts/mag160c-app-debug.apk | 4 ++-- .../rendering_diff_phone_20261006.md | 24 +++++++++++++++---- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/core/RenderPipeline.kt b/android/app/src/main/kotlin/com/mag160c/thermal/core/RenderPipeline.kt index db1a54a..dc4d56b 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/core/RenderPipeline.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/core/RenderPipeline.kt @@ -20,6 +20,16 @@ class RenderPipeline( private val ffcPeriod: Int = 1800, private val ffcDrift: Int = 250, private val warmFrames: Int = 20, + /** + * Number of *settled* shutter frames averaged into the FFC reference. + * + * The official core feeds every frame of the shutter window into + * `CAccumulator` (`CFunctions::PushBaselineRaw`), so its reference is much + * cleaner than a short 4-frame average — which is why the official image + * shows less residual FPN (fewer stripes) at the same frame-to-frame noise. + * Default 4 keeps the byte-exact C reference behaviour (tests). + */ + private val baselineFrames: Int = 4, private val cdfPivot75: Boolean = true, private val force75: Boolean = true, /** FFC command callback: param 0 = FFC(0), 1 = FFC(1). */ @@ -224,8 +234,9 @@ class RenderPipeline( for (i in 0 until npix) refAcc[i] += frame[i] } refCnt++ - if (refCnt >= 4) { - for (i in 0 until npix) ref[i] = refAcc[i] shr 2 + if (refCnt >= baselineFrames.coerceIn(1, 64)) { + val n = refCnt + for (i in 0 until npix) ref[i] = refAcc[i] / n refCnt = 0 hasRef = true } @@ -609,15 +620,16 @@ class RenderPipeline( if (idx == 1) rebuildTables(shutterAtFfc) return 1 } - if (idx <= 9) { - if (idx == 9) onFfc?.invoke(1) + val refEnd = 5 + baselineFrames.coerceIn(1, 64) + if (idx <= refEnd) { + if (idx == refEnd) onFfc?.invoke(1) if (idx == 6) { refCnt = 0 hasRef = false } return 2 // ref collection window } - if (idx <= 13) return 1 + if (idx <= refEnd + 4) return 1 // normal frame: FFC condition var drift = shutterIn - shutterAtFfc if (drift < 0) drift = -drift diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt b/android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt index b029fc2..c55142d 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/usb/IrSession.kt @@ -324,6 +324,11 @@ class IrSession(context: Context) { // off the two distributions coincide (median 50 vs 61, mean 80 vs 82). // See docs/android_app/rendering_diff_phone_20261006.md §8. cdfPivot75 = false, + // baselineFrames stays at the verified 4: extending the window to 8 made + // the output *unstable* (frame-to-frame gray noise 3.95 -> 8.8), i.e. the + // device re-opens the shutter earlier than our schedule assumes, so the + // extra frames poison the reference. A shutter-aware reference + // collection is the follow-up (docs/android_app/rendering_diff_phone_20261006.md §9.4). onFfc = { param -> sendCmd( MagProtocol.cmd8(MagProtocol.CMD_SET_SHUTTER_STATE, param), diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index 1707f69..7b704e5 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:c5c41d1fb0a9806a3db503486458963d6db45486aecea5e84fbb1a169950fcb1 -size 12742557 +oid sha256:a17396cae119c4c9ab8dac36015d5071c5bdf7f9b68a13e9188ca347e0c943c9 +size 12742579 diff --git a/docs/android_app/rendering_diff_phone_20261006.md b/docs/android_app/rendering_diff_phone_20261006.md index 8f92843..20c2b49 100644 --- a/docs/android_app/rendering_diff_phone_20261006.md +++ b/docs/android_app/rendering_diff_phone_20261006.md @@ -241,10 +241,24 @@ raw 帧**逐个推进 `CAccumulator`(`Reset(pixels, baseLineAcc=4, 0)`,mode 每次 FFC `Restart`)→ 参考帧是"长窗口指数平均";而我们 `refPush` 只做**4 帧等权平均** → 参考帧噪声显著更大 → 残余条纹更多 ✗。 -### 9.4 下一步(精确) +### 9.4 参考帧窗口的实测与教训(已参数化,默认仍 4 帧) -1. 把我们的参考帧收集改成与官方一致:FFC 窗口内**逐帧 IIR 累积**(权重 = `CAccumulator` - 的 mode,来自 BasePara2.baseLineAcc=4;窗口长度按官方 baseline 状态持续时长), - `IrSession`/`RenderPipeline` 的黄金测试继续用文档化的旧配置(默认 4 帧等权)以免破坏 C 对齐; -2. 装机用同一套灰度域指标回归:目标空间 HF ≈0.34(±20%)、帧间噪声保持不变; +把参考窗口从 4 帧延长到 8 帧(`RenderPipeline.baselineFrames`)后实测: + +| 配置 | 灰度空间 HF | 灰度帧间噪声 | +|---|---|---| +| 官方 App | 0.34 / 0.26 | 4.12 / 1.80 | +| 我们 baseline=4(原状) | 0.76 / 0.80 | 4.02 / 3.95 | +| 我们 baseline=8 | 0.77 / 0.84 | **9.91 / 8.83(变差 ✗)** | + +→ **不能盲目延长窗口**:设备和我们的 FFC 时序里,快门窗口比"6…13 帧"短, +多收的帧实际是**开快门**帧,把参考污染了(输出不稳定)。参考帧收集必须 +**感知快门状态**(例如按帧统计判断快门是否关闭),而不是按固定帧号取。 + +### 9.5 下一步(精确) + +1. 参考帧收集改成**快门感知**:在 FFC(0) 之后逐帧判断"是否仍为闭合快门帧" + (可用帧统计/与前一帧的差异判断),把**所有闭合帧**都累积进参考(对齐官方 + `PushBaselineRaw` 的语义),FFC(1) 之后重置; +2. 用同一套灰度指标回归:目标空间 HF ≈0.34、帧间噪声 ≤4; 3. 之后同步 csdk(pivot + 温度链 + 参考累积)。