从 View 到 Miuix Compose:来咔!的界面重构与离线色彩管线
接上一篇《来咔!把一整间暗房装进一台离线手机》,这篇拆「来咔!」的工程骨头:一条从导入到出片的离线色彩管线,以及把它从 View/XML 搬到 Miuix Compose 的过程。所有结论都来自对源码的实读,不臆测。 项目来源与署名 :本文所涉「来咔!(leicaMoments)」是基于开源项目 L…

接上一篇《来咔!把一整间暗房装进一台离线手机》,这篇拆「来咔!」的工程骨头:一条从导入到出片的离线色彩管线,以及把它从 View/XML 搬到 Miuix Compose 的过程。所有结论都来自对源码的实读,不臆测。
项目来源与署名:本文所涉「来咔!(leicaMoments)」是基于开源项目 L-Moment 的二次开发版本。原项目由 GitHub 用户 昔白(@a307608689-afk) 创建,采用 MIT 许可证;本站的「来咔!」在其基础上完成了 Miuix 风格化改造以及色彩管线、构建工具链等方面的优化。原作者保留全部权利,本衍生版本不开源,亦不提供源码或构建产物。
数据流总览
flowchart TD
A[相册 / 分享 Intent] --> B[读取原始字节]
B --> C{是否 RAW?}
C -- 是 --> D[RawDecoder JNI / LibRaw]
C -- 否 --> E[ImageDecoder / BitmapFactory]
D --> F[工作 JPEG + 预览 Bitmap]
E --> F
F --> G[LeicaEngine 内置风格]
F --> H[CustomStyleStore / LUT / ORT]
G --> I[调节链]
H --> I
I --> J{导出档位}
J --> K[工作版 JPEG]
J --> L[全像素条带 / 24MP 整图]
K --> M[EXIF / 徽章 / HDR 装配]
L --> M
M --> N[MediaStore 保存]
导入与工作图归一
入口 onImagePicked 的处理顺序值得单独说,因为它定下了后面所有环节的边界:
- 读原字节;Android 10+ 走
MediaStore.setRequireOriginal保留 GPS。 - 按扩展名识别 RAW、按 MIME 与 JPEG 魔数识别 JPEG;动态照片截断到主 JPEG,丢掉 EOI 之后的视频数据。
- 重编码前提取 Ultra HDR 增益图。
- RAW 走 LibRaw JNI,普通图走
ImageDecoder/BitmapFactory。 - 工作图压到引擎可接受的横置口径:长边 ≤ 4096、短边 ≤ 3072。
- 归一 EXIF 方向,落一份工作 JPEG、预览 Bitmap、全像素源与 HDR 信息。
这里最重要的工程约束是:预览图和导出图不是一个对象。预览只服务屏幕,导出必须用工作字节或全像素源。工作图还有硬上限——超过上限的原始 JPG 必须先缩放,否则某些厂商风格链会直接原生崩溃。
内置风格与降级链
LeicaEngine.process 是内置风格的统一入口,但不同风格不是同一套算法:
| 风格 | 处理路径 |
|---|---|
| M3 | 完整链 + 黑白后处理,失败退旧链 + CPU 黑白兜底 |
| M9 | 厂商完整链 → QNN CPU 补丁 → 蒸馏微网 → ORT 重链 → 纯 LUT |
| M9+ / M10R | 在 M9/M10R 链基础上做颜色后推、暗角与颗粒 |
| ARRI / F / M4 / LP9 | 主要是 3D LUT 查表,再叠颗粒 / 暗角或局部算子 |
| CCD 家族 | SepLUT 与档位参数,共用 ONNX session |
| M9D | 蒸馏微网优先,再退 ORT 或 LUT |
| LM9 | 统计式链优先,再试学习式微网,最后回退 M9 正常链 |
所以 UI 不能把风格当成"一个枚举 + 一张缩略图"。它至少要能表达:正常完成、降级完成、缺资产输出原图、等待重链、失败但保留上一张结果、以及"当前风格与超分 / 完美成片互斥"。
自定义色彩与模型色彩
CustomStyleStore 存两类色彩:QUICK(少量参考图生成的"我的色彩")和 MODEL(参考图更多、带版本与候选簇的"模型色彩")。关键在 CustomStyleDef 与 StyleVersion,一个版本要记录 LUT 文件、seed 池前缀长度、theta 快照、参数文件、模型仿射参数 aff(12)、曲线节点 knots(27)、来源与强度。
模型色彩有两条渲染路线:
- 旧 LUT / 参数路线:LUT 缺失时用 theta 快照重烘,渲染主要调
ModelColor与AlexaLut。 - M7 两段网络 + aff/knots 路线:
m7_stage1.onnx从参考图提取统计指纹,m7_stage2.onnx聚合出风格仿射与曲线节点,运行时在 native/Kotlin 里套aff + knots;多风格簇时先出 640px 候选预览供挑选。
因此"版本选择"和"候选选择"是两个必须保留的状态,而不是只留一个 selectedStyleId。
调节链
八个离散档位,每档 -10..+10:亮度、曝光、高光、阴影、黑色阶、饱和度、对比度、柔光 / 清晰化。处理不是"每个滑杆都重跑整条引擎":先拿滤镜结果,再用 Shadows.applyStackToJpeg 把多档参数一次烘焙;RAW 提亮档且存在 16-bit 线性基准时,先在 16-bit 域重放再送颜色引擎。交互上必须把拖动中的草稿值和提交后触发渲染的值分开,否则每动一下手指就是一次解码 / 编码。
导出链
export() 按源图、风格与内存预算选路:能直通时保留原始像素;LUT / 模型走条带处理(LutBandOps / ModelNetBandOps / DistillBandOps);M9/M9D 高分辨率改走约 24MP 整图链,避开条带边界归一化伪影;失败回退工作版,绝不产出"尺寸正确但没套色"的成片。最后统一处理 EXIF、徕卡兼容元数据、HDR 增益图并写回 MediaStore。导出 / 超分放在前台服务里,UI 只观察状态——Compose 页面退出不能顺带取消已经开始保存的长任务。
分享码:LMC1
ColorShareCode 当前格式 LMC1、版本 2:
MODE_RECIPE:带参考图指纹和版本链,接收端可重烘焙。MODE_LUT:带 17³ 量化 LUT,兼容性更强。- 轻量文本码用差分 + gzip + Base64Url 压缩;文件备份
.lmc则携带完整元数据与版本链。
这是兼容协议:字段顺序、魔数、CRC、版本语义都不能在重构里动,搬家之前先补往返回归测试。
为什么搬到 Miuix Compose
承接自原项目 L-Moment 的早期实现:旧版是 AppCompatActivity + setContentView + 一堆 LinearLayout/SeekBar 的多 Activity 结构,继续在 Activity 里直接管几十个 View 字段;「来咔!」在保留这层已验证的业务真源(JNI/ONNX 调用、调节与导出逻辑)的同时,把可见界面整体迁到 Miuix Compose,迁移后不会同时存在 View 与 Compose 两套真源。迁移思路是先拆状态、再做视觉,核心组件映射:
| 原功能 | Miuix 目标 |
|---|---|
| 顶栏 + 导入 / 导出 | SmallTopAppBar + IconButton |
| 滤镜卡片 | LazyRow + Card |
| 调节面板 | OverlayBottomSheet + SliderPreference |
| 设置页 | SmallTitle + Card + Preference 家族 |
| 模型库 / 详情 | LazyVerticalGrid + Card / 分组 Card |
| 画质进度 | LinearProgressIndicator + 前台通知 |
主题用语义 token(MiuixTheme.colorScheme.*),不再散落硬编码色值;模糊效果 miuix-blur 默认不加,Android 12 及更早走非模糊回退。
工具链这道坎
Miuix 0.9.4 的 AAR 元数据要求 AGP ≥ 9.1.0 且 compileSdk 37,原项目的 AGP 8.2.2 只能用于纯 View/XML 版,无法与当前 Miuix 依赖共存。所以工具链整体前移:AGP 9.4.1、Kotlin / Compose 2.4.20、compileSdk 37,同时把 targetSdk 保持与原业务语义一致的 34。NDK、CMake、ABI 与 ONNX Runtime 全程不动。
风险与规避
| 风险 | 规避 |
|---|---|
| 工具链断裂 | 先做独立兼容分支,锁定版本再合 |
| UI 重写破坏算法 | 保留 RenderGateway 接口与 golden tests |
| 长任务被回收 | 导出 / 超分留 App/Service 层,UI 只观察 |
| Bitmap 泄漏 | 大对象进 ImageSession,版本号与释放集中管理 |
| 滑动卡顿 | 草稿值 / 提交值分离 |
| 分享协议不兼容 | 先测试后移动,保持格式版本 |
落地与后续
当前采用增量接入:保留 MainActivity 里已验证的业务状态与 JNI/ONNX 调用,用 Miuix Compose 重写可见界面,通过 AndroidView 桥接复用原有预览与调节面板,滑动提交、曝光重算与 Bitmap 生命周期不变;Debug 包加 .miuixdev 后缀,与正式包并存互不覆盖。
后续不应为了"纯 Compose"去搬大对象,而是在已有行为基线之上,逐步引入不可序列化的 ImageSession、显式的 RenderGateway 与更细的 ViewModel。慢一点,但每一步都可回滚、可验证。
声明
本项目(「来咔!/ leicaMoments」)是基于 L-Moment(作者:昔白 @a307608689-afk,MIT 许可证)的二次开发衍生版本,在其基础上进行了 Miuix 风格化改造及色彩管线、构建工具链等优化;原项目的著作权归原作者所有。本衍生版本为个人学习与实验项目,与 Leica、小米及其关联公司无任何隶属、授权或背书关系;依 MIT 许可证要求,衍生使用须保留原作者署名与许可声明。暂不开源,也不分发二进制构建产物,仅供个人学习研究。