用ONNX和Worker实现浏览器本地图像AI
Lightframes将抠图、超分辨率和物体移除模型放入Web Worker,通过ONNX Runtime Web在本地处理像素,无需上传照片。文章给出模型注册与资源路径配置,编辑器采用MIT许可证。
AI 深度解读
浏览器里的 AI 修图,已经能把抠图、放大和物体移除留在用户设备上完成。Lightframes 给出的方案是:用 ONNX Runtime Web 加载模型,把像素交给 Web Worker,在本地生成结果,网络只负责下载模型文件。
这篇值得读的地方,是它把落地细节和限制都摊开了:模型如何缓存、GPU 不可用时怎么办、大图怎样切块,以及「照片不上传」为什么不等于「首次使用不联网」。对准备给前端加入图像 AI 的开发者,这些比功能演示更有参考价值。
照片留在本地,模型仍然需要下载
Lightframes 使用 ONNX Runtime Web 1.30.0。抠图加载 IS-Net,放大加载 Real-ESRGAN general x4v3,物体移除加载 MI-GAN。RAW 解码之后,也会进入同一套设备端处理流程。
模型按功能预加载:打开 Background 或主体蒙版时加载 segment,绘制 Remove 笔画时加载 inpaint,在导出框选择 2× 或 4× 时加载 upscale。
两个站内文件分别是 /models/isnet-general-use-int8.onnx 和 /models/realesr-general-x4v3.onnx,仓库中的大小为 45,314,968 字节和 4,994,683 字节。MI-GAN 从 Hugging Face 上固定版本的文件下载,仓库没有保存它,文章也没有提供大小。
loadModel 使用名为 lightframes-models 的 Cache Storage。命中缓存后,仍会通过 crypto.subtle.digest('SHA-256') 校验内容,并与 src/lib/aiModels.ts 中 MODELS 登记的摘要比较。不匹配就删除缓存;新下载同样要校验,失败则报 Model download is corrupted。
下载过程会报告 loaded 和 total 字节数。如果 caches.open 失败,刚下载的模型仍可用于推理,只是无法保存。
照片不上传,是数据流的设计;离线可用,则依赖模型已经成功缓存。
前端需要把这两个状态分别表达清楚。首次等待可能来自模型下载,不能统一包装成「AI 正在处理」。
执行环境也做了拆分。抠图和修补共用 ai.worker.ts,导入 onnxruntime-web/wasm;放大使用独立的 upscale.worker.ts,导入 onnxruntime-web/webgpu,对应构建只在放大时获取。像素缓冲区通过 postMessage 转移给 Worker,结果再转移回来,避免复制整块数据。失败的会话会被丢弃,供下次重建。
放大功能用下面这段代码选择执行后端:
async function createSession(bytes: ArrayBuffer): Promise<ort.InferenceSession> {
if ('gpu' in navigator) {
try {
return await ort.InferenceSession.create(bytes, { executionProviders: ['webgpu'] })
} catch {
// 没有可用的适配器,继续回退到 CPU。
}
}
return ort.InferenceSession.create(bytes, { executionProviders: ['wasm'] })
}
要看的是 InferenceSession.create 外层的 try:存在 navigator.gpu,也不保证 WebGPU 会话能创建成功。抠图和修补始终指定 executionProviders: ['wasm']。两个 Worker 都设置 ort.env.wasm.numThreads = 1,因为页面没有跨源隔离,无法使用 WASM 多线程。
模型处理的是固定输入,不是任意尺寸原图
抠图先通过 sourceSquare 把画面拉伸到 1024×1024,对应 SEGMENT_SIZE。普通画布直接绘入,线性 RAW 数据则先采样,再编码为 8-bit sRGB。
随后丢弃 alpha,找到至少为 1 的 RGB 峰值 peak,将各通道按 通道值 / peak - 0.5 写入 NCHW 浮点数组。NCHW 可以理解为:按批次、通道、高、宽排列,把整张图的红、绿、蓝通道分开放。
模型通过 session.inputNames[0] 接收输入,文件中名称为 input_image;输出通过 outputNames[0] 读取,名称为 output_image。结果经 min–max 缩放,变成逐像素的 alpha。
cleanCutout 还会移除小于最大连通区域 3% 的碎片,参数为 SPECK_RATIO,并保留 12 像素柔和边缘,对应 SUBJECT_MARGIN。蒙版每张照片只算一次,预览和导出通过方向、裁剪映射复用。
代价也在这里:超宽或超高的画面,会先被压成正方形再交给网络。
物体移除则使用 MI-GAN pipeline,输入为:
image: uint8 [1, 3, H, W],RGB 平面布局。mask: uint8 [1, 1, H, W],255 表示保留,0 表示填补。result: uint8 [1, 3, H, W],输出尺寸与输入一致。
应用围绕笔画截取局部区域,长边最多为 PATCH_EDGE = 1024。pipeline 内部再裁剪、缩放到 512,完成修补与融合。局部足够时,就不处理整张照片。
多个修补区域按顺序执行,后一次能看到前一次的填补结果。最终贴回的是补丁,原始文件不被修改。
放大能处理大图,靠的是切块和输出上限
Real-ESRGAN general x4v3 使用 Qualcomm AI Hub 的 ONNX 导出版本。固定参数为 TILE = 128、PAD = 8、SCALE = 4:输入是除以 255 后的 RGB float32 [1, 3, 128, 128],输出 upscaled_image 为 float32 [1, 3, 512, 512]。
切块步长为 128 − 2×8 = 112 像素。相邻块保留重叠区,拼接时舍弃上下文不足的边缘预测,主要使用中间部分。最后一块贴齐图像边界;某一边不超过 128 时,通过重复边缘像素补足输入。
用户选择 2× 时,Worker 不会先生成完整 4× 大图,再把它缩小。它直接从每个 4× 输出块采样到目标尺寸;低于 4× 时,对小网格采样取平均,subs 最多为 3。这样能省下完整中间图的内存。
放大只在导出时执行。upscaleFactor 会根据导出长边限制实际倍率,并遵守 MAX_OUTPUT_EDGE = 8192 和 MAX_OUTPUT_PIXELS = 40,000,000。倍率低于 1.1 就跳过。
模型之后还有两项补偿:通过 TEXTURE_MIX = 0.2 混回 20% 的原图平滑缩放结果,缓和强降噪带来的皮肤蜡感;原图存在透明像素时,再放大原始 alpha,以 destination-in 恢复透明区域。
这套方案的完整性,来自模型前后的图像处理和资源控制。
我的判断
我认可它把推理移出界面线程、按需加载模型、切块控制内存的做法。尤其是缓存校验和 WebGPU 失败回退,处理的是上线后一定会遇到的状态。
但文章展示的是代码路径,没有公布耗时,也没有设备对照数据。它不能证明本地推理比服务端更快,也不能证明普通手机都能流畅使用。单线程 WASM 的速度取决于浏览器和设备;8192 像素、4000 万像素是代码上限,不是逐款手机验证后的安全承诺。
质量同样缺少对照。3% 碎片过滤、12 像素边缘、20% 原图混合都有明确目的,但对不同照片是否合适,仍是经验选择。多个小主体可能受连通区域过滤影响,固定输入尺寸也可能损失细节,这些需要自己的样本验证。
与服务端推理相比,本地方案能让照片留在设备上,也减少上传依赖;代价是首次下载和客户端资源差异。原文没有成本、速度或画质对照,无法据此替团队选定路线。
许可证也要分开核对:应用为 MIT;IS-Net 为 Apache-2.0,来自 rembg 的权重通过 quantize_dynamic、QUInt8、per channel 动态量化为 int8;Real-ESRGAN 及 Qualcomm AI Hub 的 v0.64.0 float 导出为 BSD-3-Clause,项目只合并外部权重文件;MI-GAN 的代码与权重为 MIT。应用开源协议不能覆盖全部模型资产。
能拿来做什么
- 给前端加入图像 AI 时,可以沿用「按功能下载、校验缓存、Worker 推理」的结构,并分别展示下载、推理和失败状态。
- 做大图导出时,可以先确定最终尺寸,再设计切块和采样流程,避免为了 2× 输出创建完整 4× 中间图。
- 验证方案时,应分别测试 WebGPU 成功与 WASM 回退,覆盖长条图、透明图和多主体照片,记录耗时、内存与画质。
- 采用现成 ONNX 模型前,应检查输入布局、归一化、蒙版语义和许可证,不能只凭模型文件名接入。