Files
MAG160C/docs/android_app/rendering_diff_phone_20261006.md
T

17 KiB
Raw 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 + 温度链 + 参考累积)。