Files
MAG160C/docs/android_app/rendering_diff_phone_20261006.md

62 KiB
Raw Permalink Blame History

手机端渲染差异定位(Android SDK ↔ Windows/Linux SDK 交叉验证)2026-10-06

背景:相机改插 root 手机(22041211AC,Android 12 + Magisk)后,按用户要求 "官方 App 拍照 vs 我们的 App 拍照" 做对比,出现差异就同时逆 Android SDK 与 Linux/Win SDK 交叉验证。本轮结论:亮度/对比度差异的根因是"窗口半宽 = 基础窗口 × 对比度修正",官方手机 App 带对比度偏移,我们的移植只在 c=0 时一致。

1. 对比做法与结果

项 官方 App(cn.com.magnity.magnitycx) 我们的 App
照片存放 /sdcard/magnity/Cx/media/MAG-160CORE<时间>.jpg(+ others/ 里是 DDT) /sdcard/DCIM/MAG160C/MAG160C_<时间>.jpg
照片内容 纯 JPEG(无任何数据块,EOI 即文件尾) 完整 MDT(INFO0/INFO1/FRAME/NUC/RENDER/EXTREMES)
亮度(同场景) luma p25=40 p50=49 mean=84 p25=10 p50=19 mean=53
调色板索引分布(按 16 档) 峰值在 2–4 档(10%/40%/12%) 峰值在 0–1 档(15%/39%)

→ 同一静止场景,我们的图整体偏暗约 30 luma:热点位置一致、调色板一致,属于 "灰度映射/窗口"差异(不是色板或数据差异)。

2. 交叉验证:窗口是怎么算的

Android SDK(libcxsdk 反编译,权威):

CFunctions::GrayEnlarge()                     // 灰度/窗口总入口
  uVar2 = CalContrastOffsetedAutoEnlargeRange()          // 窗口"范围"(不是半宽)
  winLo = min(mean - (uVar2>>1), statMin)                // 注意:向外扩到数据范围
  winHi = max(mean + (uVar2>>1), statMax)
  HistDeRedundance()                                     // 直方图去冗余(我们的 lutRebuild)

CFunctions::CalContrastOffsetedAutoEnlargeRange():
  base  = (dev24 * 1000) >> shift          // dev24=8, shift=3 → 1000
  c     = clamp(contrast, -100, 100)       // ★ 对比度设置
  range = base - base*c / (150 + (c<0 ? -100 : 0))       // c>0:range 变小(窗口更窄→更亮)
  if (brightness 项 dev+0x6c/0x38 有效) range += 亮度修正项
  range = max(range, 0x80)

Windows SDK:MAG_SetAutoEnlargePara / MAG_SetFixedEnlargePara / MAG_SetSubsectionEnlargePara(同一组参数的用户 API)。 Linux SDK:MAG_SetAutoEnlargePara(libmagcore @0x11be50)同名同参 —— 两条 SDK 线吻合。

官方手机 App 侧:ExtPara(设备上报)含 intContrastOffset / intBrightOffset / intAutoEnlargeRange / intDetailRatio / intCurrentEnvTemperature,App 用 DeviceController.getExtParameter() 读取后回写(setExtParameter)。

3. 我们的移植差在哪

RenderPipeline.statsWindow() 的窗口结构与官方完全一致(winLo=min(mean−half,min)、 winHi=max(mean+half,max)),但:

  • half = base>>1 只在对比度 c=0 时等于官方(官方 range=base → range>>1);
  • 官方手机 App 带 intContrastOffset(正对比度 → 窗口更窄 → 更亮)→ 这就解释了我们偏暗。

证据链:PC 端(对比度 0)我们的渲染与官方 PC SDK 逐像素差 1.3 级 ✔; 手机端(官方 App 带对比度)差值 ~30 级 ✔ —— 同一实现、不同对比度输入,两个场景都对得上。

4. 顺带确认的两件事

  1. 环境温度输入:官方 App 调 setEmissivity(emissivity, ExtPara.intCurrentEnvTemperature) —— 即 PC SDK 的 dev+0xA4(我们温度链里的 ATZeroErrorPoint)就是环境温度 (设备默认 22216 = 22.2 ℃);App 的发射率设置也走这里(100 = 1.00)。 → 我们 App 的发射率设置应当接进 OfficialTemp.temperature(emissivity=…)(当前固定 0.98)。
  2. BasePara2 真机报文(60 B,magic 已由我们的读取层吃掉): fpaTempFix @+14(short)=1699、ATZeroErrorPoint @+40(int)=22216、ATErrorSlope @+44(float)=−0.02789 —— 已写入单测作为地面真值。注意:PC 上实测的补偿量 v50(=dev+0x5C−dev+0x54,0…89 mC) 不等于 fpaTempFix(1699),故 App 侧 sensorDeltaMc 暂取 0(误差 ≤0.09 ℃), 待定位 dev+0x5C 的来源(疑似滤波后的 FPA 温度)后再接入。

5. 下一步(按此顺序)

  1. 移植 CalContrastOffsetedAutoEnlargeRange(含亮度项到 statsWindow),并在设置页加 "对比度/亮度"(默认值取官方 App 的默认),重拍对比 → 目标是 luma 直方图与官方吻合。
  2. 补 getExtParameter 命令(Android SDK 侧 JNI:DeviceController.getExtParameter), 把设备的 intContrastOffset/intBrightOffset/intCurrentEnvTemperature 接进来。
  3. 温度读数接 App 的发射率设置(OfficialTemp 已支持参数)。
  4. 以上在 App 验证无误后,按用户要求同步进 csdk(C 参考实现)与 csdk 测试。

6. 追加实测(同日):实时画面亮度差已复现,差异区间收敛到"窗口/LUT"

同一静止场景、前后相隔约一分钟分别截取两个 App 的实时画面(官方横屏 3200×1440、 我们竖屏 1440×3200),按行/列亮度自动裁掉黑边后统计:

p25 p50 p75 mean
官方 App 实时 12–20 26–33 63–124 53–72
我们的实时 2–4 12–25 27 20–25

→ 差异是真实且系统性的(不是拍照时刻/场景差异),并已把差异区间收敛到显示阶段的 "窗口 + 灰度 LUT"两步。

三方交叉验证(Android ↔ Windows ↔ Linux):

阶段 Android(libcxsdk) Windows(CoreSDKLib) Linux(libmagcore)
设置入口 setAutoEnlarge(enlargeRange)(官方 App 传 BasePara2 的 enlargeRange=8) MAG_SetAutoEnlargePara(chan, range, contrast, brightness) 同名同参(+0x24/+0x30/+0x34)
窗口范围 CalContrastOffsetedAutoEnlargeRange():range = base − base*c/(150+(c<0?−100:0)),base=(dev24*1000)>>shift;winLo=min(mean−range/2, min)、winHi=max(mean+range/2, max) 同结构(我们的 statsWindow 即此移植) 同结构
灰度 LUT HistDeRedundance():1024 桶直方图(0xFFC00000/span、>>20 & 0xffc = 我们的 lutRebuild 公式)+平滑+CDF 我们的移植来源 同结构

结论:窗口公式在对比度 c=0 时与我们逐字节一致(且官方 App 只传了 range=8, 未设对比度),直方图/LUT 的骨架也一致 —— 所以剩下的差异只可能出在 ① LUT 的平滑/CDF/pivot 细节(Android 版 HistDeRedundance 的后续段尚未逐行核对), 或 ② 官方 App 通过 setEX/ExtPara 设置的亮度偏移(intBrightOffset)等参数。 这两处是下一轮的直接目标(继续用同一套"三 SDK 交叉 + 真机截图定量"的方法)。

7. 渲染差异的真正落点:调色板(铁虹表)不同

7.1 先排除的两项(都有反编译证据)

  • 窗口:Android GrayEnlarge + CalContrastOffsetedAutoEnlargeRange 与我们 statsWindow 在对比度 c=0 时逐字节一致;官方 App 只调用 setAutoEnlarge(BasePara2.enlargeRange=8),没有对比度/亮度参数。
  • 灰度 LUT/CDF:Android HistDeRedundance 的直方图与双向 CDF 段与我们 lutRebuild 逐行一致(0xFFC00000/span、>>20 & 0xffc、total>>12、iVar3=0x10000/step、 前后向各一张表、+0x100/+0x200 步进)——这段不是差异来源。

