Files
MAG160C/docs/android_app/rendering_diff_phone_20261006.md
T

419 lines
28 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 + 温度链 + 参考累积)。
## 10. 定案(真机 libcxsdk 反汇编 + 官方照片定量):显示 LUT 才是差异,§9 的机制被推翻
数据源换成**手机上的官方库本体**:`adb` 从官方 App 的 APK 目录取
`lib/arm/libcxsdk.so`(arm32,本 App 只用 arm),IDB 里按 vtable 指针
(`0x5a7xx` 段)解析出真实函数体,逐条指令核对(Hex-Rays 会吞掉隐式除数,
必须看反汇编)。以下结论**全部以该反汇编为准**。
### 10.1 先纠正前两轮的错误结论
| 前结论 | 反汇编事实 |
|---|---|
| §8「官方不启用 CDF 75% pivot,所以我们要关掉」 | **错**。pivot 支路由 `theFunc._96_4_` 控制,`Controller::Controller()` 把它设为 **1**,全库无第二处写入 → 官方**是开着**的;而且它的写法不是 75% 分位,而是 `max(center_index, (meanBelow + meanAbove) >> 1)`。当时"关掉更接近"是因为**用的 LUT 本身是 PC 版**(结构与 Android 版不同),两点误差互相抵消。 |
| §9.3「官方参考帧是长窗口 IIR 指数平均,比我们干净」 | **错**。`CAccumulator::PushFrame` 的 `mode≥4` 分支是**恰好 mode 帧的块平均**(`sum>>log2(mode)`,计满清零),mode=2 才是 `(buf+new)>>1`。`Controller::PushFrame` 的快门状态机对 **A+1..A+E 共 E=baseLineAcc=4 帧**各推一次 → 官方参考 = 4 帧块平均 = **和我们完全一样**。 |
| §9.2「官方画面空间噪声比我们低 2.3×」 | **测量有分辨率偏差**:官方 App 显示的是 SDK 原生的 160×120 输出(照片 640×480 = 4×),我们的实时画面是 320×240(SDK 2× 后再 ×3 = 960×720 照片)。逐屏像素 HF 随放大倍数变化,两个 App 的放大倍数不同 → 该数字不可比。 |
### 10.2 官方每帧链路的完整配置(都已定位到 App 自己的调用)
`Controller::StartProcess(cb, fpaW, fpaH, sn, devType, shutterCloseSpeed,
shutterOpenSpeed, ffcTriggerFrame, ffcTriggerTemperature, palette,
baseLineAcc, caliPath)` —— 对应 App 侧
`UsbCommunication.startTransfer()` 的实参;真机 BasePara2 实测:
`shutterCloseSpeed=5, shutterOpenSpeed=4, ffcTriggerFrame=1800,
ffcTriggerTemperature=250, enlargeRange=8, baseLineAcc=4, fpaTempFix=1699`。
| 阶段 | 官方取值 | 我们 |
|---|---|---|
| FFC 相位 A/E/B | 5 / 4(=baseLineAcc)/ 4 帧 | 5 / 4 / 4 ✔(`ffcStep`) |
| 参考帧 | 4 帧块平均(`CAccumulator` mode=4) | 4 帧等权平均 ✔ 等价 |
| 曲线插值温度 | `帧尾 FPA + fpaTempFix(1699)`(App 加,SDK 的 `SelectCaliCurve`/`GenerateVolatileLens` 用它) | **帧尾 FPA(少了 1699)✗ 本轮修复** |
| 窗口 | `winLo=min(mean−range/2, statMin)`、`winHi=max(mean+range/2, statMax)`,`range=(enlargeRange*1000)>>3=1000`(对比度 0;`this+0x6c` 全库未写 → 亮度项恒不生效) | 一致 ✔(DDT 头 +20 读到的正是 8) |
| 灰度 LUT | Android 版 `HistDeRedundance`(见 10.3) | **PC 版结构 ✗ 本轮修复** |
| 时域平滑 `CSmoother` | 关(`this+0x380c = IntrinsicPara[0x20]`,构造默认 1,需 >1 才生效) | 未实现 ✔ |
| 显示侧累积 `CAccumulator(+0x37a4)` | 关(`this+0x3808 = 1` → `Raw2Temperature` 直通) | 默认关 ✔ |
| `setEX` 电子变倍 | 关(`imageEX` 默认 0 → `SetEX(0)` → `0x3814=0`) | 无此功能 ✔ |
| 7×7 细节增强 | **关**:`MapTemperature` 确实把门槛 `0x3864` 置 1(所以 `PushFrame` 每帧都会调 `FilterDetailEnhancement_Simple`),但强度来自 `CFunctions+0x28 << 3`,而该字段**全库从未被写入**(CFunctions 构造只 memclr,Controller 构造设了 0x1C/0x2C/0x30 等但没设 0x28)→ 传 0 → 函数首行 `if (param_1 != 0)` 直接返回 = **空操作** | 默认 0 ✔ 一致(我们的用户开关是超集,官方 Android 侧根本没有入口) |
| 翻转 | App 侧 `dwFlip` 只是存进全局 `0x6b90c` 结构,本库无人消费(翻在 App/显示层做) | 我们在显示/保存层做 ✔ |
### 10.3 真正的差异:灰度 LUT(Android 版 vs PC 版)
拿我们**自己照片里存的 NUC 计数块**与**官方照片**做像素级对齐(官方照片 = 官方实时渲染,两者同场景同机位):
- 垂直翻转后相关性 **r = 0.99** → **计数本身几乎一致**,差异**只在"计数→调色板索引"的映射**;
- 官方映射 ≈ `0.84 × 我们的LUT + 21`(被"压缩并抬升"):官方平坦背景落在索引 **45–55 的一段平台**上,我们的旧 LUT 把它压到 **0–20**(暗端被压死)→ 这就是"官方对比度更高 / 更细腻 / 条纹更少"观感的来源;
- 逐条核对反汇编后确认 Android 版 `HistDeRedundance` 与 PC 版(我们原来移植的)**结构不同**:
固定基值 `16*(448+447*brightness/200)`(PC 版是"自动居中 + 时域平滑"的二分搜索)、
步进上限 `max(1,(0x1800+4j)·k>>13)`(`k=min(0x200,16·span/range)`)、
反向 CDF 从 `hist[pivot]` 起(PC 版从 `pivot+1`)、平滑后 `hist[1023]=hist[1022]`、
pivot 用"两段均值的平均"。
**已落地**:`RenderPipeline.androidLutOf()` = 逐指令移植(含 golden 向量单测
`AndroidLutTest`),实时会话 `androidLut = true`;PC 版路径保留,C 参考黄金测试不受影响。
### 10.4 在线拟合与遗留项
用官方照片做目标(同一 NUC 帧、逐像素比对,均方误差以索引计):
| 配置 | 平均误差 | 低端 p5/p25 |
|---|---|---|
| 我们旧 LUT(PC 版 + pivot 开,= 09-12 照片) | ≈35 | 8 / 17 |
| 我们旧 LUT(PC 版 + pivot 关,= round-30 实时) | ≈14 | 15 / 29 |
| **Android 版移植(k 按公式 = 21)** | **12.5** | 24 / 36 |
| Android 版移植,k=16(拟合最优) | **5.9** | 45 / 54 |
| 官方照片实测 | — | 45 / 52 |
→ 移植已在正确轨道上;**遗留一处常数**:反汇编算出 `k = 16·span/range = 21`,
数据偏好 ≈16。两个可能来源,都需要真机同刻 A/B 才能定:
① 计数统计仍有细微差别(最可能就是 §10.2 的 `+fpaTempFix`,它改变插值曲线→改变计数分布);
② 该 build 的 `v45` 常数与反汇编读出的 16 不同(已在反汇编里逐位核对过 `LSLS R6,R0,#4` + `uidiv`,倾向排除)。
**在真机上先验证 `+fpaTempFix` 是否让计数分布与官方一致**,再看 k。
### 10.5 本轮已改代码
1. `RenderPipeline.androidLutOf()`:Android `HistDeRedundance` 逐指令移植(新增,含测试);
2. `IrSession` 实时管线改用 `androidLut = true`,删除 round-30 的 `cdfPivot75 = false` 临时开关;
3. `RenderPipeline.fpaTempFix`(构造参数)+ `ffcStep`/`frameRemote` 调 `rebuildTables(shutter + fpaTempFix)`,
`IrSession` 从 `BasePara2` 传入(官方 `UsbCommunication` 同款语义)。
### 10.6 待办
1. 真机 A/B(手机当前被 PIN 锁屏,需机主解锁):同一静止场景,官方实时 vs 我们实时,用"调色板反解灰度 + 逐像素对齐"复核 10.4 的 k/计数假设;
2. ~~7×7 细节增强按官方默认打开~~ —— **已排除**:强度源 `CFunctions+0x28` 全库未写 = 0,
官方那一步是空操作(这也解释了为什么离线拟合"打开反而略差");我们的实现保持用户可选、
默认关。顺带:我们的 `DetailEnhance` 与反汇编有一处细节差(`LocalMap7x7_Simple` 的
min/max 还要多采每行的第 1 列,共 5 列;我们只采了求和用的 4 列),会让门槛略偏严,
属可选功能的精度问题,不影响默认路径;
3. 同步 csdk(Android 版 LUT + fpaTempFix + 温度链)。
## 11. 真机验证(2026-10-06 下午,手机解锁后)
### 11.1 先修掉一个"实时全黑"的 bug(并补上会拦住它的测试)
Android LUT 落地时把 `IntArray` 的 LUT 用 `System.arraycopy` 拷进 `ByteArray` 的 `lut`
——**每帧抛 `ArrayStoreException`**,实时画面整块全黑(SurfaceView 仍在刷,logcat 无异常)。
黄金向量测试只覆盖纯函数 `androidLutOf()`,没覆盖实例路径,所以没拦住。已改为逐元素拷贝,
并新增 `AndroidLutPipelineTest`(4 个端到端用例:`androidLut=true` 能渲染出非全黑/单色画面、
两种 LUT 结果必须不同、`fpaTempFix` 必须改变插值表、手动 FFC 之后仍能渲染)。
单测 113 → **117 全绿**。
### 11.2 同场景 A/B:映射已经对齐
手机(22041211AC,官方 App 与我们的 App 前后 2 分钟各拍一张,同一静止场景),
用**调色板索引空间**在同一 160×120 网格上比对(避开两个 App 不同的放大倍数):
| 指标 | 官方 | 我们 |
|---|---|---|
| 索引分位 p5/p25/p50/p75/p95 | 82 / 89 / **96** / 117 / 133 | 85 / 92 / **98** / 114 / 134 |
| 逐计数中位索引(9490→10330 共 21 档)差值 | — | **中位 2.0(最大 4.5)** |
| 全图平均 \|Δ索引\| | — | **4.47 / 255**,中位 3.6,85.1% ≤8,99.2% ≤16 |
对比上一轮的旧 LUT:低端被压死(我们 p5/p25 = 8/17,官方 45/52,等效系统偏差 ≈35 级)
→ **"官方对比度更高、更细腻"的映射差异已消除**。
### 11.3 复核那个遗留常数 k:反汇编公式是对的
用我们照片里存的 **NUC 计数**逐像素对官方照片做模型比对(新的一对照片,
计数窗 [9142,10383],span=1241,公式 k=19):
| 步进常数 | 平均 \|Δ索引\| | 中位 | ≤8 占比 |
|---|---|---|---|
| **k = 16·span/range(反汇编公式,=19)** | **4.64** | **3.9** | **82.9%** |
| k = 16(上一轮"拟合最优") | 5.76 | 5.1 | 73.6% |
| k = 12 | 9.16 | 8.1 | 49.2% |
| k = 24 | 4.92 | 4.1 | 80.8% |
→ **按反汇编公式实现即为最优**;上一轮"数据偏好 ≈16"是当时那对照片(曝光/窗宽都不同)的产物,
**结论作废,代码不动**(§10.4 的遗留项关闭)。
### 11.4 遗留并已定位到域:我们的"计数"噪声偏大
同一个 LUT 作用在**我们的 NUC 计数**上得到的 HF(传感器网格)明显高于官方成品:
| 数据 | HF \|dx\| / \|dy\| |
|---|---|
| 官方照片(其渲染输出) | 2.46 / 2.22 |
| 官方实时截图(1.2 s ×3) | 1.61 / 1.51,帧间 1.83/1.77 |
| 我们的实时截图 | 2.35 / 2.39,帧间 2.34/2.40 |
| 我们的 NUC → 同一个 LUT(模型) | 4.65 / 4.25 |
即:**映射一致、噪声不一致**,且证据指向"我们的计数本身比官方毛"(同 LUT 下模型 HF 高 1.9×)。
排除项:FFC 之后立刻截图(新鲜参考)几乎不改善(帧间 2.44→2.34)→ 不是参考老化;
我们的实时帧率实测 **15.0 fps**、单帧绘制 6-9 ms(与第 22 轮记录一致,无回归)。
下一步方向:把**实时 FFC 参考采集**与已验证逐字节一致的离线/PC 路径对齐(同一个参考下
PC 端 NUC 是逐字节相同的,所以嫌疑在"实时状态机采到的参考帧")。
### 11.5 运维笔记(真机调试踩坑)
1. **正在推流时 force-stop App 会把相机卡死**(命令端点 halt、`GetCaliInfo` 超时、此后无流数据)。
恢复:`su -c 'echo 0 > /sys/bus/usb/devices/2-1/authorized; sleep 2; echo 1 > ...'` 重新枚举;
切换 App 时**先按 HOME 让 App 正常停流**。
2. MIUI 的 USB 授权在**每次重装/重新枚举**后都要重新授权;要勾"一律打开 MAG160C"
必须点**复选框本体**(x≈186),点标签中部(x≈718)不会勾上;授权后 App 需要重启进程才生效。
3. 相机被卡住时 App 的统计会显示 `reads=0 frames=0 timeouts=N`,UI 以 ~3.3 fps 空转刷新
(不是渲染帧率)。