# 手机端渲染差异定位(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 + 温度链 + 参考累积)。