7.2 官方默认调色板 = 索引 2(和我们一样),但表不同

  • 官方 App:ImageViewer.java:383 / DeviceController.java:91 → setColorPalette(SharedPreferencesManager.getInt("paletteIndex", 2)) → 默认 2(铁虹)。
  • 判定方法:"亮度 → 颜色"曲线在同一调色板下唯一(与灰度分布/亮度水平无关), 用它做检验(把像素代入调色板表,取同亮度最近项的色差):
    • 对照(我们自己的像素 → 我们的表):中位色差 0.3,83.6% 在 ±10 内;
    • 官方像素 → 我们的表:中位色差 20.3,只有 36% 在 ±10 内;最接近的调色板也不是 2 号;
    • 分亮度段看:亮度 32–96 时官方是橙色(R−B≈+90,R≈97–102/G≈44–77/B≈6), 我们同一亮度是紫/蓝(R−B≈−33…+20)。
  • 原因(已定位到二进制):我们索引 2 用的是 PC SDK 的表 (OfficialTables.PALETTE256_ARGB,来自 CoreSDKLib dev+0xb18),而官方手机 App 用的是 Android SDK 运行时生成的表 —— CFunctions::SetColorPalette case 2(libcxsdk @0x26c70)。 上一轮把 case 0/1/3…10 的生成式移植成了 VendorPalettes,case 2(铁虹)当时是拿 PC 表 做"锚点校验"、没有移植生成式 → 两表在中间段不同,颜色/亮度观感因此不同。
  • "对比度更高"和"条纹更少"都由此解释:不同色带的亮度斜率不同(观感对比度不同), 且官方色调更亮(暗端被抬起)→ 噪声与 FPN 在暗端不再显眼。

7.3 条纹本身的实测(数据层无差异)

指标 官方 我们
NUC 帧 column-profile HF(PC 抓帧对照) 7.81(0.63% of spread) 7.20(0.63%)
渲染图平坦块列抖动(同刻截图,16×16 块) 0.81–1.44 0.34–0.94
渲染图 HF 能量 mean|dx|/mean|dy| 2.80 / 3.67 2.71 / 3.47

→ 数据与降噪层面我们不差于官方;"条纹更明显"是色调/亮度的感知效应 (官方实时画面 HF 更低 1.27 vs 2.92,也可能有 SDK 时域平滑参与)。

7.4 下一步

  1. 移植 SetColorPalette case 2 的生成式(4 段线性,常量 0x75/0xbc/0x25/0xff/0x27/0x3b/0x1c3… 已在反编译中), 替换 App 索引 2 的表;然后同一分钟内截两 App 实时图做"亮度→颜色"曲线回归(目标:中位色差降到控制组水平)。
  2. 顺带核对其余 11 个索引是否也应改用 Android 生成式(本轮 VendorPalettes 已覆盖 10 个)。
  3. 之后再按用户要求把验证过的改动同步进 csdk。

8. 定案并修复:灰度 CDF 的 75% pivot(cdfPivot75)

8.1 先纠正 §7 的结论

按"照片"(不经过屏幕显示路径)重做调色板拟合:官方照片与我们的照片都完美拟合 2 号铁虹 (中位色差 0.67 / 1.00)→ 调色板不是差异来源;§7 里"官方颜色对不上"是当时裁剪/对话框 污染造成的误判(已用照片复核推翻)。VendorPalettes 头注释也记录上一轮已验证 SetColorPalette case 2 与我们的铁虹表 256/256 逐项一致。

8.2 决定性测量(同刻截图 + 调色板反解灰度)

关闭两边所有对话框后前后约一分钟各截一张实时画面,图像区用调色板反解出灰度值 (消除显示路径/色域影响;99%+ 像素能唯一反解):

p5 p25 p50 p75 p95 mean
官方 App 0 48 61 129 202 82.2
我们(cdfPivot75=true,原状) 11 23 33 82 189 60.2
我们(cdfPivot75=false) 15–17 31–34 50–54 132–135 200–202 79.8–81.2

→ 我们的灰度整体偏低约 1 档半("偏暗、对比度低"),把 pivot 关掉后与官方分布重合。

8.3 根因与修复

  • SDK 里这段 CDF 有一个由参数控制的支路(Android 反编译 HistDeRedundance: if (*(int *)(this + 0x60) != 0) { ...把 lres 抬到 75% 分位... });
  • 我们的移植(来自 PC SDK 版语义)把它固定为 开(RenderPipeline.cdfPivot75 = true, 也是 C 参考实现的默认);
  • 但官方手机 App 实际是不启用它的 → 灰度被压低、观感偏暗、低对比,噪声/FPN 在暗端更显眼 (这同时解释了用户说的"对比度更高""条纹更少")。

修复:IrSession 构造实时管线时显式传 cdfPivot75 = false(类默认与 C 参考保持 true,matchesCReferencePixelExact 等黄金测试不受影响)。已装机验证(上图对比), 同刻灰度分布与官方一致。

8.4 待办

  1. 同一步骤同步进 csdk(C 参考实现把该参数作为配置项/默认关闭,并更新其测试与文档)。
  2. 其它 11 个调色板索引与 Android 生成式的逐表核对(本轮已确认索引 2 一致)。
  3. 若要进一步逼近官方观感,可继续量:ExtPara 的亮度/对比度偏移(intBrightOffset) 与官方 SDK 的时域平滑(实时画面 HF 更低)——目前主项已对齐。

9. 用户追问"官方还是更细腻"→ 找到遗漏的阶段:时域累积器(CAccumulator)

9.1 逐个排除

候选 结论
imageEX 官方 setEX(imageEX, w/2, h/2) 的 imageEX 是电子变倍(UI 1×/2×/4×/8×,默认 0=1×),不是细节增强 ✗
细节增强 DDE 官方 App 从不调 setDetailEnhancement;PC 参考里该字段(dev+0x30)= 0;我们 App 默认也是 0。实测把我们 DDE 开到 2 级后灰度空间噪声反而更大(0.76→0.82) ✗ 不是差异来源
时域累积 ✅ 命中:libcxsdk 有 CAccumulator(Reset/PushFrame/Restart),mode 2 为 buf = (buf + new) >> 1(逐帧 IIR);官方 App 的 startProcess(...) 里就传了 baseLineAcc 参数

9.2 实测(灰度域,已用调色板反解消除"色带斜率"干扰)

同一静止场景,各自连拍 3 张(间隔 1.5 s),图像区反解成灰度后统计:

灰度空间 HF mean|dx| / mean|dy| 灰度帧间噪声 1→2 / 2→3
官方 App 0.34 / 0.26 4.12 / 1.80(在收敛 ↓)
我们(pivot 已修) 0.76 / 0.80 4.02 / 3.95(不收敛)
我们 + DDE 2 级 0.82 / 0.87(更差) 4.35 / 4.52

两点结论:

  1. 官方的灰度画面比我们平滑约 2.3 倍(0.34 vs 0.76)——这不是调色板/亮度能解释的;
  2. 官方的帧间噪声在收敛(4.12→1.80:IIR 累加的特征),我们是平的(无时域累积)→ 正是 CAccumulator 那种"新老帧加权平均"的效果;这也同时解释了"条纹看起来更少" (残留 FPN 被时域平均压低)。

9.3 实施与实测:不是显示侧平滑,而是参考帧(baseline)更干净

我把 CAccumulator 的显示侧累积按两种权重都实现并装机实测(代码已留在 RenderPipeline, 默认关闭):

配置 灰度空间 HF mean|dx|/|dy| 灰度帧间噪声 1→2 / 2→3
官方 App(参考) 0.34 / 0.26 4.12 / 1.80
我们,累积关 0.76 / 0.80 4.02 / 3.95
我们,显示侧累积 >>2(mode 4) 0.18 / 0.20(过平滑 ✗) 0.14 / 0.14(几乎冻结 ✗)
我们,显示侧累积 >>1(mode 2) 0.77 / 0.85 3.69 / 5.67

