纯前端实现 AI 抠图:我如何用 ONNX + Canvas 在浏览器里跑背景移除

前言
市面上的抠图工具几乎都需要把照片上传到服务器,等 AI 处理完再下载。隐私?全靠信任。
我做了一个不一样的方案:AI 模型直接跑在浏览器里,图片从头到尾不离开你的设备。最近把核心逻辑开源了,两个文件,400 行代码,拿来就能跑。
- 🔗 在线体验:toolknit.com/tools/backg...
- 🔗 开源代码:GitHub
- 🔗 完整工具站(61个免费工具):toolknit.com
技术架构总览
css
用户上传图片 → 加载 ONNX 模型 → WebAssembly 推理 → 生成 Mask → Canvas 合成 → 下载 PNG
全程浏览器端完成,零后端。
核心依赖
| 库 | 作用 | 大小 |
|---|---|---|
@imgly/background-removal |
ONNX 语义分割模型 + Runtime | ~40MB(首次加载,之后缓存) |
| Canvas 2D API | 图像合成、Mask 编辑 | 浏览器原生 |
@imgly/background-removal 封装了:
- ONNX Runtime Web(WebAssembly 后端)
- 预训练的人像/物体分割模型
- 图像预处理和后处理管线
实现步骤拆解
第一步:动态加载 AI 库
javascript
const LIB_CDN = 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.5.5';
async function loadLibrary() {
const module = await import(LIB_CDN + '/+esm');
removeBackgroundFn = module.removeBackground;
}
为什么用动态 import() 而不是打包?
- 模型文件 40MB,打包进 bundle 不现实
- 动态加载 = 用到才下载,不用不花流量
- 浏览器自带缓存,第二次秒加载
第二步:执行推理
javascript
// 把用户图片转为 Blob
const imageBlob = await new Promise(r => canvas.toBlob(r, 'image/png'));
// 调用 AI 推理
const resultBlob = await removeBackgroundFn(imageBlob, {
model: 'medium',
output: { format: 'image/png' },
progress: (key, current, total) => {
// 更新进度条
}
});
内部流程:
- 将图片缩放到模型输入尺寸
- 像素数据转 Tensor
- ONNX Runtime 通过 WebAssembly 执行分割网络
- 输出 per-pixel 前景概率图
- 应用 alpha 通道得到透明背景图
性能参考:
- 笔记本(M1/i7):2-5 秒
- 手机(中端):8-15 秒
- 首次加载模型:额外 20-30 秒(之后缓存)
第三步:构建可编辑 Mask
AI 的结果不是最终答案------它只是起点。我把 AI 输出的 alpha 通道提取为一张灰度 Mask:
javascript
// 白色 = 前景(保留),黑色 = 背景(移除)
for (let i = 0; i < resultData.data.length; i += 4) {
const alpha = resultData.data[i + 3]; // AI 输出的 alpha 通道
maskData.data[i] = alpha; // R
maskData.data[i + 1] = alpha; // G
maskData.data[i + 2] = alpha; // B
maskData.data[i + 3] = 255; // Mask 本身始终不透明
}
为什么不直接用 AI 输出?
因为没有 AI 能 100% 完美。头发丝、透明物体、相似色背景------总会有瑕疵。把结果转为 Mask 后,用户可以用画笔手动修正。
第四步:画笔精修
这是让工具真正可用的关键功能:
javascript
function paintOnMask(e) {
const brushSize = parseInt(brushSizeEl.value);
const softness = parseInt(brushSoftEl.value) / 100;
maskCtx.lineCap = 'round';
maskCtx.lineWidth = brushSize;
// 边缘柔化 = 对笔触施加模糊
if (softness > 0) {
maskCtx.filter = `blur(${Math.round(brushSize * softness * 0.3)}px)`;
}
// 画笔 = 画白色(恢复前景)
// 橡皮 = 画黑色(擦除为背景)
if (currentTool === 'brush') {
maskCtx.globalCompositeOperation = 'lighter';
maskCtx.strokeStyle = '#ffffff';
} else {
maskCtx.globalCompositeOperation = 'source-over';
maskCtx.strokeStyle = '#000000';
}
maskCtx.beginPath();
maskCtx.moveTo(lastX, lastY);
maskCtx.lineTo(x, y);
maskCtx.stroke();
}
技术细节:
- 坐标映射:编辑画布被 CSS 缩放以适应视口,但 Mask 始终在原始分辨率下操作。每个鼠标坐标都要从显示坐标换算到 Mask 坐标
- 边缘柔化 :利用 Canvas 2D 的
filter: blur()实现羽化效果 - 撤销栈 :每次 mousedown 保存完整
ImageData快照,最多 20 层 - 快捷键 :
B切画笔、E切橡皮、[/]调大小、Ctrl+Z撤销
第五步:合成输出
最后把 Mask 应用到原图:
javascript
function applyMaskToOriginal() {
for (let i = 0; i < origData.data.length; i += 4) {
outData.data[i] = origData.data[i]; // R --- 原图
outData.data[i + 1] = origData.data[i + 1]; // G --- 原图
outData.data[i + 2] = origData.data[i + 2]; // B --- 原图
outData.data[i + 3] = mData.data[i]; // A --- 来自 Mask 的 R 通道
}
}
Mask 的 R 通道值直接作为输出的 alpha 通道。白=不透明,黑=全透明,灰=半透明(适合头发和柔化边缘)。
精修模式的视觉反馈
在精修模式下,被移除的区域会显示半透明红色覆盖层:
javascript
for (let i = 0; i < overlayData.data.length; i += 4) {
const maskVal = overlayData.data[i];
if (maskVal < 128) {
// 被移除的区域 → 半透明红
overlayData.data[i] = 220; // R
overlayData.data[i + 1] = 50; // G
overlayData.data[i + 2] = 50; // B
overlayData.data[i + 3] = 120; // A
} else {
// 保留的区域 → 完全透明(显示下面的原图)
overlayData.data[i + 3] = 0;
}
}
用户可以实时看到哪些区域被删除了,边画边看效果。
性能优化要点
| 问题 | 方案 |
|---|---|
| 模型太大(40MB) | 浏览器缓存 + 进度条提示 |
| 大图内存占用高 | 三张 Canvas(原图/Mask/输出)共存,4000×3000 约 144MB |
| 画笔实时渲染卡顿 | requestAnimationFrame 节流 |
| 移动端手势冲突 | passive: false + preventDefault 阻止滚动 |
| 大图预览模糊 | 编辑 Canvas 始终以 CSS 缩放,Mask 保持原始分辨率 |
开源版 vs 生产版
| 特性 | 开源版 | 生产版(ToolKnit) |
|---|---|---|
| 核心抠图 | ✅ | ✅ |
| 手动精修 | ✅ | ✅ |
| 撤销/快捷键 | ✅ | ✅ |
| 触屏支持 | ✅ | ✅ |
| 使用次数限制 | ❌ | ✅(公平使用) |
| 模型自托管 | ❌(CDN) | ✅(自有 CDN,更快) |
| 数据统计 | ❌ | ✅ |
| 音效反馈 | ❌ | ✅ |
生产版在 toolknit.com/tools/backg...,开源版两个文件直接跑:
bash
git clone https://github.com/2645149786-dotcom/toolknit.git
cd toolknit/open-source/background-remover-standalone
npx serve .
# 打开 http://localhost:3000
扩展方向
如果你想基于这个做更多:
- 替换背景 --- 在 Mask 的黑色区域填入纯色或自定义图片
- 批量处理 --- 多张图片排队推理
- WebGPU 加速 --- ONNX Runtime Web 已支持 WebGPU 后端,推理速度可提升 3-5x
- 边缘后处理 --- 对 Mask 做可调半径的高斯模糊,统一边缘质量
- 导出格式 --- 支持 WebP/AVIF 输出
总结
2026 年的浏览器已经强大到能跑完整的语义分割模型了。@imgly/background-removal + Canvas 2D API 的组合让我们可以做到:
- 🔒 零上传 --- 图片从不离开设备
- 🎨 可精修 --- 不是"一键出图"然后只能接受
- 📦 可开源 --- 核心逻辑 400 行,两个文件
- 📱 跨平台 --- PC/手机/平板通用
如果你也在做需要图像处理的前端项目,这个方案值得参考。
相关链接:
- 在线体验:toolknit.com/tools/backg...
- 开源仓库:github.com/2645149786-...
- ToolKnit 全站(61个免费浏览器工具):toolknit.com
我是 ToolKnit 的开发者,一个人从零搭建了 61 个纯浏览器端的免费在线工具。如果这篇文章对你有帮助,点个赞鼓励一下 👍