Files
MAG160C/docs/android_app/rendering_diff_phone_20261006.md
T

265 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 手机端渲染差异定位(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 + 温度链 + 参考累积)。