判定:显示侧平滑会同时压低空间噪声和帧间噪声(我们 mode 4 的结果就是如此, 而且强累积把静态横条纹暴露得更明显——噪声原本在掩盖它,截图可见)。 但官方的实测是"空间噪声低 2.3×、帧间噪声与我们相同"——这个组合只有一种解释: 官方的参考帧(FFC baseline)比我们的干净,NUC 后残余 FPN 更小 → 条纹更少、画面更细腻, 而逐帧传感器噪声不变 ✔✔。

机制(已定位到代码):官方用 CFunctions::PushBaselineRaw 把整个 FFC 窗口内的 raw 帧逐个推进 CAccumulator(Reset(pixels, baseLineAcc=4, 0),mode 4 = >>2 的 IIR, 每次 FFC Restart)→ 参考帧是"长窗口指数平均";而我们 refPush 只做4 帧等权平均 → 参考帧噪声显著更大 → 残余条纹更多 ✗。

9.4 参考帧窗口的实测与教训(已参数化,默认仍 4 帧)

把参考窗口从 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 + 温度链 + 参考累积)。

10. 定案(真机 libcxsdk 反汇编 + 官方照片定量):显示 LUT 才是差异,§9 的机制被推翻

数据源换成手机上的官方库本体:adb 从官方 App 的 APK 目录取 lib/arm/libcxsdk.so(arm32,本 App 只用 arm),IDB 里按 vtable 指针 (0x5a7xx 段)解析出真实函数体,逐条指令核对(Hex-Rays 会吞掉隐式除数, 必须看反汇编)。以下结论全部以该反汇编为准。

10.1 先纠正前两轮的错误结论

前结论 反汇编事实
§8「官方不启用 CDF 75% pivot,所以我们要关掉」 错。pivot 支路由 theFunc._96_4_ 控制,Controller::Controller() 把它设为 1,全库无第二处写入 → 官方是开着的;而且它的写法不是 75% 分位,而是 max(center_index, (meanBelow + meanAbove) >> 1)。当时"关掉更接近"是因为用的 LUT 本身是 PC 版(结构与 Android 版不同),两点误差互相抵消。
§9.3「官方参考帧是长窗口 IIR 指数平均,比我们干净」 错。CAccumulator::PushFrame 的 mode≥4 分支是恰好 mode 帧的块平均(sum>>log2(mode),计满清零),mode=2 才是 (buf+new)>>1。Controller::PushFrame 的快门状态机对 A+1..A+E 共 E=baseLineAcc=4 帧各推一次 → 官方参考 = 4 帧块平均 = 和我们完全一样。
§9.2「官方画面空间噪声比我们低 2.3×」 测量有分辨率偏差:官方 App 显示的是 SDK 原生的 160×120 输出(照片 640×480 = 4×),我们的实时画面是 320×240(SDK 2× 后再 ×3 = 960×720 照片)。逐屏像素 HF 随放大倍数变化,两个 App 的放大倍数不同 → 该数字不可比。

10.2 官方每帧链路的完整配置(都已定位到 App 自己的调用)

Controller::StartProcess(cb, fpaW, fpaH, sn, devType, shutterCloseSpeed, shutterOpenSpeed, ffcTriggerFrame, ffcTriggerTemperature, palette, baseLineAcc, caliPath) —— 对应 App 侧 UsbCommunication.startTransfer() 的实参;真机 BasePara2 实测: shutterCloseSpeed=5, shutterOpenSpeed=4, ffcTriggerFrame=1800, ffcTriggerTemperature=250, enlargeRange=8, baseLineAcc=4, fpaTempFix=1699。

阶段 官方取值 我们
FFC 相位 A/E/B 5 / 4(=baseLineAcc)/ 4 帧 5 / 4 / 4 ✔(ffcStep)
参考帧 4 帧块平均(CAccumulator mode=4) 4 帧等权平均 ✔ 等价
曲线插值温度 帧尾 FPA + fpaTempFix(1699)(App 加,SDK 的 SelectCaliCurve/GenerateVolatileLens 用它) 帧尾 FPA(少了 1699)✗ 本轮修复
窗口 winLo=min(mean−range/2, statMin)、winHi=max(mean+range/2, statMax),range=(enlargeRange*1000)>>3=1000(对比度 0;this+0x6c 全库未写 → 亮度项恒不生效) 一致 ✔(DDT 头 +20 读到的正是 8)
灰度 LUT Android 版 HistDeRedundance(见 10.3) PC 版结构 ✗ 本轮修复
时域平滑 CSmoother 关(this+0x380c = IntrinsicPara[0x20],构造默认 1,需 >1 才生效) 未实现 ✔
显示侧累积 CAccumulator(+0x37a4) 关(this+0x3808 = 1 → Raw2Temperature 直通) 默认关 ✔
setEX 电子变倍 关(imageEX 默认 0 → SetEX(0) → 0x3814=0) 无此功能 ✔
7×7 细节增强 关:MapTemperature 确实把门槛 0x3864 置 1(所以 PushFrame 每帧都会调 FilterDetailEnhancement_Simple),但强度来自 CFunctions+0x28 << 3,而该字段全库从未被写入(CFunctions 构造只 memclr,Controller 构造设了 0x1C/0x2C/0x30 等但没设 0x28)→ 传 0 → 函数首行 if (param_1 != 0) 直接返回 = 空操作 默认 0 ✔ 一致(我们的用户开关是超集,官方 Android 侧根本没有入口)
翻转 App 侧 dwFlip 只是存进全局 0x6b90c 结构,本库无人消费(翻在 App/显示层做) 我们在显示/保存层做 ✔

10.3 真正的差异:灰度 LUT(Android 版 vs PC 版)

拿我们自己照片里存的 NUC 计数块与官方照片做像素级对齐(官方照片 = 官方实时渲染,两者同场景同机位):

  • 垂直翻转后相关性 r = 0.99 → 计数本身几乎一致,差异只在"计数→调色板索引"的映射;
  • 官方映射 ≈ 0.84 × 我们的LUT + 21(被"压缩并抬升"):官方平坦背景落在索引 45–55 的一段平台上,我们的旧 LUT 把它压到 0–20(暗端被压死)→ 这就是"官方对比度更高 / 更细腻 / 条纹更少"观感的来源;
  • 逐条核对反汇编后确认 Android 版 HistDeRedundance 与 PC 版(我们原来移植的)结构不同: 固定基值 16*(448+447*brightness/200)(PC 版是"自动居中 + 时域平滑"的二分搜索)、 步进上限 max(1,(0x1800+4j)·k>>13)(k=min(0x200,16·span/range))、 反向 CDF 从 hist[pivot] 起(PC 版从 pivot+1)、平滑后 hist[1023]=hist[1022]、 pivot 用"两段均值的平均"。

已落地:RenderPipeline.androidLutOf() = 逐指令移植(含 golden 向量单测 AndroidLutTest),实时会话 androidLut = true;PC 版路径保留,C 参考黄金测试不受影响。

10.4 在线拟合与遗留项

用官方照片做目标(同一 NUC 帧、逐像素比对,均方误差以索引计):

配置 平均误差 低端 p5/p25
我们旧 LUT(PC 版 + pivot 开,= 09-12 照片) ≈35 8 / 17
我们旧 LUT(PC 版 + pivot 关,= round-30 实时) ≈14 15 / 29
Android 版移植(k 按公式 = 21) 12.5 24 / 36
Android 版移植,k=16(拟合最优) 5.9 45 / 54
官方照片实测 — 45 / 52

