811 lines
55 KiB
Markdown
811 lines
55 KiB
Markdown
# 手机端渲染差异定位(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 空转刷新
|
||
(不是渲染帧率)。
|
||
|
||
## 12. 追查"计数噪声偏大"(2026-10-06 傍晚,真机分阶段实测)
|
||
|
||
用户:"把'计数'噪声偏大修一下"。做法:在管线里临时插桩(DebugLog 打开、逐级打印
|
||
raw/参考/差值/NUC 的 mean/std/HF),真机跑一遍,把噪声预算算清楚,再看 A/B 差在哪里。
|
||
|
||
### 12.1 真机实测的噪声预算(设备 160043865,同一静止场景)
|
||
|
||
```
|
||
[diag] ffcRefFrame mean=15084 std=3625 hfdx=1172 ×4 帧(快门帧)
|
||
[diag] refFrameSpread sigma=30.2 / 27.5 / 27.2 ← 逐帧像素噪声(快门帧间差)
|
||
[diag] stage.raw mean=11442 std=3466 hfdx=1168 ← 场景帧原始响应
|
||
[diag] stage.ref mean=15083 std=3624 hfdx=1172 ← 4 帧快门平均
|
||
[diag] stage.diff mean=-1821 std=326 hfdx=29.1 ← (raw-ref)>>1
|
||
[diag] stage.nuc mean=9731 std=245 hfdx=24.2 ← 波段映射输出
|
||
```
|
||
|
||
- **快门帧是真快门**:参考帧均值 15084 与场景帧 11442 相差 3600 counts,且 4 帧之间
|
||
mean/std 一致到 0.1% → 快门在参考窗口内确实闭合、且是同一位置。
|
||
- **逐像素时域噪声 σ_n ≈ 21–24 counts**(由 refFrameSpread 的 √(1+1/k) 反解)。
|
||
- **我们的差值噪声 = 0.559·σ_n = 12.3 counts**(= 0.5·√(1+1/4),4 帧平均参考的理论值),
|
||
由实测 hfdx=29.1/1.128 推得的白噪声部分 ≈ 25.8 里含场景结构(NUC 的 24.2 同量级)→
|
||
**与理论预算一致,没有额外噪声源** ✓。
|
||
- **FPN 抵消到 2.4%**(1168 → 29.1)✓。
|
||
|
||
结论:**我们的参考采集与 NUC 链路的噪声与时域表现完全符合物理预算**,"计数噪声偏大"
|
||
在数据链路上**不成立**(上轮凭跨 App 指标推断的"我们的计数更毛"没有证据支持)。
|
||
|
||
### 12.2 那 1.2–1.5× 的跨 App 差是什么
|
||
|
||
同场景 A/B 复测(官方 16:10 / 我们 16:08,16:08 那张同时给了 raw+NUC 块):
|
||
|
||
| 指标 | 官方 | 我们 | 比 |
|
||
|---|---|---|---|
|
||
| 索引 HF \|dx\|(照片) | 1.98 | 2.84 | 1.43 |
|
||
| 索引 HF \|dx\|(实时截图) | 1.61 | 2.35 | 1.46 |
|
||
| 实时帧间 \|Δ\|(重采样到 160×120) | 1.83/1.77 | 2.34/2.40 | 1.31 |
|
||
| HF 随亮度分档比(40→200 级) | — | — | 1.18–1.35(近似**加性**,不是乘性) |
|
||
|
||
已知的**测量混淆项**(不足以解释全部,但都在同一方向):
|
||
1. 我方形如 960×720(=6× 传感器,JPEG q≈95 → 0.29 B/px),官方 640×480(=4× → 0.42 B/px)
|
||
→ 我们每像素压得更狠,JPEG 振铃/块效应**加性**抬 HF(与"近似加性"的观察一致);
|
||
2. 官方 App 的 `ImageViewer` 用 `new Paint(1)`(只有抗锯齿、**没有** FILTER_BITMAP)并且先画进
|
||
640×480 离屏位图 → 它的放大链是**最近邻**,我们的是 160→320→屏幕两级**双线性**;
|
||
我的"重采样回 160×120"这一步对两种链路的**近 Nyquist 衰减不同**,会系统性地把
|
||
双线性那一侧测得更"平滑"。
|
||
3. 我们 18-1 的**实时**帧间指标此前受 UI 元素(OSD 文本/色条)污染,本轮已用遮罩排除到 99%。
|
||
|
||
### 12.3 本轮据此**真正修掉**的两件事
|
||
|
||
1. **MDT 照片的 raw/NUC 配对**(数据完整性 bug):照片里的 FRAME 块原本写
|
||
`session.lastRawFrame`(每帧都更新,包括 FFC 窗口内的**快门帧**),而 NUC 块是
|
||
**场景帧**的渲染结果 → 拍在 FFC 之后的那张照片,两块不是同一次曝光(离线再渲染/
|
||
分析界面正是消费这一对)。现在由管线交付 `copyLastRenderRaw()`:**渲染该帧时**
|
||
把 raw 载荷存下来,照片两块必然同源。新增回归测试
|
||
`MdtRawNucPairTest`(对**低通后**的两块做相关性,>0.8;raw 的方差 99% 是 FPN,
|
||
逐像素相关性即使配对正确也接近 0,这个坑在测试里写明了)。
|
||
2. **LUT 的 32 位语义对齐厂商**:`cdf*denom` 的乘法与 `>>14` 改为 32 位无符号
|
||
(Kotlin 侧 `u32(...)` + 逻辑右移);对真实数据结果不变(golden 向量仍全绿),
|
||
但退化场景(几乎全空的直方图)下与反汇编一致。
|
||
|
||
### 12.4 留给下一轮的(不盲改)
|
||
|
||
- 若要*严格*分清"我们比官方毛 1.3×"是渲染还是测量:需要**受控采集**——同一帧数据
|
||
过两条链(官方 App 只有 JPEG 输出、没有 raw 服务),或用一次 PNG/无损导出对比。
|
||
- 官方实时画面更平滑的**可复现线索**:其显示链是"离屏 640×480 + 最近邻放大"。
|
||
我们若要更接近,可以(a)把保存的照片改成 4×(640×480)或提高 JPEG 质量,
|
||
(b)实时预览改单级放大。两者都是**表现层**改动,不影响已对齐的映射,风险低。
|
||
|
||
## 13. 用户复现:"低温区条纹更多"——定位到**大负差值**处的噪声放大(未修完,留了下一步)
|
||
|
||
用户:"我换了个场景,背景的低温区域在我们的 app 里面明显条纹更多"。手机仍在,做同刻 A/B
|
||
(同一场景:我们 16:35 拍、官方 16:37 拍;另有 12 s 间隔的漂移标定)。
|
||
|
||
### 13.1 先标定漂移(12 s 内、同一 App 两张)
|
||
|
||
官方 App 相隔 12 s 两张:分位 [9,17,44,73,229] → [10,18,45,74,229],逐像素 |Δ| 均值 1.20(中位 1.0)。
|
||
→ 12 s 的漂移约 1 级,**不足以解释跨 App 的分位差**,所以下面的对比是有效的。
|
||
|
||
### 13.2 冷区(最冷 25% 像素)量化
|
||
|
||
| 指标(照片,160×120 网格) | 官方 | 我们 | 比 |
|
||
|---|---|---|---|
|
||
| 逐计数中位索引(冷区 9830→10000 counts) | — | — | **最大 1.7 级** ✔ 映射一致 |
|
||
| 列条纹幅度(去 9 宽趋势后的列剖面 std) | 2.12 | 2.28 | 1.08 ✔ 基本一致 |
|
||
| 细纹理 HF \|dx\| | 1.71 | 2.17 | **1.27 ✗** |
|
||
| 实时截图冷区 HF(无 JPEG 干扰) | 1.06 | 1.01 | 0.95 ✔ |
|
||
|
||
**结论**:冷区的**映射**和**列条纹(FPN)**都与官方一致,差的是**细纹理/像素噪声**(1.27×)。
|
||
JPEG 只能解释一部分:把我们那张(960×720 q95,202 KB)按 640×480/q98 重压后冷区 HF 从
|
||
2.17 只降到 1.96(官方 1.71)→ 仍差 1.15×。所以**我们确实在冷区多出约 1.2–1.3× 的计数噪声**。
|
||
|
||
### 13.3 哪来的:整帧预算吻合,冷区超预算 1.65×
|
||
|
||
- 整帧(中等场景)实测:σ_n≈21–24 counts → 差值噪声 0.559σ_n=12.3 ✔ 与实测吻合(§12.1);
|
||
- **冷区**(大负 d;d=(raw-ref)>>1)实测计数 HF = **20.4 counts**,而按同一预算应为 12.3
|
||
→ **1.65×** ✗ —— 与"冷区更毛"的用户观察方向一致,且只在大负 d 处出现。
|
||
|
||
已逐条排除:阈值/增益的 16 位解析(与厂商一致,且厂商侧也是**有符号**比较)、band 选择
|
||
(映射一致说明段选择一致)、快门参考(4 帧快门平均 ✔)、JPEG(只占约 1/3)、
|
||
显示链(实时冷区 HF 反而更低)。
|
||
|
||
### 13.4 下一步(精确定位,需一次带插桩的实机)
|
||
|
||
在大负 d 的像素上分层打印:`d` 的分布、命中的 band、该 band 的 `gain/off`(插值后)与
|
||
`σ_n`,对 DDT 里 band0 的原始 gain/off 核一遍。最可疑的两条(都可控):
|
||
1. **`fpaTempFix` 是否也该作用在图像链**:我们按官方 App 的代码给插值温度加了 1699
|
||
(§10.5),如果该设备上图像链实际用的是帧尾原值,则 band0 的 gain 会被插值偏,
|
||
表现为"映射总体还对、但低端噪声被放大"——正好是现象;
|
||
验证:临时把 `fpaTempFix = 0` 出两版,量冷区 HF(同样一张场景、同样流程)。
|
||
2. band0 的 gain 插值本身与 PC 参考路径的差异(PC 端 NUC 逐字节一致,说明差异在
|
||
Android 的 `GenerateVolatileLens` 重生成路径,而非表本身)。
|
||
|
||
## 14. 定案并修复:"低温区条纹更多"= 我们自造的 2× 交错图案(反汇编依据 + 实测验证)
|
||
|
||
### 14.1 依据(官方安卓路径没有插值级)
|
||
|
||
`ImageViewer`(官方 App,jadx 源码):
|
||
- `this.bmpIr_ = Bitmap.createBitmap(fpaWidth, fpaHeight, ARGB_8888)`(**160×120**,即 SDK 直接给原生尺寸);
|
||
- `this.paint_ = new Paint(1)` —— 只有 ANTI_ALIAS,**没有 FILTER_BITMAP**;离屏 `cachedbmp_ = Bitmap.createBitmap(640, 480)` 就用这些 paint 画进去(= **最近邻**)。
|
||
|
||
而我们的 `upscale2x`(移植自 PC SDK)是**双线性**:偶列取原样本、奇列取邻均值 →
|
||
在传感器 Nyquist 上**人为造出一层"全幅/半幅"交错的细竖条纹**,且**只在色带最陡的暗/低温区**最扎眼。
|
||
|
||
### 14.2 实测验证(同场景 A/B,冷区=最冷 25% 像素)
|
||
|
||
| 冷区指标(照片,160×120 网格) | 修复前(双线性 2×) | 修复后(最近邻 2×) |
|
||
|---|---|---|
|
||
| 细尺度列条纹(去趋势宽 5)之比 | 1.16 ✗ | **1.00 ✔** |
|
||
| 细尺度列条纹(去趋势宽 9)之比 | 1.29 ✗ | **0.93 ✔** |
|
||
| 宽尺度列条纹(21 / 41)之比 | 0.90 / 0.87 ✔ | 0.82 / 0.72 ✔ |
|
||
| 列剖面相关性(同一 FPN 图案) | +0.815 | +0.832 ✔ |
|
||
| \|dx\| HF 之比 | 1.27 | 1.67(见 14.3) |
|
||
|
||
→ **用户报的"细条纹"就是这层自造交错**:去掉后细尺度条纹与官方一致(1.00/0.93),
|
||
而真实 FPN(宽尺度)我们本来就比官方更轻。
|
||
|
||
### 14.3 顺带说明:HF 比值反而变大不是变差
|
||
|
||
重建后的 `|dx|` HF 从 1.27 升到 1.67,是因为**旧的交错本身就是一次列方向的平滑**
|
||
(奇列被平均掉一半),它把这个指标"压低"了——去掉假平滑后露出的是真实逐像素噪声。
|
||
即旧值 1.27 是"假图案 + 被平滑的噪声"的合成,新值是纯噪声。逐像素噪声的差异
|
||
(同一场景、同一映射下)仍待查,方向已收窄到"照片路径的 JPEG/缩放"与"设备帧噪声"两者,
|
||
方法学与数据见 §12/§13。
|
||
|
||
### 14.4 落地
|
||
|
||
- `RenderPipeline.upscaleNearest`(构造参数,实时会话 = true;默认 false 保持 C 参考字节不变);
|
||
- csdk:`mag160c_render.h/.c` 增加 `upscale_nearest`(两个 demo 默认 1),同一段注释说明依据;
|
||
- 单测新增 `UpscaleParityTest`(两种模式必须不同 + 最近邻必须逐行复制),共 **119 全绿**。
|
||
|
||
## 15. 用户复核:"我们颗粒更大,尤其高低温分界边沿"——是我上一轮 14 节的最近邻改错了
|
||
|
||
用户:"我们的app图片看起来像素颗粒更大啊,特别是高低温分界的边沿,是不是啥滤波没开"。
|
||
|
||
### 15.1 眼睛 + 数据都指向同一个原因
|
||
|
||
17:51 同尺寸对照(左=我们 17:06 版,右=官方):**我们的边沿是 2 个传感器像素一级的台阶**,
|
||
官方是**单像素**的平滑过渡——即 14 节里"最近邻 2×"把有效分辨率砍半了(相邻两个传感器像素被复制成同一个值)。
|
||
定量复核:冷区 `|dx|` 比值 1.27(我们更毛)。
|
||
|
||
反汇编/源码再核对:官方 `ImageViewer` 除 `new Paint(1)` 外还有 `PaintFlagsDrawFilter pfd_`
|
||
(会**加上** FILTER_BITMAP)→ 它的显示/保存链其实是**从 160×120 单级滤波放大**
|
||
(`cachedbmp_` 640×480),既没有交错也没有块状复制。
|
||
|
||
### 15.2 修法(照片改为单级放大;实时缓冲回退到插值)
|
||
|
||
- **照片**:`PhotoSaver.SENSOR_W/H` 改为 **160×120**(原生传感器网格,与探针/NUC 同一空间),
|
||
`RENDER_SCALE = 6`(输出仍是 960×720);调用方交入的帧改为**精确取样** 320×240 缓冲的偶数像素
|
||
(`o[2x]=src[x]`,所以偶数采样就是原始像素,无重采样误差)。→ **单级 6× 双线性**,
|
||
传感器像素级颗粒、无交错、无块状。
|
||
- **实时缓冲**:`upscaleNearest` 回退为 **false**(插值 2×):最近邻会把颗粒变成 2 传感器像素、
|
||
边沿出现粗台阶,是可见的退化。csdk 的两个 demo 同步回退为 0,选项保留并写明测得的教训。
|
||
|
||
### 15.3 实测(同场景:我们 17:54 / 官方 17:56,翻转对齐 r=0.994)
|
||
|
||
| 指标 | 官方 | 我们(新) |
|
||
|---|---|---|
|
||
| 索引分位 p5/25/50/75/95 | 21 / 48 / 76 / 90 / 224 | 18 / 47 / 78 / 92 / 224 |
|
||
| 冷区 `\|dx\|` | 4.32 | **3.29(比值 0.76)** |
|
||
|
||
→ 修好后我们**比官方更平滑**(官方因最近邻 4× 反而有 4 像素大块,见 x8 放大对照图)。
|
||
单测 119 全绿(`PhotoNucMappingTest` 更新为 160×120→960×720 的新策略)。
|
||
|
||
## 16. 「官方背景条纹更不明显」——逆向排除全部候选后,定位为**我们独有的逐行固定偏移**(2026-10-06 深夜,交接轮)
|
||
|
||
用户发了同场景双截图(官方 vs 我们,均为竖屏、12 px/传感器像素、同一调色板铁虹)并要求
|
||
"看逆向出来的代码是不是还漏了什么"。本轮做了两件事:把官方每帧链路逐条再对一遍(IDA 批量
|
||
反编译),并对两台 App 的显示结果做**调色板弧长反演**的定量对比。**结论:每帧算法没有遗漏;
|
||
差异是一个官方没有、我们有的"逐传感器行固定偏移"(残余行 FPN,显示上就是水平条纹)。**
|
||
|
||
### 16.1 逆向再核对(libcxsdk = 普通版 MAG-Cx 的 SDK,全部读反编译原文)
|
||
|
||
- **内参来源定案**:`Controller::StartProcess(cb, w,h,sn,devType, close,open, ffcFrame,ffcTemp, palette, baseLineAcc, cali)`
|
||
只写 Controller+0x28(w)/+0x2c(h)/+0x18(palette)/+0x40(baseLineAcc),其余字段保持
|
||
`Controller::Controller()` 的默认:+0x44=**1**、+0x48=**1**、+0x4c=5。
|
||
映射到 CFunctions:0x3804=baseLineAcc=**4**、0x3808=**1**(温度累积器**旁路**)、
|
||
0x380c=**1**(**CSmoother 关**)、0x3810=5。→ 官方手机链路**没有**平滑器/累积器,
|
||
之前 §8/§9/§10 的结论不用改。
|
||
- **每帧链路** = `Calibration`(DDT 有效时走 `CalibrationMain_RTV`:
|
||
`d=(raw−base)>>1` → 按段阈值选 `(gain,off)` → `(d*gain>>12)+off`,clamp 0..0xffff)
|
||
+ `BlindCompensation`(盲元按 3..8 邻域均值替换,DDT 盲元表) + Mirror(关) +
|
||
`TempRangeStat` + `GrayEnlarge`(内含 `HistDeRedundance`)。E2X/E4X 仅当 EX≠0
|
||
(App imageEX=0),`FilterDetailEnhancement(_Simple)` 仅当 CFunctions+0x28≠0
|
||
(**从未被写**=0)。→ 与我们的实现一一对应,**没有缺的阶段**。
|
||
- **标定表只在 FFC 窗口重建**:`DoSomething(param_1==0)`→`SelectCaliCurve`+
|
||
`GenerateVolatileLens`;param_1≥1 时在窗口各帧**分块** `InterpolateShort/UShort`
|
||
(块大小按 `(13+总字节+尾-2)/(13-1)+7&~7`,完成后置 0x2d24=1)。不在每帧跑 →
|
||
我们"FFC 时 rebuildTables"一致。
|
||
- **FFC 时序逐帧一致**:触发→cb(0) 关快门 →帧 1..5→帧 6..9 逐帧 `PushBaselineRaw`
|
||
(首帧 Restart,4 帧块平均)→**第 9 帧回调 cb(1) 开快门**→帧 10..13 DoSomething→帧 14 起正常。
|
||
我们的状态机(idx 1..5 / 6..9 / idx==9 onFfc(1) / 10..13)完全同相。
|
||
- **设备配置命令**:`setFrameRate`(P2D_SetFrameRate=0x6BB6B679,载荷 interFrame/interLine
|
||
两个 u32;fpsMode 0→9/12,否则 105/105)只在 **devType 1/2** 时发;我们 devType=**3**
|
||
→ 官方也不发。`P2D_SetParameter1/2` 官方 USB 路径只发 Get。→ 原始流配置两边相同。
|
||
- **工具链记录**:普通版 APK=`app/【普通版】MAG-Cx.apk`(libcxsdk.so 372KB armeabi,
|
||
md5 4d8d98bb…,与 analysis/sdk_re/android_app/bin 相同;JNI 层是 libcxx2java.so 67KB)。
|
||
`analysis/ida/libcoresdk_arm64.so` 是**专业版 MAG-Mx** 的库,别拿它推普通版
|
||
(它的 0x439d80/88 内参在 .bss,文件里读不到)。IDA Pro 9.4 批量反编译命令:
|
||
`idat.exe -A -S"<script.idc>" -L"<log>" <so>`(脚本按 `get_next_func` 遍历;
|
||
对 `__imp__` 导入 thunk 会异常中断,注意跳过;Ghidra headless 兜底)。
|
||
- `test_frames.bin` 夹具是合成/重复帧(60 帧 mean/std 完全相同),**不能**用于噪声分析。
|
||
|
||
### 16.2 定量对比(调色板弧长反演,脚本在 %TEMP%\mag160c_re\analyzer.py,临时件)
|
||
|
||
方法:ironbow 256 色表(VendorPalettes)→ RGB 弧长参数化(消除调色板斜率影响)→
|
||
12 px/传感器像素块中值采样到 160×120 → 3×3 高通。数据:用户双截图(竖屏)+
|
||
真机连拍 3 帧(官方横屏 crop、我们竖屏,均 PNG 截屏无 JPEG 伪影)。
|
||
|
||
| 指标(弧长单位 ×1e-2) | 官方 | 我们 | 比值 |
|
||
|---|---|---|---|
|
||
| 整图低频 σ(对比度/音调曲线) | 20.2 | 21.2 | 1.05(一致) |
|
||
| scene(>8px) σ(真实场景对比度) | — | — | 0.95~0.97(一致) |
|
||
| 高频 σ(单帧 1~8px 频带) | 1.16~1.21 | 2.10~2.51 | **~2×** |
|
||
| 时间分量(连拍差分/√2) | 0.52 | 0.75 | 1.44× |
|
||
| **沿显示水平线均值的 std(=逐传感器行固定偏移)** | **0.106~0.15(max 0.57)** | **1.20~1.84(max 12.1)** | **8~17×** |
|
||
|
||
分块统计(背景块):scene 比 0.95、1-8px 频带比 1.58、时间比 2.0 —— 我们多出来的
|
||
能量集中在 1~8px 频带且**逐帧可复现(固定)**。
|
||
|
||
### 16.3 结论与交接(下一步)
|
||
|
||
- **缺陷**:我们的渲染里有一个官方没有的**逐传感器行固定偏移**(残余行 FPN)。
|
||
显示旋转 90° 后,传感器行=屏幕水平线 → 就是用户看到的"背景条纹"。
|
||
它是固定的(连拍均值里都在),所以不是每帧算法、也不是显示插值(两边显示链等价:
|
||
都是 160×120 数据 → 12 px/px 双线性上屏)。
|
||
- **候选(按嫌疑排序)**:
|
||
1. FFC 参考帧被污染/时序错位:官方与我们都"关 5 帧→采 4 帧→第 9 帧开",但我们的
|
||
帧计数从自己的处理循环来;USB 抖动/丢帧会让采集窗错相(§9.4 曾测得
|
||
"延到 8 帧反而变差"——设备实际快门相与我们假设的错位的旁证)。
|
||
参考≠纯快门帧 → 减出的残差里留下行结构。
|
||
2. DDT 段表 (gain,off) 的布局/插值在当前 FPA 温度下与我们不一致
|
||
(我们 `rebuildTables(shutterAtFfc + fpaTempFix)`;官方 `SelectCaliCurve` 的温度
|
||
来源、端点选择、Interpolate 的语义需要逐字段比对)。
|
||
3. 盲元处理差异(我们已实现 `nucAndBlind`,但要和官方 `BlindCompensation` 的
|
||
邻域定义逐条对——它用 10 int/条 的记录、邻居偏移含 ±1 与 ±行距)。
|
||
- **验证路径(下一个模型从这里接)**:
|
||
1. App 加"原始帧录制"开关(IrSession 读线程帧入口处写 files/,含一次完整 FFC 的
|
||
60+ 帧与帧头);真机录一段。
|
||
2. 离线重放:raw → 参考 → NUC → 表插值 → LUT,**每一级都算"沿行均值 std"**,
|
||
找出引入行偏移的那一级;再用录到的数据做 A/B(改窗口/改表温度/改盲元)复现官方数值。
|
||
3. 真机 A/B 验收指标就用 16.2 的表:目标"逐行固定偏移 std ≤ 0.2"。
|
||
|
||
## 17. 第三十八轮:原始帧录制 + 逐级重放全链路定案 + 显示链修复(2026-10-07 凌晨)
|
||
|
||
按 §16.3 清单执行。工具:`IrSession.rawFrameSink` + `RawFrameRecorder`(调试开关,
|
||
`files/rawcap.enable` 标志文件触发,录 raw+NUC dump 到 `files/rawcap/run_*.bin`,
|
||
带 FFC 触发,300 帧自动停);离线重放脚本在 `%TEMP%\mag160c_re\`(临时件)。
|
||
|
||
### 17.1 逐级重放(整数语义复刻,与真机 NUC dump 互锁)
|
||
|
||
录制 300 帧(含 3 次完整 FFC 窗口)。Python 逐级复刻(i32/算术移位/向零截断与
|
||
Kotlin 一致),与 App 自己 `copyNuc` 的 dump 对拍:corr=0.9996,仅盲元像素大差异
|
||
——**复刻可信**。各级行交替(alt = 行均值相邻二阶差,固定分量):
|
||
|
||
| 级 | 行交替 std(counts) |
|
||
|---|---|
|
||
| raw(场景帧固定行剖面) | 596.9 |
|
||
| ref(4 帧快门平均) | 596.7(与 raw 图样相关 0.999) |
|
||
| d2 = (raw−ref)>>1 | 5.20 |
|
||
| NUC(查表后,App dump) | 4.25 |
|
||
|
||
**结论 1:参考减除消掉 98.4% 的行 FPN,表再消一半,残差 4.25 counts 就是
|
||
显示条纹的直接来源**(换算到显示域 ≈ §16.2 的我们 1.2–1.84)。
|
||
|
||
### 17.2 排除清单(都做了 A/B)
|
||
|
||
- **参考窗相位**(±2 帧移动):4.01–4.33,不敏感。
|
||
- **参考平均长度**(1/4/6/8 帧):4.14–4.51,不敏感(时间噪声早已被平均压低)。
|
||
- **表插值温度**(33637…47664 扫描):4.06–4.57,不敏感。
|
||
- **残差时间结构**:FFC 后 1–10.3 s 固定分量 5.14–5.34,完全平坦 → 不是参考老化。
|
||
- **跨 FFC 窗口**:d2 残差图样相关 +0.986 → 不是每次 FFC 的随机量。
|
||
- **残差与 raw/ref 图样相关** ≈ 0.02/0.03 → 白状固定残差,**即"快门态与开放态的
|
||
行 offset FPN 图样差"(≈1.7%)**,设备固有。
|
||
- **GenerateVolatileLens**(0x18314):11 段温度响应折线,只进温度链(`this+3163`
|
||
默认 1 时直通),与图像链无关 → "官方把参考融合进表"的假设**不成立**。
|
||
- **官方 SDK 全函数核对**(本轮新读):`CalibrationMain_RTV`(段选择/`(d*gain>>12)+off`/
|
||
布局与步进逐字段一致)、`PushBaselineRaw`(双累积器 +14212/+14244)、
|
||
`CAccumulator::Reset/PushFrame/Restart`(mode=baseLineAcc=4 恰好 4 帧块平均)、
|
||
`Raw2Temperature`/`CFunctions::PushFrame`(0x3808=1 旁路、每帧渲染、FFC 条件同相)、
|
||
`TempRangeStat`(纯 min/max/mean)、`BlindCompensation`(typ 3..8 邻域 switch)、
|
||
`InterpolateShort/UShort`(Q12 端点插值同式)、`SelectCaliCurve`、`DoSomething`、
|
||
`Controller::PushFrame/StartProcess`(A=5/E=4/C=4 同相;PushBaselineRaw 收 JNI 原样
|
||
帧,Java `ThreadImgProcess` 不做预处理)。**官方每帧链路与我们逐字段一致,没有
|
||
任何"额外处理"能解释残差差**。
|
||
|
||
### 17.3 显示域 A/B(同场景同铁虹,§16.2 口径:12px 中值网格 + 弧长 + hf3 行/列剖面)
|
||
|
||
| 测量 | ROW-dir | COL-dir |
|
||
|---|---|---|
|
||
| 官方截图(横屏) | 0.12 | 0.31 |
|
||
| 我们修复前截图 | 1.41 | 0.46 |
|
||
| 我们修复后截图 | 1.32 | 0.59 |
|
||
| 我们修复后截图(**mask 掉 UI 覆盖层后**) | 0.76 | 0.67 |
|
||
|
||
**重要测量教训**:我们的竖屏 UI 把中心温度/MAX 标注/色标条画在热像区内,它们对
|
||
行剖面的贡献与真实 FPN 同量级——**对比测量必须 mask**(官方 UI 覆盖为 0)。
|
||
§16.2 的"8–17×"里含此混淆 + 几何错位(37 轮连拍的官方横屏 crop 与我们竖屏
|
||
行/列方向不一致);本轮修正后**真实 NUC 域差距约 1.6×**(同一份我们的数据走
|
||
官方显示链复刻 = 0.19,走我们链 = 0.32,官方截图实测 0.12)。
|
||
|
||
### 17.4 定案与修复(初版,17.6 有修正)
|
||
|
||
1. **官方显示链的两级结构**(jadx `ImageViewer` + 本轮重核):SDK 输出 160×120
|
||
bitmap → `PaintFlagsDrawFilter + FILTER_BITMAP` 画入 **640×480 离屏**(4× 双线性)
|
||
→ SurfaceView 再放大到屏。**中间 4× 双线性对逐行 FPN 图样有 ~0.6× 的低通**——
|
||
同一份我们的 NUC 数据:走官方两级链复刻 = 0.12,走我们单级 12× 复刻 = 0.32。
|
||
**"官方背景条纹更不明显"的主导机制是显示链,不是 NUC 域**。
|
||
2. **修复 A(nativeOut)**:`RenderPipeline` 去掉 PC 版遗留的内部 2× 插值级
|
||
(官方安卓没有),输出原生 160×120 调色板图;`LiveRenderer`/录像/拍照全链
|
||
适配。复刻证明它对行交替数值无影响,但消除了自造的"全幅/半幅"行调制
|
||
(§35 的竖列交错在行方向的同类项),显示等价性更完整。
|
||
3. **修复 B(640×480 中间级)**:`LiveRenderer` 按官方 viewer 补上 640×480 离屏
|
||
位图(FILTER_BITMAP 4×)再上屏,**显示链与官方逐级等价**。
|
||
4. **遗留(诚实记录)**:UI mask 后我们 0.76 vs 官方 0.12 仍有差距,其中
|
||
a) NUC 域残差 2.87 vs 官方等效 ~1.3–1.8 counts(**1.6×**,同显示链对比)——
|
||
与"快门态/开放态行 offset FPN 图样差"相关,设备固有,官方同算法也应承担,
|
||
差异来源未定位(疑似两 App 会话时设备状态不同);
|
||
b) 真机 GPU 显示链与 PIL 复刻间仍有 ~2× 未解释差(复刻 0.34 vs 真机 0.76),
|
||
需下一轮用"真机截图 vs 同帧 NUC dump 的逐像素对照"收敛。
|
||
§16.3 的验收目标"逐行 std ≤ 0.2"未达成;下一轮从 b) 项入手(它是纯测量
|
||
差异还是真机显示链仍有未建模环节)。
|
||
|
||
### 17.6 追加测量(同日,同帧对照把差距收敛到真实量级)
|
||
|
||
17.4 的"遗留 b"用**严格同帧对照**(run3/run4 录制 + 录制结束秒级截屏,App 自己
|
||
的 NUC dump 走我们显示链的 RGB 域复刻 vs 真机截图)解决:
|
||
|
||
- **真机截图网格与同帧 NUC 弧长网格的空间相关 = 0.994**——真机显示链上没有任何
|
||
未建模环节,17.4-b 的"2×"是三件事叠加的假象。
|
||
- **假象一(测量方法)**:PIL 的 F-mode resize 在本机对 float32 有严重失真
|
||
(round-trip 后剖面 corr≈0.0002),此前所有"F 域模拟显示链"的数字作废;
|
||
改 RGB uint8 域模拟后,所有几何变体(单级 12×、640 中间级 2.25×、官方 3×)
|
||
的行/列交替全部落在 0.07–0.11 / 0.24–0.28——**显示链几何不产生交替**。
|
||
- **假象二(UI 覆盖层)**:我们竖屏 UI 画在热像区内的中心温度/MAX 标注/色标条
|
||
对剖面贡献与 FPN 同量级,且 mask 边界会留下异常行。改用**鲁棒剖面**
|
||
(逐行 median + 2.5·MAD 剔除)后三方对照:
|
||
|
||
| 同口径(鲁棒,×1e-2 弧长) | ROW-dir | COL-dir |
|
||
|---|---|---|
|
||
| 官方截图 | **0.10** | **0.28** |
|
||
| 我们(修复前截图,320×240 内插) | 0.22 | 0.33 |
|
||
| 我们(640 中间级版截图) | 0.24 | 0.55 |
|
||
|
||
- **修复 B 回退**:640×480 中间级在**竖屏 fitRect 下第二级是 2.25× 非整数**
|
||
(官方横屏是 3× 整数),真机 GPU 渲染它使 COL 交替 0.33→0.55(RGB 域复刻
|
||
预测 0.28,未复现该伪影)→ **已回退**,保留 nativeOut 单级(160×120→
|
||
1440×1920 = 9×/16× 整数,无伪影)。
|
||
- **最终状态**:COL 方向我们 0.33 vs 官方 0.28(**1.2×,达标**);ROW 方向
|
||
0.22 vs 0.10(**2.2×**)。ROW 的 2.2× = NUC 域 ~1.6× + 显示/反演细节。
|
||
NUC 域残差与"快门态 vs 开放态行 offset FPN 图样差"相关(设备固有,官方同
|
||
算法同承担),同链对比下我们 2.87 counts vs 官方等效 ~1.3–1.8 counts 的
|
||
来源仍未定位——下一轮可用"反复重连 × 交替连接两个 App × 每次连接量 NUC
|
||
行交替"的统计实验判定是设备状态随机性还是系统性差异。
|
||
- §16.3 验收目标修正:以本轮建立的**鲁棒同口径**为准,COL 已达标(0.33 vs
|
||
0.28),ROW 2.2× 是下一轮的目标(0.22 → ≤0.2 需把 NUC 域残差压到官方
|
||
等效水平,先判定 1.6× 的来源)。
|
||
- **COL 波动的待验证项**:回退 640 级后的复测截图 COL=0.56 未回到 0.33,且
|
||
NUC 域 COL 本身跨会话波动(run2 6.91 → run3 8.0 counts)——COL 交替可能
|
||
随连接/温度状态漂移,判定需要"同一连接内录+截"的同帧对照(run4/run5 两次
|
||
尝试因屏幕保活与热像仪被拔中断,工具链已就绪:`rawcap.enable` + 保活 keyevent
|
||
循环 + `/data/local/tmp` 中转拉取 + RGB 域旋转正确的复刻脚本 `sync4.py`,
|
||
注意 sample_grid 前必须 `np.rot90(rgb, k=-1)` 对齐竖屏几何,PIL 的 F-mode
|
||
resize 在本机失真不可用)。
|
||
|
||
### 17.5 工具与现场
|
||
|
||
- `RawFrameRecorder`/`rawFrameSink`/flag 轮询已合入(调试工具,默认不活动)。
|
||
- `DebugLog.enabled` 本轮临时 true,提交前已改回 false。
|
||
- 现场状态:rotation 锁竖屏(原值 1,**待用户恢复**);我们 App 前台出流;
|
||
MIUI 的 App 自动启动会在授权后把焦点拉回我们 App,切官方测前先 force-stop
|
||
我们,用 `TAB×2+ENTER` 过 USB 弹窗(横屏时 input tap 坐标不可靠)。
|