ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenReel Video 滤镜预设子系统:基于 LUT 的 60 款滤镜目录设计与配方驱动生成工具链

OpenReel Video 滤镜预设子系统:基于 LUT 的 60 款滤镜目录设计与配方驱动生成工具链 OpenReel Video 滤镜预设子系统基于 LUT 的 60 款滤镜目录设计与配方驱动生成工具链【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video导读本文围绕 OpenReel Video开源浏览器端视频编辑器README.md 所描述的开源 CapCut 替代方案中滤镜预设Filter Presets这一子系统的设计规格展开完整解读 Filter Presets v1 Design Spec。你将掌握如何用 YAML 配方 Python 代码生成 33³ 三维查找表LUT如何通过 Cloudflare R2 公共域以无 Worker方式交付 60 款滤镜如何在 iOS / Android 上实现 LUT 渲染、0–100% 强度混合与 CapCut 风格的滤镜选择器以及从既有 ~20 个参数化滤镜预设到 LUT 目录的无损迁移方案。背景与目标从参数化滤镜到 LUT 目录当前 OpenReel 的视频调色能力由约 20 个参数驱动的滤镜预设组成仓库中的实现位于 packages/core/src/video/filter-presets.ts以FilterPreset接口描述id、name、category、description、effects[]效果类型包括brightness、contrast、saturation、hue、blur、sharpen、vignette、grain等并作为能力清单通过 packages/core/src/capabilities/manifest.ts 的filterPresets字段对外声明。这类预设由参数实时计算跨平台渲染难以保证逐通道一致。Filter Presets v1 的目标是把它升级为经过策展、代码生成的约 60 款基于 LUT 的滤镜目录60 款滤镜分布在 6 个分类下无需发布应用即可上线纯静态内容下发选择器打开后暖缓存下首块贴图绘制 ≤ 250 ms强度滑杆在片段clip级别混合原始画面与滤镜输出跨平台渲染一致性iOS 与 Android 在相同(source, filter, intensity)三元组下输出差异 ≤ 每通道 1 LSB引用旧版filterPreset效果的存量用户项目透明迁移。明确的非目标Non-Goals设计文档划定了严格边界避免范围蔓延晕影vignette、颗粒grain、模糊blur、漏光light leaks等无法表达为 3D LUT的程序化效果归入独立的程序化特效规格AR/美颜、瘦身、化妆、面部贴纸等效果用户导入.cube文件推迟到 v2收藏Favorites交互v1 只有 Recents项目级全局观感v1 仅支持片段级付费/Pro 滤镜层级本设计不含 entitlement 系统。架构总览三块独立可测的组件设计采用三层架构每层边界清晰、可独立测试┌──────────────────────────┐ ┌────────────────────────────┐ │ Build-time tool │ │ Cloudflare R2 (public) │ │ ───────────────── │ push │ ────────────────── │ │ scripts/filters/ │ ─────▶ │ R2 bucket: │ │ - recipes/*.yaml │ │ openreel-filters/ │ │ - generate.py │ │ manifest.json │ │ - manifest writer │ │ cube/id.cube │ │ │ │ Custom domain: │ │ Outputs: │ │ filters.openreel.video │ │ - 60 × .cube │ │ /manifest.json │ │ - manifest.json │ │ /cube/id.cube │ │ │ │ (R2 native ETag cache) │ └──────────────────────────┘ └────────────┬───────────────┘ │ ▼ ┌────────────────────────────────────────────────┐ │ Mobile (iOS Swift Android Kotlin) │ │ ────────────────────────────────────── │ │ • FilterCatalogService │ │ • FilterLutCache │ │ • FilterRenderer │ │ • FilterPickerViewModel view │ │ • Clip model: filterId? intensity │ └────────────────────────────────────────────────┘三大边界工具边界输入为recipes/*.yaml输出为.cube manifest。这是一个纯函数配套 golden-file 测试托管边界R2 公共桶 Cloudflare 自定义域ETag 与不可变缓存直接来自 R2路径上无 Worker移动端边界两个平台各实现四个类表面接口完全一致每个类只负责一件事。与现有代码的插槽关系iOS 的Core/Effects/CubeLUTParser.swift与Core/Rendering/VideoEffectRenderer.swift已能解析.cube并运行 CIFilter 链Android 的core/effects/CubeLUTParser.kt与core/effects/ClipEffectPipeline.kt是对等物。滤镜交付属于静态内容负载直接由 R2 服务仓库中的apps/cloudWorker 保持不动它单独处理模板/分享/AI且按仓库约定被 gitignore。Recipe 工具链YAML 配方驱动的 LUT 工厂目录结构设计文档规划的工具目录位于仓库根下的scripts/filters/当前仓库中已实际落地scripts/filters/ ├── generate.py # 主工具入口 ├── transforms.py # 单个颜色算子逐像素 RGB→RGB ├── recipe.py # YAML 配方加载与 step 注册表 ├── lut.py # 33³ 恒等 LUT、向量化变换、.cube 写出 ├── manifest.py # manifest 条目构建 JSON Schema 校验 ├── manifest_schema.json ├── deploy.sh # 上传 out/ 到 R2wrangler ├── requirements.txt ├── recipes/ │ └── cinematic/ │ └── teal_orange.yaml # 首个英雄配方Teal Orange ├── tests/ │ ├── fixtures/sample.yaml sample.cube (golden) │ ├── test_generate.py / test_lut.py / test_recipe_loader.py / test_transforms.py └── out/ # 生成产物.gitignored ├── cube/ # 60 × .cube 文件 └── manifest.jsonRecipe 格式每个滤镜一个文件设计文档给出的配方示例如下仓库中的真实文件 scripts/filters/recipes/cinematic/teal_orange.yaml 与之一致id: cinematic.teal_orange name: Teal Orange category: cinematic accent: #38BDF8 sort: 10 steps: - temperature: -8 - tint: 3 - contrast: curve: s_curve amount: 1.15 - split_tone: shadows: #1E3A5F highlights: #FFA94D balance: 0.0 - saturation: 1.10 - hue_shift: reds: -5每个字段的含义字段说明id全局唯一标识如cinematic.teal_orange同时作为.cube文件名与 manifest 索引键name面向用户的显示名如 Teal Orangecategory所属分类cinematic/portrait/vlog/retro/mood/bwaccent选择器中选中状态的主题色十六进制如#38BDF8sort分类内排序权重数值越小越靠前steps按序执行的调色步骤列表每一步都必须是逐像素 RGB→RGB 变换才能折叠进 3D LUTv1 支持的 steps 全集temperature、tint、exposure、contrastlinear/gamma/s_curve三种曲线、saturation、vibrance、hue_shift按通道或全局、split_tone阴影 / 高光 / balance、lift_gamma_gain、channel_mixer3×3 RGB 矩阵、tone_curve控制点、clip黑白电平、monochrome带通道权重。v1 明确排除vignette、grain、blur、light_leak—— 它们不是 LUT 可表达的推迟到程序化特效子系统。源码中 scripts/filters/recipe.py 的STEP_REGISTRY把 YAML step 名映射到 scripts/filters/transforms.py 中的具体函数例如temperature→apply_temperatureamount/100折算为 RGB 偏移、exposure→apply_exposureimage * 2^stops、contrast的s_curve用tanh实现软 S 曲线、saturation按 Rec.709 亮度系数[0.2126, 0.7152, 0.0722]加权。未知 step 会在加载时直接报错对应测试 test_recipe_loader.py 的test_load_recipe_rejects_unknown_step。生成器算法设计文档定义的四步算法在仓库实现中逐一对应构建 33³ 恒等 LUT—— scripts/filters/lut.py 的identity_lut()用np.linspace(0, 1, 33)在 R/G/B 三个轴上做 meshgrid 生成 33×33×33×3 的 float32 数组LUT_SIZE 33与设计一致对 LUT 采样点向量化应用每个 step——apply_transforms_to_lut()把 LUT 重塑为(-1, 1, 3)后逐函数作用设计文档估计每张 LUT 约 5 ms以 Adobe 标准格式写出.cube——write_cube()依次输出TITLE、DOMAIN_MIN 0 0 0、DOMAIN_MAX 1 1 1、LUT_3D_SIZE 33及按 B→G→R 嵌套序排列的r g b三通道浮点值%.6f兼容 iOS 与 Android 两个既有解析器把元信息追加进 manifest—— scripts/filters/manifest.py 的build_manifest_entry()计算.cube的sha256与字节数连同id/name/category/accent/sort/cubeUrl构成条目。分类目录v16 × ~10CategoryFilters代表性示例最终清单在 Phase 2 锁定CinematicTeal Orange, Blockbuster, Movie, Noir, Hollywood, Drama, Bleach BypassPortraitSoft, Warm, Golden, Porcelain, NaturalVlogCrisp, Vibrant, Punchy, Soft PopRetro70s, 80s, Polaroid, VHS, Sepia, Faded Film, Old PhotoMoodDreamy, Moody, Golden Hour, Cold Blue, Stormy, Soft MistBWClassic, High Contrast, Matte, Faded, Soft Mono, Gritty构建与部署本地生成scripts/filters/README.md 给出标准流程python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python generate.py # 输出到 out/cube/*.cube 与 out/manifest.json pytest tests/ -v # 运行工具层测试generate.py支持--recipes、--out、--base-url、--version参数并按 6 分类的有序字典生成categories段见 generate.py。部署由 scripts/filters/deploy.sh 完成对.cube使用public, max-age31536000, immutable缓存头对manifest.json使用较短的public, max-age300, s-maxage3600以便客户端快速发现新滤镜./deploy.sh # 等价于 aws s3 sync out/ s3://openreel-filters/ 的 wrangler 版本Hosting DeliveryR2 公共桶与无 Worker 静态交付R2 桶结构独立桶openreel-filters与openreel-templates、openreel-shares分离便于生命周期与权限管理openreel-filters/ ├── manifest.json └── cube/ ├── cinematic.teal_orange.cube ├── cinematic.blockbuster.cube └── ... (60 files)Manifest 结构{ version: 2026-05-22T1, minClientVersion: 1.0.0, filters: [ { id: cinematic.teal_orange, name: Teal Orange, category: cinematic, accent: #38BDF8, sort: 10, cubeUrl: https://filters.openreel.video/cube/cinematic.teal_orange.cube, sha256: abc123..., bytes: 154832, oldIds: [] } ], categories: [ { id: cinematic, name: Cinematic, sort: 1 }, { id: portrait, name: Portrait, sort: 2 }, { id: vlog, name: Vlog, sort: 3 }, { id: retro, name: Retro, sort: 4 }, { id: mood, name: Mood, sort: 5 }, { id: bw, name: BW, sort: 6 } ] }关键设计点每个滤镜的sha256与bytes支持下载后完整性校验与跨 manifest 版本复用决策仓库中manifest.py已实现 sha256 计算test_recipe_loader.py 的test_build_manifest_entry_includes_sha_and_bytes对此有断言oldIds数组允许客户端在滤镜改名时透明重映射避免用户片段悬空manifest 在写出前会通过 scripts/filters/manifest.py 用 manifest_schema.json 做jsonschema.validateCI 中亦有 schema 校验。公共交付与缓存策略GET https://filters.openreel.video/manifest.json → Cache-Control: public, max-age300, s-maxage3600 GET https://filters.openreel.video/cube/id.cube → Cache-Control: public, max-age31536000, immutableR2 原生提供 ETag 与 304 语义上传时通过wrangler r2 object put --cache-control设置缓存头桶上一次性配置 CORSPUT/GET/HEAD、*源Web 编辑器可直接跨域拉取。manifest 中的cubeUrl使用绝对 URL未来即使回迁 Worker 或更换宿主也不需要客户端发版。版本化与缓存失效version字段采用 ISO 时间戳 计数器如2026-05-22T1客户端保存上次见到的 version在启动与选择器打开时以If-None-Match重取 manifest304 → 用缓存200 → 执行 reconcileLUT URL 按 id 稳定不变缓存层校验sha256配方一旦变化自动使下游缓存失效。客户端预取策略应用启动后台低优先级拉取 manifest选择器打开先下载可见区域的 LUT约 10 个其余后台续拉Wi-Fi预取全部 60 个约 9 MB蜂窝网络仅按需加载可在设置中配置LRU 磁盘缓存上限 50 MB。移动端数据模型、缓存与渲染集成片段数据模型data class AppliedFilter( val id: String, // matches manifest.filters[].id val intensity: Float, // 0.0 .. 1.0 ) // clip.filter: AppliedFilter? null存储在与现有effects: []相邻的位置。存量项目反序列化时filter null行为与今天完全一致。四个类双平台相同表面FilterCatalogService singleton, reactive state: StateFlowFilterCatalog ← .loading | .ready(filters, categories) | .error refresh() async ← fetch manifest, reconcile against on-disk snapshot Persists last good manifest so the picker is never empty after first successful launch. FilterLutCache fun get(id: String): LutData? ← memory hit suspend fun fetch(id: String): LutData ← disk hit → network fetch → sha verify fun prefetch(ids: ListString) ← background, queued, cancellable LRU 50MB on disk. Parsed LutData (33³ float array) memoized in memory. FilterRenderer fun apply(image, filterId, intensity): image Looks up LUT via FilterLutCache.get (synchronous, must already be cached). If not cached: returns source unmodified and signals caller to await fetch. Mix: out lerp(srcPixel, lutSample(srcPixel), intensity). FilterPickerViewModel state: StateFlow{ categories, filters, selectedId, intensity, thumbnails } onSelect(id), onIntensityChange(value), onCategoryChange(catId)边界收得很紧渲染器不知道 HTTP目录服务不知道 Metal/GL缓存不知道 UI。iOS 集成插槽于Core/Rendering/VideoEffectRenderer.swiftFilterCatalogService是actorUI 通过MainActor快照包装器绑定FilterLutCache.fetch使用URLSession.downloadTask文件落到Caches/openreel-filters/{id}.cubeLUT 变成名为CIColorCube的CIFilterinputCubeDimension: 33、inputCubeData: Data强度用CIBlendWithMask源 CIImage 与 LUT 应用后的 CIImage 之间用一张 alpha intensity 的纯色蒙版混合单个 CIFilter、GPU 融合执行。Android 集成插槽于core/effects/ClipEffectPipeline.ktFilterCatalogService暴露StateFlowFilterCatalog缓存使用 OkHttp withContext(Dispatchers.IO)渲染器添加一个 Media3GlEffect把 33³ LUT 上传为GL_TEXTURE_3D在 fragment shader 中逐像素采样intensity 作为 uniformGLSL 中执行mix(src, lut, intensity)。片段渲染链顺序双平台锁定source → LUT (filter intensity) → user color adjustments → spatial effects → outputLUT 排在最前意味着用户后续的颜色调整可预测地叠加在滤镜之上这是 CapCut 的行为顺序颠倒会让调整在不同滤镜间表现不一致。Filter Picker UXCapCut 风格选择器入口选中视频片段时上下文工具栏上现有的 Filter 入口保持不变仅替换内部面板。布局双平台一致顶部为实时预览区其下是强度滑杆带百分比、Reset/Apply按钮再往下是分类 TabRecent / Cinematic / Portrait / Vlog / Retro / Mood / BW底部为横向滚动的滤镜贴图行None始终是最左侧贴图。交互细节点选滤镜 → 立即以 100% 强度应用选中贴图显示对勾 按 manifestaccent着色的圆环再次点选同一滤镜 → 关闭回到 None拖动强度 → 实时预览松手前不提交选中 None 时隐藏滑杆Reset→ 强度回到 100%滤镜保持选中Apply→ 提交并关闭不点 Apply 直接关闭同样会提交CapCut 行为用户有 UndoRecents Tab记录跨项目的最近 12 个使用项持久化在 user defaults / DataStore长按贴图 → 应用到全部片段作为单一可撤销动作无障碍VoiceOver / TalkBack 播报滤镜名, 分类, 选中 | 未选中强度滑杆步进 5%选中态用边框 对勾而非仅靠颜色区分色盲安全。缩略图渲染管线选择器打开 → 以低分辨率抓取当前预览帧竖屏 144×256、横屏 256×144iOS 从MetalVideoView.lastFrame.pixelBuffer取Android 从最新的 ExoPlayer surface texture 取快照在内存中是单一 CIImage / Bitmap对每个可见贴图初始约 8 个在后台队列运行FilterRenderer.apply(snapshot, filterId, 1.0)用户滚动时入队新贴图、取消视口外任务以(filterID, snapshotHash)为键缓存[filterID → thumbnail]播放头移动超过 1 s 时失效。单贴图状态机unknown → pending-lut → ready → rendered \─────────────▶ failed (retries on tap)pending-lut与failed贴图渲染占位符accent 色 滤镜名仍可点按点按会提升下载优先级。空态 / 失败 / 离线副本从未加载到 manifest空态 重试按钮离线且无任何缓存同上离线但有部分缓存显示已缓存滤镜 横幅提示 More filters available when youre online。错误处理与边界情况项目引用了 manifest 中已不存在的滤镜若 LUT 仍在本地缓存片段正常渲染选择器在 Unavailable 区以灰色显示该滤镜、禁止新应用若 manifest 与缓存均无则以intensity 0渲染并在检查器提示一行警告Schema 漂移manifest 解码器忽略未知字段新增必填字段由minClientVersion门控低于该版本的客户端隐藏受影响滤镜并提示升级磁盘上 LUT 损坏读取时校验sha256不匹配则删除 重取一次二次不匹配贴图进入failed磁盘满ENOSPC时缓存驱逐最旧的 5 条并重试仍失败则仅内存保留 LUT 供当前会话使用并弹一次性 Storage full 提示下载中途应用进入后台可断点续传iOS 用 URLSession background 配置Android 用 OkHttpRange:恢复后选择器随下载完成刷新贴图两个片段并发打开选择器目录与缓存均为单例同一filterId返回同一份 memoizedLutData不会重复解析或下载旧filterPreset效果迁移generate.py首次运行会把现有 20 个参数预设烘焙成.cube项目加载迁移在AppState中把effects: [filterPreset(...)]重写为clip.filter: { id, intensity }旧渲染路径保留一个应用版本作为回退随后移除两个片段使用同一滤镜共享同一份内存中的LutData滤镜 id 改名manifest 的oldIds数组在 reconcile 时透明重映射manifest 服务端 5xx使用磁盘上的 last-known-good manifest首次成功启动后选择器永远不会为空检查时点 vs 渲染时点长导出开始时拍摄LutData快照导出中途 manifest 变化不影响本次导出隐私 / 遥测滤镜请求不携带 PII、不做按用户标记未来可选聚合 filter applied 事件同样无 PII。测试策略与性能预算工具层scripts/filters/每个变换算子有单测已知 RGB 三元组上容差 ±1 LSB仓库中 test_transforms.py 覆盖temperature、tint、contrast、saturation等核心算子Golden-file 测试fixture 配方 → 期望.cube重新生成不一致即报 difftest_generate.py 的test_sample_generates_expected_cubemanifest 在 CI 中按 JSON Schema 校验test_recipe_loader.py 的test_write_manifest_validates_against_schema每个配方冒烟测试可加载、可生成、可确定性渲染固定色卡 PNG提交的期望 PNG 作为视觉回归面。托管层R2 公共桶部署后 curl 冒烟curl -sI https://filters.openreel.video/manifest.json应返回 200 ETag 正确Cache-Control携带If-None-Match: etag重复请求应得 304对.cubeURL 同样验证immutable头。移动端服务层无 UIFilterCatalogService用 fixture manifest 做 reconcile 测试首次拉取、无变化重取、增/删/改/改名、未知字段容忍FilterLutCache内存 磁盘命中、未命中 → 拉取 → sha 校验、不匹配重试后失败、上限驱逐、并发拉取返回共享结果FilterRendererintensity 0/50/100 的 golden-image 快照测试容差 ≤ 1 LSB / 通道。跨平台一致性Phase 1 起为 CI 门禁同一组(sample.png, filterId, intensity)三元组分别通过 iOS 与 Android 渲染器输出 PNG 逐通道比对任一处偏差 1 LSB 即 CI 失败。性能预算选择器打开 → 首块贴图绘制≤ 250 ms中端设备、暖缓存CI 强制LUT 解析 GPU 上传≤ 20 ms / 滤镜profiled 而非门禁1080p 每帧 LUT 通道≤ 1 msprofiled 而非门禁。明确排除端到端模拟器测试不稳定、慢、网络混沌模糊测试失败面已被 mock 单测覆盖。实施阶段与风险登记Rollout PhasesPhase 0 — 基础约 1.5 天无用户可见变化创建 R2 桶openreel-filters 自定义域filters.openreel.video搭建scripts/filters/generate.py、transforms、schema、golden fixtures、1 个英雄配方 Teal Orange用wrangler r2 object put --cache-control部署并 curl 验证CI 接入工具单测。退出门禁manifest 与 Teal Orange.cube在filters.openreel.video上以正确 ETag Cache-Control 上线Phase 1 — 单滤镜端到端约 4 天Teal Orange 贯通双平台移动管线iOS 四类接入VideoEffectRenderer、选择器替换旧滤镜面板Android 同样四类接入ClipEffectPipeline选择器显示 1 个滤镜 None强度可用Apply 提交跨平台一致性测试通过。退出门禁TestFlight Android 内部轨道对真实视频应用滤镜且肉眼一致Phase 2 — 目录规模化约 5 天可并行编写剩余约 55 个配方把现有 20 个FilterPresetCatalog参数预设烘焙进 recipes/LUT60 个全部过generate.py并部署 R260 个全部跑跨平台一致性每个分类对真实素材做内部 QA。退出门禁60 款滤镜上线、一致性全绿、QA 签字Phase 3 — 迁移与发布约 2 天AppState项目加载迁移旧VideoEffectType.filterPreset渲染路径保留一个版本后移除提交 App Store Play Store。退出门禁带新选择器的公开版本Phase 4 — 内容迭代每次零工程投入新滤镜 写配方 →generate.py→ 同步 R2客户端下次启动自动生效可选同一观感的双 manifest 条目 A/B可选无 PII 的聚合分析。风险登记配方质量最大风险代码生成的滤镜不经肉眼调校容易数学味过重。缓解Phase 2 对每个分类的真实素材做 QA快速迭代闭环改 YAML →generate.py→ 上传 → 客户端即时生效跨平台颜色漂移CIColorCube与 GLES 3D 纹理采样之间的差异。缓解Phase 1 起一致性测试作为 CI 门禁R2 成本 / 热路径60 个 LUT × 多设备并非真实规模问题但Cache-Control: immutable是必须的确保流量命中边缘缓存而非回源桶。工作量估算单人端到端约 2 周两人各自负责一个平台Phase 0 后并行约 1 周另加 Phase 2 中 1–2 天的调色品味决策时间。小结Filter Presets v1 是一个把调色能力产品化的完整范本以YAML 配方 NumPy 向量化生成 33³ LUT的方式把滤镜从代码中的参数预设抽离为可版本化、可热更新、可跨平台一致的静态资产借助R2 公共桶 不可变缓存做到无需发版即可上新滤镜并通过单例目录服务 LRU 缓存 sha256 完整性校验 四类组件分离在移动端保证首屏 250 ms 以内的体验与渲染一致性。仓库中 scripts/filters/ 目录已经落地了配方加载、变换算子、LUT 写出、manifest 校验与 R2 部署脚本等核心实现配合 docs/superpowers/plans/2026-05-22-filter-presets.md 的实施计划可作为后续开发或复刻此类云端驱动滤镜目录架构的直接参考。【免费下载链接】openreel-videoOpenReel Video - Professional browser-based video editor. Open source CapCut alternative. 100% browser-based, no installation, no cloud uploads, no watermarks.项目地址: https://gitcode.com/GitHub_Trending/op/openreel-video创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进