→ 移植已在正确轨道上;遗留一处常数:反汇编算出 k = 16·span/range = 21, 数据偏好 ≈16。两个可能来源,都需要真机同刻 A/B 才能定: ① 计数统计仍有细微差别(最可能就是 §10.2 的 +fpaTempFix,它改变插值曲线→改变计数分布); ② 该 build 的 v45 常数与反汇编读出的 16 不同(已在反汇编里逐位核对过 LSLS R6,R0,#4 + uidiv,倾向排除)。 在真机上先验证 +fpaTempFix 是否让计数分布与官方一致,再看 k。

10.5 本轮已改代码

  1. RenderPipeline.androidLutOf():Android HistDeRedundance 逐指令移植(新增,含测试);
  2. IrSession 实时管线改用 androidLut = true,删除 round-30 的 cdfPivot75 = false 临时开关;
  3. RenderPipeline.fpaTempFix(构造参数)+ ffcStep/frameRemote 调 rebuildTables(shutter + fpaTempFix), IrSession 从 BasePara2 传入(官方 UsbCommunication 同款语义)。

10.6 待办

  1. 真机 A/B(手机当前被 PIN 锁屏,需机主解锁):同一静止场景,官方实时 vs 我们实时,用"调色板反解灰度 + 逐像素对齐"复核 10.4 的 k/计数假设;
  2. 7×7 细节增强按官方默认打开 —— 已排除:强度源 CFunctions+0x28 全库未写 = 0, 官方那一步是空操作(这也解释了为什么离线拟合"打开反而略差");我们的实现保持用户可选、 默认关。顺带:我们的 DetailEnhance 与反汇编有一处细节差(LocalMap7x7_Simple 的 min/max 还要多采每行的第 1 列,共 5 列;我们只采了求和用的 4 列),会让门槛略偏严, 属可选功能的精度问题,不影响默认路径;
  3. 同步 csdk(Android 版 LUT + fpaTempFix + 温度链)。

11. 真机验证(2026-10-06 下午,手机解锁后)

11.1 先修掉一个"实时全黑"的 bug(并补上会拦住它的测试)

Android LUT 落地时把 IntArray 的 LUT 用 System.arraycopy 拷进 ByteArray 的 lut ——每帧抛 ArrayStoreException,实时画面整块全黑(SurfaceView 仍在刷,logcat 无异常)。 黄金向量测试只覆盖纯函数 androidLutOf(),没覆盖实例路径,所以没拦住。已改为逐元素拷贝, 并新增 AndroidLutPipelineTest(4 个端到端用例:androidLut=true 能渲染出非全黑/单色画面、 两种 LUT 结果必须不同、fpaTempFix 必须改变插值表、手动 FFC 之后仍能渲染)。 单测 113 → 117 全绿。

11.2 同场景 A/B:映射已经对齐

手机(22041211AC,官方 App 与我们的 App 前后 2 分钟各拍一张,同一静止场景), 用调色板索引空间在同一 160×120 网格上比对(避开两个 App 不同的放大倍数):

指标 官方 我们
索引分位 p5/p25/p50/p75/p95 82 / 89 / 96 / 117 / 133 85 / 92 / 98 / 114 / 134
逐计数中位索引(9490→10330 共 21 档)差值 — 中位 2.0(最大 4.5)
全图平均 |Δ索引| — 4.47 / 255,中位 3.6,85.1% ≤8,99.2% ≤16

对比上一轮的旧 LUT:低端被压死(我们 p5/p25 = 8/17,官方 45/52,等效系统偏差 ≈35 级) → "官方对比度更高、更细腻"的映射差异已消除。

11.3 复核那个遗留常数 k:反汇编公式是对的

用我们照片里存的 NUC 计数逐像素对官方照片做模型比对(新的一对照片, 计数窗 [9142,10383],span=1241,公式 k=19):

步进常数 平均 |Δ索引| 中位 ≤8 占比
k = 16·span/range(反汇编公式,=19) 4.64 3.9 82.9%
k = 16(上一轮"拟合最优") 5.76 5.1 73.6%
k = 12 9.16 8.1 49.2%
k = 24 4.92 4.1 80.8%

→ 按反汇编公式实现即为最优;上一轮"数据偏好 ≈16"是当时那对照片(曝光/窗宽都不同)的产物, 结论作废,代码不动(§10.4 的遗留项关闭)。

11.4 遗留并已定位到域:我们的"计数"噪声偏大

同一个 LUT 作用在我们的 NUC 计数上得到的 HF(传感器网格)明显高于官方成品:

数据 HF |dx| / |dy|
官方照片(其渲染输出) 2.46 / 2.22
官方实时截图(1.2 s ×3) 1.61 / 1.51,帧间 1.83/1.77
我们的实时截图 2.35 / 2.39,帧间 2.34/2.40
我们的 NUC → 同一个 LUT(模型) 4.65 / 4.25

即:映射一致、噪声不一致,且证据指向"我们的计数本身比官方毛"(同 LUT 下模型 HF 高 1.9×)。 排除项:FFC 之后立刻截图(新鲜参考)几乎不改善(帧间 2.44→2.34)→ 不是参考老化; 我们的实时帧率实测 15.0 fps、单帧绘制 6-9 ms(与第 22 轮记录一致,无回归)。 下一步方向:把实时 FFC 参考采集与已验证逐字节一致的离线/PC 路径对齐(同一个参考下 PC 端 NUC 是逐字节相同的,所以嫌疑在"实时状态机采到的参考帧")。

11.5 运维笔记(真机调试踩坑)

  1. 正在推流时 force-stop App 会把相机卡死(命令端点 halt、GetCaliInfo 超时、此后无流数据)。 恢复:su -c 'echo 0 > /sys/bus/usb/devices/2-1/authorized; sleep 2; echo 1 > ...' 重新枚举; 切换 App 时先按 HOME 让 App 正常停流。
  2. MIUI 的 USB 授权在每次重装/重新枚举后都要重新授权;要勾"一律打开 MAG160C" 必须点复选框本体(x≈186),点标签中部(x≈718)不会勾上;授权后 App 需要重启进程才生效。
  3. 相机被卡住时 App 的统计会显示 reads=0 frames=0 timeouts=N,UI 以 ~3.3 fps 空转刷新 (不是渲染帧率)。

12. 追查"计数噪声偏大"(2026-10-06 傍晚,真机分阶段实测)

用户:"把'计数'噪声偏大修一下"。做法:在管线里临时插桩(DebugLog 打开、逐级打印 raw/参考/差值/NUC 的 mean/std/HF),真机跑一遍,把噪声预算算清楚,再看 A/B 差在哪里。

12.1 真机实测的噪声预算(设备 ,同一静止场景)

[diag] ffcRefFrame mean=15084 std=3625 hfdx=1172   ×4 帧(快门帧)
[diag] refFrameSpread sigma=30.2 / 27.5 / 27.2      ← 逐帧像素噪声(快门帧间差)
[diag] stage.raw   mean=11442 std=3466 hfdx=1168    ← 场景帧原始响应
[diag] stage.ref   mean=15083 std=3624 hfdx=1172    ← 4 帧快门平均
[diag] stage.diff  mean=-1821 std=326  hfdx=29.1    ← (raw-ref)>>1
[diag] stage.nuc   mean=9731  std=245  hfdx=24.2    ← 波段映射输出
  • 快门帧是真快门:参考帧均值 15084 与场景帧 11442 相差 3600 counts,且 4 帧之间 mean/std 一致到 0.1% → 快门在参考窗口内确实闭合、且是同一位置。
  • 逐像素时域噪声 σ_n ≈ 21–24 counts(由 refFrameSpread 的 √(1+1/k) 反解)。
  • 我们的差值噪声 = 0.559·σ_n = 12.3 counts(= 0.5·√(1+1/4),4 帧平均参考的理论值), 由实测 hfdx=29.1/1.128 推得的白噪声部分 ≈ 25.8 里含场景结构(NUC 的 24.2 同量级)→ 与理论预算一致,没有额外噪声源 ✓。
  • FPN 抵消到 2.4%(1168 → 29.1)✓。

结论:我们的参考采集与 NUC 链路的噪声与时域表现完全符合物理预算,"计数噪声偏大" 在数据链路上不成立(上轮凭跨 App 指标推断的"我们的计数更毛"没有证据支持)。

12.2 那 1.2–1.5× 的跨 App 差是什么

同场景 A/B 复测(官方 16:10 / 我们 16:08,16:08 那张同时给了 raw+NUC 块):

指标 官方 我们 比
索引 HF |dx|(照片) 1.98 2.84 1.43
索引 HF |dx|(实时截图) 1.61 2.35 1.46
实时帧间 |Δ|(重采样到 160×120) 1.83/1.77 2.34/2.40 1.31
HF 随亮度分档比(40→200 级) — — 1.18–1.35(近似加性,不是乘性)

已知的测量混淆项(不足以解释全部,但都在同一方向):

  1. 我方形如 960×720(=6× 传感器,JPEG q≈95 → 0.29 B/px),官方 640×480(=4× → 0.42 B/px) → 我们每像素压得更狠,JPEG 振铃/块效应加性抬 HF(与"近似加性"的观察一致);
  2. 官方 App 的 ImageViewer 用 new Paint(1)(只有抗锯齿、没有 FILTER_BITMAP)并且先画进 640×480 离屏位图 → 它的放大链是最近邻,我们的是 160→320→屏幕两级双线性; 我的"重采样回 160×120"这一步对两种链路的近 Nyquist 衰减不同,会系统性地把 双线性那一侧测得更"平滑"。
  3. 我们 18-1 的实时帧间指标此前受 UI 元素(OSD 文本/色条)污染,本轮已用遮罩排除到 99%。

12.3 本轮据此真正修掉的两件事

  1. MDT 照片的 raw/NUC 配对(数据完整性 bug):照片里的 FRAME 块原本写 session.lastRawFrame(每帧都更新,包括 FFC 窗口内的快门帧),而 NUC 块是 场景帧的渲染结果 → 拍在 FFC 之后的那张照片,两块不是同一次曝光(离线再渲染/ 分析界面正是消费这一对)。现在由管线交付 copyLastRenderRaw():渲染该帧时 把 raw 载荷存下来,照片两块必然同源。新增回归测试 MdtRawNucPairTest(对低通后的两块做相关性,>0.8;raw 的方差 99% 是 FPN, 逐像素相关性即使配对正确也接近 0,这个坑在测试里写明了)。
  2. LUT 的 32 位语义对齐厂商:cdf*denom 的乘法与 >>14 改为 32 位无符号 (Kotlin 侧 u32(...) + 逻辑右移);对真实数据结果不变(golden 向量仍全绿), 但退化场景(几乎全空的直方图)下与反汇编一致。

12.4 留给下一轮的(不盲改)

  • 若要严格分清"我们比官方毛 1.3×"是渲染还是测量:需要受控采集——同一帧数据 过两条链(官方 App 只有 JPEG 输出、没有 raw 服务),或用一次 PNG/无损导出对比。
  • 官方实时画面更平滑的可复现线索:其显示链是"离屏 640×480 + 最近邻放大"。 我们若要更接近,可以(a)把保存的照片改成 4×(640×480)或提高 JPEG 质量, (b)实时预览改单级放大。两者都是表现层改动,不影响已对齐的映射,风险低。

13. 用户复现:"低温区条纹更多"——定位到大负差值处的噪声放大(未修完,留了下一步)

用户:"我换了个场景,背景的低温区域在我们的 app 里面明显条纹更多"。手机仍在,做同刻 A/B (同一场景:我们 16:35 拍、官方 16:37 拍;另有 12 s 间隔的漂移标定)。

13.1 先标定漂移(12 s 内、同一 App 两张)

官方 App 相隔 12 s 两张:分位 [9,17,44,73,229] → [10,18,45,74,229],逐像素 |Δ| 均值 1.20(中位 1.0)。 → 12 s 的漂移约 1 级,不足以解释跨 App 的分位差,所以下面的对比是有效的。

13.2 冷区(最冷 25% 像素)量化

指标(照片,160×120 网格) 官方 我们 比
逐计数中位索引(冷区 9830→10000 counts) — — 最大 1.7 级 ✔ 映射一致
列条纹幅度(去 9 宽趋势后的列剖面 std) 2.12 2.28 1.08 ✔ 基本一致
细纹理 HF |dx| 1.71 2.17 1.27 ✗
实时截图冷区 HF(无 JPEG 干扰) 1.06 1.01 0.95 ✔

结论:冷区的映射和列条纹(FPN)都与官方一致,差的是细纹理/像素噪声(1.27×)。 JPEG 只能解释一部分:把我们那张(960×720 q95,202 KB)按 640×480/q98 重压后冷区 HF 从 2.17 只降到 1.96(官方 1.71)→ 仍差 1.15×。所以我们确实在冷区多出约 1.2–1.3× 的计数噪声。

13.3 哪来的:整帧预算吻合,冷区超预算 1.65×

  • 整帧(中等场景)实测:σ_n≈21–24 counts → 差值噪声 0.559σ_n=12.3 ✔ 与实测吻合(§12.1);
  • 冷区(大负 d;d=(raw-ref)>>1)实测计数 HF = 20.4 counts,而按同一预算应为 12.3 → 1.65× ✗ —— 与"冷区更毛"的用户观察方向一致,且只在大负 d 处出现。

已逐条排除:阈值/增益的 16 位解析(与厂商一致,且厂商侧也是有符号比较)、band 选择 (映射一致说明段选择一致)、快门参考(4 帧快门平均 ✔)、JPEG(只占约 1/3)、 显示链(实时冷区 HF 反而更低)。

13.4 下一步(精确定位,需一次带插桩的实机)

在大负 d 的像素上分层打印:d 的分布、命中的 band、该 band 的 gain/off(插值后)与 σ_n,对 DDT 里 band0 的原始 gain/off 核一遍。最可疑的两条(都可控):

  1. fpaTempFix 是否也该作用在图像链:我们按官方 App 的代码给插值温度加了 1699 (§10.5),如果该设备上图像链实际用的是帧尾原值,则 band0 的 gain 会被插值偏, 表现为"映射总体还对、但低端噪声被放大"——正好是现象; 验证:临时把 fpaTempFix = 0 出两版,量冷区 HF(同样一张场景、同样流程)。
  2. band0 的 gain 插值本身与 PC 参考路径的差异(PC 端 NUC 逐字节一致,说明差异在 Android 的 GenerateVolatileLens 重生成路径,而非表本身)。

14. 定案并修复:"低温区条纹更多"= 我们自造的 2× 交错图案(反汇编依据 + 实测验证)

14.1 依据(官方安卓路径没有插值级)

ImageViewer(官方 App,jadx 源码):

  • this.bmpIr_ = Bitmap.createBitmap(fpaWidth, fpaHeight, ARGB_8888)(160×120,即 SDK 直接给原生尺寸);
  • this.paint_ = new Paint(1) —— 只有 ANTI_ALIAS,没有 FILTER_BITMAP;离屏 cachedbmp_ = Bitmap.createBitmap(640, 480) 就用这些 paint 画进去(= 最近邻)。

而我们的 upscale2x(移植自 PC SDK)是双线性:偶列取原样本、奇列取邻均值 → 在传感器 Nyquist 上人为造出一层"全幅/半幅"交错的细竖条纹,且只在色带最陡的暗/低温区最扎眼。

14.2 实测验证(同场景 A/B,冷区=最冷 25% 像素)

冷区指标(照片,160×120 网格) 修复前(双线性 2×) 修复后(最近邻 2×)
细尺度列条纹(去趋势宽 5)之比 1.16 ✗ 1.00 ✔
细尺度列条纹(去趋势宽 9)之比 1.29 ✗ 0.93 ✔
宽尺度列条纹(21 / 41)之比 0.90 / 0.87 ✔ 0.82 / 0.72 ✔
列剖面相关性(同一 FPN 图案) +0.815 +0.832 ✔
|dx| HF 之比 1.27 1.67(见 14.3)

→ 用户报的"细条纹"就是这层自造交错:去掉后细尺度条纹与官方一致(1.00/0.93), 而真实 FPN(宽尺度)我们本来就比官方更轻。

14.3 顺带说明:HF 比值反而变大不是变差

重建后的 |dx| HF 从 1.27 升到 1.67,是因为旧的交错本身就是一次列方向的平滑 (奇列被平均掉一半),它把这个指标"压低"了——去掉假平滑后露出的是真实逐像素噪声。 即旧值 1.27 是"假图案 + 被平滑的噪声"的合成,新值是纯噪声。逐像素噪声的差异 (同一场景、同一映射下)仍待查,方向已收窄到"照片路径的 JPEG/缩放"与"设备帧噪声"两者, 方法学与数据见 §12/§13。

14.4 落地

  • RenderPipeline.upscaleNearest(构造参数,实时会话 = true;默认 false 保持 C 参考字节不变);
  • csdk:mag160c_render.h/.c 增加 upscale_nearest(两个 demo 默认 1),同一段注释说明依据;
  • 单测新增 UpscaleParityTest(两种模式必须不同 + 最近邻必须逐行复制),共 119 全绿。

15. 用户复核:"我们颗粒更大,尤其高低温分界边沿"——是我上一轮 14 节的最近邻改错了

用户:"我们的app图片看起来像素颗粒更大啊,特别是高低温分界的边沿,是不是啥滤波没开"。

15.1 眼睛 + 数据都指向同一个原因

17:51 同尺寸对照(左=我们 17:06 版,右=官方):我们的边沿是 2 个传感器像素一级的台阶, 官方是单像素的平滑过渡——即 14 节里"最近邻 2×"把有效分辨率砍半了(相邻两个传感器像素被复制成同一个值)。 定量复核:冷区 |dx| 比值 1.27(我们更毛)。

反汇编/源码再核对:官方 ImageViewer 除 new Paint(1) 外还有 PaintFlagsDrawFilter pfd_ (会加上 FILTER_BITMAP)→ 它的显示/保存链其实是从 160×120 单级滤波放大 (cachedbmp_ 640×480),既没有交错也没有块状复制。

15.2 修法(照片改为单级放大;实时缓冲回退到插值)

  • 照片:PhotoSaver.SENSOR_W/H 改为 160×120(原生传感器网格,与探针/NUC 同一空间), RENDER_SCALE = 6(输出仍是 960×720);调用方交入的帧改为精确取样 320×240 缓冲的偶数像素 (o[2x]=src[x],所以偶数采样就是原始像素,无重采样误差)。→ 单级 6× 双线性, 传感器像素级颗粒、无交错、无块状。
  • 实时缓冲:upscaleNearest 回退为 false(插值 2×):最近邻会把颗粒变成 2 传感器像素、 边沿出现粗台阶,是可见的退化。csdk 的两个 demo 同步回退为 0,选项保留并写明测得的教训。

15.3 实测(同场景:我们 17:54 / 官方 17:56,翻转对齐 r=0.994)

指标 官方 我们(新)
索引分位 p5/25/50/75/95 21 / 48 / 76 / 90 / 224 18 / 47 / 78 / 92 / 224
冷区 |dx| 4.32 3.29(比值 0.76)

→ 修好后我们比官方更平滑(官方因最近邻 4× 反而有 4 像素大块,见 x8 放大对照图)。 单测 119 全绿(PhotoNucMappingTest 更新为 160×120→960×720 的新策略)。

16. 「官方背景条纹更不明显」——逆向排除全部候选后,定位为我们独有的逐行固定偏移(2026-10-06 深夜,记录轮)

用户发了同场景双截图(官方 vs 我们,均为竖屏、12 px/传感器像素、同一调色板铁虹)并要求 "看逆向出来的代码是不是还漏了什么"。本轮做了两件事:把官方每帧链路逐条再对一遍(IDA 批量 反编译),并对两台 App 的显示结果做调色板弧长反演的定量对比。结论:每帧算法没有遗漏; 差异是一个官方没有、我们有的"逐传感器行固定偏移"(残余行 FPN,显示上就是水平条纹)。

16.1 逆向再核对(libcxsdk = 普通版 MAG-Cx 的 SDK,全部读反编译原文)

  • 内参来源定案:Controller::StartProcess(cb, w,h,sn,devType, close,open, ffcFrame,ffcTemp, palette, baseLineAcc, cali) 只写 Controller+0x28(w)/+0x2c(h)/+0x18(palette)/+0x40(baseLineAcc),其余字段保持 Controller::Controller() 的默认:+0x44=1、+0x48=1、+0x4c=5。 映射到 CFunctions:0x3804=baseLineAcc=4、0x3808=1(温度累积器旁路)、 0x380c=1(CSmoother 关)、0x3810=5。→ 官方手机链路没有平滑器/累积器, 之前 §8/§9/§10 的结论不用改。
  • 每帧链路 = Calibration(DDT 有效时走 CalibrationMain_RTV: d=(raw−base)>>1 → 按段阈值选 (gain,off) → (d*gain>>12)+off,clamp 0..0xffff)
    • BlindCompensation(盲元按 3..8 邻域均值替换,DDT 盲元表) + Mirror(关) + TempRangeStat + GrayEnlarge(内含 HistDeRedundance)。E2X/E4X 仅当 EX≠0 (App imageEX=0),FilterDetailEnhancement(_Simple) 仅当 CFunctions+0x28≠0 (从未被写=0)。→ 与我们的实现一一对应,没有缺的阶段。
  • 标定表只在 FFC 窗口重建:DoSomething(param_1==0)→SelectCaliCurve+ GenerateVolatileLens;param_1≥1 时在窗口各帧分块 InterpolateShort/UShort (块大小按 (13+总字节+尾-2)/(13-1)+7&~7,完成后置 0x2d24=1)。不在每帧跑 → 我们"FFC 时 rebuildTables"一致。
  • FFC 时序逐帧一致:触发→cb(0) 关快门 →帧 1..5→帧 6..9 逐帧 PushBaselineRaw (首帧 Restart,4 帧块平均)→第 9 帧回调 cb(1) 开快门→帧 10..13 DoSomething→帧 14 起正常。 我们的状态机(idx 1..5 / 6..9 / idx==9 onFfc(1) / 10..13)完全同相。
  • 设备配置命令:setFrameRate(P2D_SetFrameRate=0x6BB6B679,载荷 interFrame/interLine 两个 u32;fpsMode 0→9/12,否则 105/105)只在 devType 1/2 时发;我们 devType=3 → 官方也不发。P2D_SetParameter1/2 官方 USB 路径只发 Get。→ 原始流配置两边相同。
  • 工具链记录:普通版 APK=app/【普通版】MAG-Cx.apk(libcxsdk.so 372KB armeabi, md5 4d8d98bb…,与 analysis/sdk_re/android_app/bin 相同;JNI 层是 libcxx2java.so 67KB)。 analysis/ida/libcoresdk_arm64.so 是专业版 MAG-Mx 的库,别拿它推普通版 (它的 0x439d80/88 内参在 .bss,文件里读不到)。IDA Pro 9.4 批量反编译命令: idat.exe -A -S"<script.idc>" -L"<log>" <so>(脚本按 get_next_func 遍历; 对 __imp__ 导入 thunk 会异常中断,注意跳过;Ghidra headless 兜底)。
  • test_frames.bin 夹具是合成/重复帧(60 帧 mean/std 完全相同),不能用于噪声分析。

16.2 定量对比(调色板弧长反演,脚本在 %TEMP%\mag160c_re\analyzer.py,临时件)

方法:ironbow 256 色表(VendorPalettes)→ RGB 弧长参数化(消除调色板斜率影响)→ 12 px/传感器像素块中值采样到 160×120 → 3×3 高通。数据:用户双截图(竖屏)+ 真机连拍 3 帧(官方横屏 crop、我们竖屏,均 PNG 截屏无 JPEG 伪影)。

指标(弧长单位 ×1e-2) 官方 我们 比值
整图低频 σ(对比度/音调曲线) 20.2 21.2 1.05(一致)
scene(>8px) σ(真实场景对比度) — — 0.95~0.97(一致)
高频 σ(单帧 1~8px 频带) 1.16~1.21 2.10~2.51 ~2×
时间分量(连拍差分/√2) 0.52 0.75 1.44×
沿显示水平线均值的 std(=逐传感器行固定偏移) 0.106~0.15(max 0.57) 1.20~1.84(max 12.1) 8~17×

分块统计(背景块):scene 比 0.95、1-8px 频带比 1.58、时间比 2.0 —— 我们多出来的 能量集中在 1~8px 频带且逐帧可复现(固定)。

16.3 结论与下一步

  • 缺陷:我们的渲染里有一个官方没有的逐传感器行固定偏移(残余行 FPN)。 显示旋转 90° 后,传感器行=屏幕水平线 → 就是用户看到的"背景条纹"。 它是固定的(连拍均值里都在),所以不是每帧算法、也不是显示插值(两边显示链等价: 都是 160×120 数据 → 12 px/px 双线性上屏)。
  • 候选(按嫌疑排序):
    1. FFC 参考帧被污染/时序错位:官方与我们都"关 5 帧→采 4 帧→第 9 帧开",但我们的 帧计数从自己的处理循环来;USB 抖动/丢帧会让采集窗错相(§9.4 曾测得 "延到 8 帧反而变差"——设备实际快门相与我们假设的错位的旁证)。 参考≠纯快门帧 → 减出的残差里留下行结构。
    2. DDT 段表 (gain,off) 的布局/插值在当前 FPA 温度下与我们不一致 (我们 rebuildTables(shutterAtFfc + fpaTempFix);官方 SelectCaliCurve 的温度 来源、端点选择、Interpolate 的语义需要逐字段比对)。
    3. 盲元处理差异(我们已实现 nucAndBlind,但要和官方 BlindCompensation 的 邻域定义逐条对——它用 10 int/条 的记录、邻居偏移含 ±1 与 ±行距)。
  • 验证路径(下一个模型从这里接):
    1. App 加"原始帧录制"开关(IrSession 读线程帧入口处写 files/,含一次完整 FFC 的 60+ 帧与帧头);真机录一段。
    2. 离线重放:raw → 参考 → NUC → 表插值 → LUT,每一级都算"沿行均值 std", 找出引入行偏移的那一级;再用录到的数据做 A/B(改窗口/改表温度/改盲元)复现官方数值。
    3. 真机 A/B 验收指标就用 16.2 的表:目标"逐行固定偏移 std ≤ 0.2"。

17. 第三十八轮:原始帧录制 + 逐级重放全链路定案 + 显示链修复(2026-10-07 凌晨)

按 §16.3 清单执行。工具:IrSession.rawFrameSink + RawFrameRecorder(调试开关, files/rawcap.enable 标志文件触发,录 raw+NUC dump 到 files/rawcap/run_*.bin, 带 FFC 触发,300 帧自动停);离线重放脚本在 %TEMP%\mag160c_re\(临时件)。

17.1 逐级重放(整数语义复刻,与真机 NUC dump 互锁)

录制 300 帧(含 3 次完整 FFC 窗口)。Python 逐级复刻(i32/算术移位/向零截断与 Kotlin 一致),与 App 自己 copyNuc 的 dump 对拍:corr=0.9996,仅盲元像素大差异 ——复刻可信。各级行交替(alt = 行均值相邻二阶差,固定分量):

级 行交替 std(counts)
raw(场景帧固定行剖面) 596.9
ref(4 帧快门平均) 596.7(与 raw 图样相关 0.999)
d2 = (raw−ref)>>1 5.20
NUC(查表后,App dump) 4.25

结论 1:参考减除消掉 98.4% 的行 FPN,表再消一半,残差 4.25 counts 就是 显示条纹的直接来源(换算到显示域 ≈ §16.2 的我们 1.2–1.84)。

17.2 排除清单(都做了 A/B)

  • 参考窗相位(±2 帧移动):4.01–4.33,不敏感。
  • 参考平均长度(1/4/6/8 帧):4.14–4.51,不敏感(时间噪声早已被平均压低)。
  • 表插值温度(33637…47664 扫描):4.06–4.57,不敏感。
  • 残差时间结构:FFC 后 1–10.3 s 固定分量 5.14–5.34,完全平坦 → 不是参考老化。
  • 跨 FFC 窗口:d2 残差图样相关 +0.986 → 不是每次 FFC 的随机量。
  • 残差与 raw/ref 图样相关 ≈ 0.02/0.03 → 白状固定残差,即"快门态与开放态的 行 offset FPN 图样差"(≈1.7%),设备固有。
  • GenerateVolatileLens(0x18314):11 段温度响应折线,只进温度链(this+3163 默认 1 时直通),与图像链无关 → "官方把参考融合进表"的假设不成立。
  • 官方 SDK 全函数核对(本轮新读):CalibrationMain_RTV(段选择/(d*gain>>12)+off/ 布局与步进逐字段一致)、PushBaselineRaw(双累积器 +14212/+14244)、 CAccumulator::Reset/PushFrame/Restart(mode=baseLineAcc=4 恰好 4 帧块平均)、 Raw2Temperature/CFunctions::PushFrame(0x3808=1 旁路、每帧渲染、FFC 条件同相)、 TempRangeStat(纯 min/max/mean)、BlindCompensation(typ 3..8 邻域 switch)、 InterpolateShort/UShort(Q12 端点插值同式)、SelectCaliCurve、DoSomething、 Controller::PushFrame/StartProcess(A=5/E=4/C=4 同相;PushBaselineRaw 收 JNI 原样 帧,Java ThreadImgProcess 不做预处理)。官方每帧链路与我们逐字段一致,没有 任何"额外处理"能解释残差差。

17.3 显示域 A/B(同场景同铁虹,§16.2 口径:12px 中值网格 + 弧长 + hf3 行/列剖面)

测量 ROW-dir COL-dir
官方截图(横屏) 0.12 0.31
我们修复前截图 1.41 0.46
我们修复后截图 1.32 0.59
我们修复后截图(mask 掉 UI 覆盖层后) 0.76 0.67

重要测量教训:我们的竖屏 UI 把中心温度/MAX 标注/色标条画在热像区内,它们对 行剖面的贡献与真实 FPN 同量级——对比测量必须 mask(官方 UI 覆盖为 0)。 §16.2 的"8–17×"里含此混淆 + 几何错位(37 轮连拍的官方横屏 crop 与我们竖屏 行/列方向不一致);本轮修正后真实 NUC 域差距约 1.6×(同一份我们的数据走 官方显示链复刻 = 0.19,走我们链 = 0.32,官方截图实测 0.12)。

17.4 定案与修复(初版,17.6 有修正)

  1. 官方显示链的两级结构(jadx ImageViewer + 本轮重核):SDK 输出 160×120 bitmap → PaintFlagsDrawFilter + FILTER_BITMAP 画入 640×480 离屏(4× 双线性) → SurfaceView 再放大到屏。中间 4× 双线性对逐行 FPN 图样有 ~0.6× 的低通—— 同一份我们的 NUC 数据:走官方两级链复刻 = 0.12,走我们单级 12× 复刻 = 0.32。 "官方背景条纹更不明显"的主导机制是显示链,不是 NUC 域。
  2. 修复 A(nativeOut):RenderPipeline 去掉 PC 版遗留的内部 2× 插值级 (官方安卓没有),输出原生 160×120 调色板图;LiveRenderer/录像/拍照全链 适配。复刻证明它对行交替数值无影响,但消除了自造的"全幅/半幅"行调制 (§35 的竖列交错在行方向的同类项),显示等价性更完整。
  3. 修复 B(640×480 中间级):LiveRenderer 按官方 viewer 补上 640×480 离屏 位图(FILTER_BITMAP 4×)再上屏,显示链与官方逐级等价。
  4. 遗留(诚实记录):UI mask 后我们 0.76 vs 官方 0.12 仍有差距,其中 a) NUC 域残差 2.87 vs 官方等效 ~1.3–1.8 counts(1.6×,同显示链对比)—— 与"快门态/开放态行 offset FPN 图样差"相关,设备固有,官方同算法也应承担, 差异来源未定位(疑似两 App 会话时设备状态不同); b) 真机 GPU 显示链与 PIL 复刻间仍有 ~2× 未解释差(复刻 0.34 vs 真机 0.76), 需下一轮用"真机截图 vs 同帧 NUC dump 的逐像素对照"收敛。 §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 在本机失真不可用)。

  • 同帧残差分解(run3 dump 末 30 帧 vs 同秒截图,均转弧长域):

    • 空间相关 0.994,残差整体 std 0.022(大结构完全一致);
    • 显示链"添加"的交替:ROW +0.146 / COL +0.358(×1e-2)——把 arc 域 ROW 0.13/COL 0.31 抬到截图实测 0.21/0.45–0.52(平方和吻合)。来源是 真机 GPU 渲染链(160×120→1440×1920 整数放大 + SurfaceView 合成)与 RGB 域 PIL 复刻之间的差异——RGB 复刻(含 2.25× 中间级、官方 3× 等全部 几何变体)均不产生此添加(0.07–0.11 / 0.24–0.28);
    • 残差无强周期(FFT 顶部为低频 (0,±2)),COL 剖面 x≈75–78 的尖峰是 MAX 文字 mask 边缘的反演伪影。
  • SearchSeparateLine(0x1a768)排除:逐行均值 → 滑动分割统计找"场景上下 差异最大的行"(地平线/挡板边缘检测),供分区统计/增强(SubSectionEnlarge) 用,与 FPN 校正无关;由 this[23](字节 92)控制。同轮排除的还有 DoSomething(= 分块表插值,与我们一次全量等价)与 base 指针闭环 (this[3555] = CAccumulator(+14212) 输出缓冲 = 4 帧块平均,this[3560] = 其初始化标志)。

  • 收敛建立:显示域差距 = NUC 域差(~1.6×)⊕ 显示链添加(ROW +0.146/ COL +0.358)。两个未解子问题:①真机 GPU 渲染为何添加交替(Skia 采样 行为/色彩管理/dither),可用 App 内直读 Bitmap vs 截图对比定位;②NUC 域 官方等效 1.3–1.9 counts vs 我们 2.3–2.5/4.7–5.7 的根源(同算法同参考同表 下不应有差,每帧路径已穷举——剩下只有标定文件内容差异一条未验证:我们用 assets 内置 DDT,"cali cache vs bundled DDT: identical" 的 md5 检查成立, 但官方缓存由 GetCaliFile 从设备下载,可逐字节 diff 官方缓存与 assets) 以及 LoadCalibrationTable(0x19a98)的加载语义未逐行核对。

  • 波动性实验定案(run6/7/8:同连接换参考 + 跨连接,鲁棒口径):

    run NUC ROW (counts) NUC COL DISP ROW DISP COL
    run6(连接 A,参考 A) 2.37 4.74 0.21 0.45
    run7(连接 A,FFC 换参考 B) 2.25 5.19 0.21 0.52
    run8(USB 重枚举,连接 B) 2.45 5.69 0.23 0.50

    ROW 跨参考/跨连接 ±5% 稳定、COL ±20%——差距是系统性的,不是设备状态 随机性。本轮三次测量的显示域 ROW 稳定在 0.21–0.23(与 17.6 的 0.22 吻合)。 最终量化:显示域 ROW 2.2×、COL 1.7–1.8×(0.22/0.10、0.50/0.28),全部 源于 NUC 域(我们 ROW 2.3–2.5 / COL 4.7–5.7 counts vs 官方等效 ~1.3/1.9)。 下一轮主攻方向:获取官方 NUC 域的直接数据(官方照片 MDT 块结构再核查、或 官方会话下的间接测量),以及重新审视"参考减除之外,官方标定表在使用时是否 带着现场参考做了一次我们没复刻的更新"——所有已读函数均无此逻辑,需要更早期 的初始化路径(CFunctions::Reset→LoadCalibrationTable 的文件格式)再排查。

