Files
MAG160C/docs/android_app/rendering_diff_phone_20261006.md
T

12 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 更低)——目前主项已对齐。