10 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。