17.5 工具与现场

  • RawFrameRecorder/rawFrameSink/flag 轮询已合入(调试工具,默认不活动)。
  • DebugLog.enabled 本轮临时 true,提交前已改回 false。
  • 现场状态:rotation 锁竖屏(原值 1,待用户恢复);我们 App 前台出流; MIUI 的 App 自动启动会在授权后把焦点拉回我们 App,切官方测前先 force-stop 我们,用 TAB×2+ENTER 过 USB 弹窗(横屏时 input tap 坐标不可靠)。

17.7 第三十九轮:GPU 画布假设被真机 A/B 推翻,真因 = 7×7 细节增强(2026-10-07)

按 §16.3/17.6 待办清单执行(首验 GPU 硬件画布 → 正式修复 → A/B 验收)。全部测量用 39 轮新脚本 final39.py/final39b/c/d/e.py(口径:12px 网格 + UI mask + 弧长反演 + 鲁棒 std;暗区 = sensor 行 0..40 × 列 90..160;官方横屏直接同区;38 轮存档截图全部 可复现,官方 FULL 0.10/0.28、暗区 0.15 与 17.6 表逐位一致,DARK 对照里照片 JPEG 0.25 也精确复现)。

  • 首验(软件画布强制 false):fps 11.6-12.4(avg 84.6 ms < 15 fps 预算 66 ms), 软件画布在 160×120 buffer 时代仍不达标(33 轮历史问题重演)。同场景 A/B (唯一变量 = 画布类型,5 分钟内截屏):暗区 ROW 0.49/0.28/0.19/0.33(软)vs 0.46/0.31/0.32/0.36(硬)——无显著差,"GPU 硬件画布添加交替"定案否定。 17.6 遗留的"暗区 ROW 2.07"在所有存档截图 × 所有合理口径(网格/逐像素 × mask/无 mask × 顶/底暗区)下均无法复现(最高 0.47),该数字是 38 轮会话末 一次性计算、无脚本存档,且与其方向性结论一起作废。
  • 备选 a(Skia 6× 预放大 + 硬件整数 2× blit):720×960 中间级实现后 fps 达标 (avg 31.9 ms,15.1 paints),但暗区 ROW 0.43/0.28/0.41/0.43 vs 单级 0.46/0.31/0.32/0.36——无显示收益,已回退(SF 采样比假设同时否定)。
  • App 内位图 dump(决定性归属实验,flag 门控调试设施,结论落地后已删): 同帧三方(统一口径)——管线 RGB 输出(bitmap dump)DARK ROW 0.35、 Skia 6× 后 0.21、同秒截屏 0.27;离线复刻(同 NUC 数据走干净显示链) 0.16。合成/截屏不添加(dump≈截屏);差源头在管线 RGB 输出本身 (0.35 vs 复刻 0.16)。
  • 真因定位:真机 mag160c_settings.xml 实测 enhanceLevel=4(strength=32, 7×7 细节增强开着)。DetailEnhance.enhance 在灰度图上 gray += strength·detail/32768 (detail ∝ center−7×7 局部均值,暗区行 FPN 被同比例放大);而官方 App 从不 调用 SetDetailEnhancement(jadx 全量无此符号,22 轮已记录"开 2 级噪声反而 更大")——官方背景条纹更不明显的真因 = 我们开着官方从不开启的增强。 38 轮的"显示链添加 ROW +0.146"(复刻 0.13 vs 截图 0.21 的平方和差)与"NUC 域 1.6×"疑云同源:复刻链无增强、真机链有。
  • 修复与验收(同场景 A/B,唯一变量 enhanceLevel 4→0): 暗区 ROW 0.31-0.46 → 0.14-0.23(官方 0.15);全幅(final38 同口径) ROW 0.29 → 0.15(官方 0.10,1.5×)、COL 0.54 → 0.34(官方 0.28,1.2×, 38 轮已达标)。§16.3 目标"逐行 std ≤ 0.2":nomask 暗区 0.18 达标,mask 口径 0.22-0.23 贴线(mask 边缘残差,官方无 UI 不受此影响)。fps 15.1(38 轮同 代码实测)。
  • 落点:LiveRenderer 维持 38 轮 nativeOut 单级(预放大/dump 试验代码已回退, 类注释记录 39 轮定案);DebugLog.enabled 改回 false;设置页"图像增强"对话框 去掉"4 级(推荐)"标记并注明"开启会放大暗场条纹、官方实时画面从不开启"; 用户设备 enhanceLevel 已恢复 0。DetailEnhance 移植本身保留(官方 SDK 功能、 单测锁定,用户可选开)。csdk/README.md 的 38 轮段已同步修正。
  • 口径教训(承 17.6):38 轮"同帧对照"的截图(sync 系列)与照片对照 (withphoto/photo.mdt)都含增强,而所有"复刻"都无增强——两个未解子问题 (①真机 GPU 为何添加交替 ②NUC 域 1.6× 根源)被同一个混淆变量解释,均结案。