28 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。
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 待办
- 同一步骤同步进
csdk(C 参考实现把该参数作为配置项/默认关闭,并更新其测试与文档)。 - 其它 11 个调色板索引与 Android 生成式的逐表核对(本轮已确认索引 2 一致)。
- 若要进一步逼近官方观感,可继续量: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 |
两点结论:
- 官方的灰度画面比我们平滑约 2.3 倍(0.34 vs 0.76)——这不是调色板/亮度能解释的;
- 官方的帧间噪声在收敛(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 下一步(精确)
- 参考帧收集改成快门感知:在 FFC(0) 之后逐帧判断"是否仍为闭合快门帧"
(可用帧统计/与前一帧的差异判断),把所有闭合帧都累积进参考(对齐官方
PushBaselineRaw的语义),FFC(1) 之后重置; - 用同一套灰度指标回归:目标空间 HF ≈0.34、帧间噪声 ≤4;
- 之后同步 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 本轮已改代码
RenderPipeline.androidLutOf():AndroidHistDeRedundance逐指令移植(新增,含测试);IrSession实时管线改用androidLut = true,删除 round-30 的cdfPivot75 = false临时开关;RenderPipeline.fpaTempFix(构造参数)+ffcStep/frameRemote调rebuildTables(shutter + fpaTempFix),IrSession从BasePara2传入(官方UsbCommunication同款语义)。
10.6 待办
- 真机 A/B(手机当前被 PIN 锁屏,需机主解锁):同一静止场景,官方实时 vs 我们实时,用"调色板反解灰度 + 逐像素对齐"复核 10.4 的 k/计数假设;
7×7 细节增强按官方默认打开—— 已排除:强度源CFunctions+0x28全库未写 = 0, 官方那一步是空操作(这也解释了为什么离线拟合"打开反而略差");我们的实现保持用户可选、 默认关。顺带:我们的DetailEnhance与反汇编有一处细节差(LocalMap7x7_Simple的 min/max 还要多采每行的第 1 列,共 5 列;我们只采了求和用的 4 列),会让门槛略偏严, 属可选功能的精度问题,不影响默认路径;- 同步 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 运维笔记(真机调试踩坑)
- 正在推流时 force-stop App 会把相机卡死(命令端点 halt、
GetCaliInfo超时、此后无流数据)。 恢复:su -c 'echo 0 > /sys/bus/usb/devices/2-1/authorized; sleep 2; echo 1 > ...'重新枚举; 切换 App 时先按 HOME 让 App 正常停流。 - MIUI 的 USB 授权在每次重装/重新枚举后都要重新授权;要勾"一律打开 MAG160C" 必须点复选框本体(x≈186),点标签中部(x≈718)不会勾上;授权后 App 需要重启进程才生效。
- 相机被卡住时 App 的统计会显示
reads=0 frames=0 timeouts=N,UI 以 ~3.3 fps 空转刷新 (不是渲染帧率)。