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

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

前言

市面上的抠图工具几乎都需要把照片上传到服务器,等 AI 处理完再下载。隐私?全靠信任。

我做了一个不一样的方案:AI 模型直接跑在浏览器里,图片从头到尾不离开你的设备。最近把核心逻辑开源了,两个文件,400 行代码,拿来就能跑。


技术架构总览

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() 而不是打包?

  1. 模型文件 40MB,打包进 bundle 不现实
  2. 动态加载 = 用到才下载,不用不花流量
  3. 浏览器自带缓存,第二次秒加载

第二步:执行推理

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) => {
    // 更新进度条
  }
});

内部流程:

  1. 将图片缩放到模型输入尺寸
  2. 像素数据转 Tensor
  3. ONNX Runtime 通过 WebAssembly 执行分割网络
  4. 输出 per-pixel 前景概率图
  5. 应用 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

扩展方向

如果你想基于这个做更多:

  1. 替换背景 --- 在 Mask 的黑色区域填入纯色或自定义图片
  2. 批量处理 --- 多张图片排队推理
  3. WebGPU 加速 --- ONNX Runtime Web 已支持 WebGPU 后端,推理速度可提升 3-5x
  4. 边缘后处理 --- 对 Mask 做可调半径的高斯模糊,统一边缘质量
  5. 导出格式 --- 支持 WebP/AVIF 输出

总结

2026 年的浏览器已经强大到能跑完整的语义分割模型了。@imgly/background-removal + Canvas 2D API 的组合让我们可以做到:

  • 🔒 零上传 --- 图片从不离开设备
  • 🎨 可精修 --- 不是"一键出图"然后只能接受
  • 📦 可开源 --- 核心逻辑 400 行,两个文件
  • 📱 跨平台 --- PC/手机/平板通用

如果你也在做需要图像处理的前端项目,这个方案值得参考。


相关链接


我是 ToolKnit 的开发者,一个人从零搭建了 61 个纯浏览器端的免费在线工具。如果这篇文章对你有帮助,点个赞鼓励一下 👍

相关推荐
白鲸开源4 小时前
干货!SeaTunnel(2.3.12)高阶用法(一):核心概念之数据流
java·大数据·github
逛逛GitHub5 小时前
Karpathy 加入 Anthropic 了,盘点他开源的 5 个硬核 GitHub 项目。
github
你的保护色5 小时前
RAID学习
github
ChampaignWolf5 小时前
GitHub Copilot 用于 SAP ABAP 在 VS Code 中:本地部署团队设置指南
github·copilot
VIV-5 小时前
Pycharm项目上传到Github
ide·pycharm·github
xG8XPvV5d5 小时前
GitHub Actions自动化部署全攻略
运维·自动化·github
ChampaignWolf8 小时前
GitHub 发布全新 Copilot 独立应用,正面硬刚 Claude Code 与 Codex
github·copilot
冴羽yayujs10 小时前
GitHub 热门项目-日榜(2026-05-19)
前端·javascript·github
梦梦代码精10 小时前
LikeShop开源多端商城系统:半年使用记录
git·uni-app·github