16 KiB
手机端渲染差异定位(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. 顺带确认的两件事
- 环境温度输入:官方 App 调
setEmissivity(emissivity, ExtPara.intCurrentEnvTemperature)—— 即 PC SDK 的dev+0xA4(我们温度链里的ATZeroErrorPoint)就是环境温度 (设备默认 22216 = 22.2 ℃);App 的发射率设置也走这里(100 = 1.00)。 → 我们 App 的发射率设置应当接进OfficialTemp.temperature(emissivity=…)(当前固定 0.98)。 - 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. 下一步(按此顺序)
- 移植
CalContrastOffsetedAutoEnlargeRange(含亮度项到statsWindow),并在设置页加 "对比度/亮度"(默认值取官方 App 的默认),重拍对比 → 目标是 luma 直方图与官方吻合。 - 补
getExtParameter命令(Android SDK 侧 JNI:DeviceController.getExtParameter), 把设备的intContrastOffset/intBrightOffset/intCurrentEnvTemperature接进来。 - 温度读数接 App 的发射率设置(
OfficialTemp已支持参数)。 - 以上在 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::SetColorPalettecase 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 下一步
- 移植
SetColorPalettecase 2 的生成式(4 段线性,常量 0x75/0xbc/0x25/0xff/0x27/0x3b/0x1c3… 已在反编译中), 替换 App 索引 2 的表;然后同一分钟内截两 App 实时图做"亮度→颜色"曲线回归(目标:中位色差降到控制组水平)。 - 顺带核对其余 11 个索引是否也应改用 Android 生成式(本轮
VendorPalettes已覆盖 10 个)。 - 之后再按用户要求把验证过的改动同步进
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 待办
- 同一步骤同步进
csdk(C 参考实现把该参数作为配置项/默认关闭,并更新其测试与文档)。 - 其它 11 个调色板索引与 Android 生成式的逐表核对(本轮已确认索引 2 一致)。
- 若要进一步逼近官方观感,可继续量: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 |
两点结论:
- 官方的灰度画面比我们平滑约 2.3 倍(0.34 vs 0.76)——这不是调色板/亮度能解释的;
- 官方的帧间噪声在收敛(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 下一步(精确)
- 把我们的参考帧收集改成与官方一致:FFC 窗口内逐帧 IIR 累积(权重 =
CAccumulator的 mode,来自 BasePara2.baseLineAcc=4;窗口长度按官方 baseline 状态持续时长),IrSession/RenderPipeline的黄金测试继续用文档化的旧配置(默认 4 帧等权)以免破坏 C 对齐; - 装机用同一套灰度域指标回归:目标空间 HF ≈0.34(±20%)、帧间噪声保持不变;
- 之后同步 csdk(pivot + 温度链 + 参考累积)。