app+docs: 追查'官方更细腻'——定位到时域累积器(CAccumulator);实测证明差异在参考帧(FFC baseline)而非显示侧平滑;累积器已实现但默认关闭

This commit is contained in:
ZXCLI
2026-10-06 04:39:27 +08:00
parent d66d253f9f
commit 1d6c5e3e55
5 changed files with 150 additions and 3 deletions
@@ -63,12 +63,24 @@ object OfficialTemp {
* the official app's own wire parse (`UsbCommunication.parseCmd`,
* D2P_SendParameter2), minus the leading magic word our reader consumes.
*
* [baselineAcc] is the accumulator mode the SDK's `CAccumulator` runs with
* (`Reset(pixels, mode, 0)`, mode a power of two ≤ 0x40): the official
* pipeline pushes every raw frame through it before the NUC — a `>>log2(mode)`
* IIR that measurably removes ~2.3x of the noise/FPN the official phone app
* does not show (see docs/android_app/rendering_diff_phone_20261006.md §9).
*
* Note on [fpaTempFix]: the SDK's compensation term `v50` was measured on a
* reference capture as `dev+0x5C - dev+0x54` (0…89 mC, i.e. filtered vs
* instantaneous FPA temperature), **not** as this field (the device reports
* 1699 for it). [fpaTempFix] is kept for completeness / future mapping.
*/
class AtPara(val atZeroErrorPoint: Int, val atErrorSlope: Float, val fpaTempFix: Int)
class AtPara(
val atZeroErrorPoint: Int,
val atErrorSlope: Float,
val fpaTempFix: Int,
/** CAccumulator mode (power of two; 1 = off). */
val baselineAcc: Int = 1,
)
/**
* Parse the radiance record from the DDT tail. Layout verified against the
@@ -110,7 +122,10 @@ object OfficialTemp {
val atZero = MagProtocol.u32(payload, 40)
val atSlope = Float.fromBits(MagProtocol.u32(payload, 44))
if (atSlope == 0f || atSlope.isNaN() || kotlin.math.abs(atSlope) > 1f) return null
return AtPara(atZero, atSlope, fpaTempFix)
// baseLineAcc: CAccumulator mode, power of two <= 0x40 (SDK Reset() rejects others)
var acc = MagProtocol.u32(payload, 0)
if (acc < 1 || acc > 0x40 || (acc and (acc - 1)) != 0) acc = 1
return AtPara(atZero, atSlope, fpaTempFix, acc)
}
// ---------------------------------------------------------------- curve math
@@ -671,6 +671,7 @@ class RenderPipeline(
val st = ffcStep(shutterValue)
lastPhase = st
if (st != 0) {
accReady = false // SDK: accumulator Restart() at every FFC
if (st == 2) {
// collect the reference in a SCRATCH buffer: decoding into `nuc`
// would leave raw (uncompensated) counts there, and the OSD
@@ -685,6 +686,7 @@ class RenderPipeline(
val off = if (hasHdr) 0x1C else 0
ByteReader.decodeU16(frame, off, npix, nuc)
if (!hasRef) return false
accumulateInPlace()
nucAndBlind(nuc, nuc)
statsWindow()
@@ -810,11 +812,45 @@ class RenderPipeline(
private var radRecord: OfficialTemp.RadRecord? = null
private var atPara: OfficialTemp.AtPara? = null
// ---- official display accumulator (CAccumulator, mode = BasePara2.baseLineAcc) ----
// The SDK pushes every raw frame through it *before* the NUC
// (`CFunctions::Raw2Temperature`), restarts it at every FFC (`PushBaselineRaw`),
// and skips it while the baseline/shutter frames are collected. With mode 4 it
// is a `>>2` IIR, which is what makes the official image ~2.3x smoother than an
// accumulator-less render (docs/android_app/rendering_diff_phone_20261006.md §9).
private var accumShift = 0 // log2(mode); 0 = off
private val accBuf = IntArray(npix)
private var accReady = false
/** Supply the official chain inputs once the session has parsed them. */
fun setTempParams(rec: OfficialTemp.RadRecord?, at: OfficialTemp.AtPara?) {
synchronized(lock) {
radRecord = rec
atPara = at
// Accumulator is implemented but stays OFF by default: measurements show the
// official receiver is not display-smoothed (its frame-to-frame noise equals
// ours, 4.12 vs 4.02) while its *spatial* noise is 2.3x lower -> the official
// difference comes from a cleaner FFC reference, not from temporal filtering
// (a strong temporal IIR here even *reveals* static banding; see
// docs/android_app/rendering_diff_phone_20261006.md §9).
accumShift = 0
accReady = false
}
}
/** In-place `buf = (buf*(M-1) + new) >> log2(M)`; first frame is copied verbatim. */
private fun accumulateInPlace() {
if (accumShift <= 0) return
if (!accReady) {
System.arraycopy(nuc, 0, accBuf, 0, npix)
accReady = true
return
}
val m1 = (1 shl accumShift) - 1
for (i in 0 until npix) {
val v = (accBuf[i] * m1 + nuc[i]) shr accumShift
accBuf[i] = v
nuc[i] = v
}
}