
之前有个朋友问我公司电脑不让装软件又不想把照片传到别人的服务器上处理只想随手拖一张图进去几秒钟拿到透明背景素材怎么办。我脑子里转了一圈Photoshop太重Python环境折腾一圈能劝退一半人在线抠图API要注册要Key还要按次计费。最后我直接在浏览器里搭了个纯前端页面塞进去一个40MB左右的离线模型自动抠图效果还不赖。整个过程不用Python、不用后端服务、不用申请API Key、也不用显卡一个静态网页就能跑。这篇文章我准备把整个思路和实现过程拆开讲清楚包括模型怎么选、40MB这个体积是怎么压出来的、浏览器里的推理管线怎么搭、实际跑了哪些坑。就算你不是前端开发按着步骤也能在本地把页面跑起来。1. 为什么非要在浏览器里啃下自动抠图这块骨头1.1 传统抠图方案的门槛到底在哪先说最常见的几条路。Photoshop抠图除非你是老手否则用钢笔工具一点一点描边遇到头发丝直接崩溃。你要是会点Python打开思路去跑深度学习模型理论上很强大但实际操作全是坑。我见过太多人卡在环境配置上先装Anaconda再建虚拟环境然后pip install一堆依赖结果PyTorch装好了CUDA版本又对不上。模型下载下来几百MB跑起来还要看显卡脸色。可能折腾一下午最后呈现的结果就是一张带着半截背景的图。这套玩法只适合真正准备长期研究AI的人不适合我就想抠张图发朋友圈的场景。另一条路是调在线API。现在很多平台都提供抠图接口效果确实好但你得先注册账号、绑定支付方式、拿到API Key然后按照文档构造请求。免费额度用得飞快图片还要上传到第三方服务器。如果你是处理私人照片或者公司敏感素材心理上那关很难过。所以我当时的判断是对于低频、即时、注重隐私的抠图需求一个能在本地打开、加载离线模型、不依赖网络请求的网页工具才是最舒服的解法。1.2 网页端运行AI模型为什么突然可行了很多人对浏览器跑AI模型的印象还停留在玩具阶段。但实际上现在浏览器里跑深度学习推理已经非常成熟主要是两个东西在起作用一个是WebAssembly另一个是各类前端推理框架。WebAssembly可以理解成给浏览器准备的一种高性能指令集。它把C、Rust写的推理引擎编译成.wasm文件浏览器执行的时候接近原生性能。TensorFlow.js、ONNX Runtime Web这些库本质上就是把底层的推理逻辑编译成WASM再给JavaScript暴露一套简单的API。也就是说训练好的模型只要能转换成ONNX或者量化格式前端就能直接加载并计算结果。再加上现代浏览器对多线程、SIMD指令的支持纯CPU跑一个小模型的速度完全够用。40MB的模型体积对于现代网页来说是个挺合理的资源大小首屏加载也就一两秒。这就是整个方案能成立的基础模型不大推理框架成熟浏览器性能足够。下面要解决的就是用什么模型和怎么把模型塞进网页这两个关键问题。2. 40MB离线模型是怎么选出来和压出来的2.1 适合浏览器运行的抠图模型有哪些自动抠图本质上是个语义分割或显著性目标检测任务。模型输入一张图片输出一张和原图同尺寸的灰度掩码掩码里每个像素的值代表它属于前景的概率。网页端能跑的模型我主要关注三类。MODNet轻量级人像抠图模型结构精简FP32权重大概二十多MB。速度非常快处理人像边缘效果不错缺点是更偏人像场景对宠物、商品等通用目标的泛化能力一般。U²-Net经典显著性目标检测网络R型结构分割效果稳定。FP32权重体积大约170MB左右但压缩空间大量化到40MB级别完全可行。RMBG-1.4这一类的通用背景移除模型训练数据覆盖多种目标抠图泛化能力强原始ONNX体积在170MB上下量化后也能控制在几十MB范围。我最后选了U²-Net结构的一个变体转成ONNX格式后再做量化压缩最终体积稳定在40MB左右。这个体积的好处非常明确放在静态服务器上加载很快就算用户电脑配置老一些浏览器加载40MB也不会卡到让人失去耐心。2.2 从几百MB压到40MB的量化思路大模型塞进浏览器最直接的矛盾就是体积。这里用到的核心技术叫模型量化。常规深度学习训练时参数存的是32位浮点数也就是FP32。如果把这个精度降到16位甚至8位整数权重体积就会相应缩小到二分之一或四分之一。我用的流程是先把PyTorch或Keras训练好的模型导出成ONNX格式然后用ONNX Runtime自带的量化工具做静态量化。这个工具会把权重从FP32转成INT8同时喂一批校准图片来尽量降低精度损失。量化过程中最需要留意的是边界质量。抠图任务对边缘精细度要求很高量化太激进的话头发丝或者物体边缘会出现锯齿甚至断裂。所以我通常会用FP16做中间方案体积和精度比较均衡如果对效果不满意再考虑部分层用动态量化。40MB这个数字其实是FP16少量INT8混合之后的结果我实测下来对日常商品图、人像图的边缘影响可以接受。2.3 ONNX格式在网页端的部署优势为什么不直接用TensorFlow.js格式因为ONNX是目前几乎所有训练框架都能导出、几乎所有前端推理框架都能加载的中转格式适配性最好。ONNX Runtime Web从底层支持WASM执行和前端结合非常丝滑。加载模型只需要一行代码指定模型地址然后就可以像调用普通JavaScript函数一样输入张量、拿输出张量。这样我们后面写页面逻辑就很省事。3. 网页端抠图管线的完整拆解3.1 从图片文件到模型输入张量整个网页端抠图流程用户看到的是拖一张图进去等几秒下载透明背景图。但背后其实是一条数据处理管线。第一步是拿到图片数据。页面上放一个拖拽区域监听drop事件从DataTransfer对象里取出File。接下来把它显示到页面上同时用一个隐藏的Canvas把图片画出来。为什么要有这一步因为模型输入不接受File对象我们需要把图片解码成像素数组。第二步是缩放。模型输入尺寸通常是固定的比如512x512或320x320。直接用canvas.drawImage把图片绘制到指定宽高的画布上这一步等于做了一次双线性插值缩放。需要特别注意的是原图宽高比可能不是1:1直接拉伸会变形。正确做法是等比缩放后居中填充多余部分用纯黑或纯白补边这样模型才能正确判断目标轮廓。第三步是转成张量。通过ctx.getImageData拿到的是一维数组每个像素占4个字节顺序是RGBA。但模型通常要求NCHW格式也就是先按通道排列每个通道再按高、宽展开。所以我们要做一个数据重组把RGBA拆成RGB三个通道去掉Alpha再按通道顺序填入Float32Array。3.2 用ONNX Runtime Web完成推理模型加载和推理这块我直接用onnxruntime-web库。初始化时指定模型文件路径然后创建一个InferenceSessionconst session await ort.InferenceSession.create(./model.onnx, { executionProviders: [wasm], graphOptimizationLevel: all }); const inputTensor new ort.Tensor(float32, pixelData, [1, 3, 512, 512]); const feeds { [session.inputNames[0]]: inputTensor }; const results await session.run(feeds); const maskTensor results[session.outputNames[0]];这里有个细节容易踩坑输入张量的形状必须和模型要求完全一致否则会直接报错。大多数从PyTorch导出的ONNX模型输入都是[1,3,height,width]如果有人导出的模型是动态尺寸推理速度会明显下降所以在导出模型时最好固定成静态形状。推理完成后拿到的输出张量形状一般是[1,1,height,width]取值范围在0到1之间表示每个像素属于前景的概率。后面要做的就是把这张低分辨率掩码还原回原图大小。3.3 把掩码做成透明背景PNG掩码和原图尺寸不一致必须先缩放回原图尺寸。可以用另一个Canvas把掩码数据抽出来绘制到和原图相同大小的画布上。如果你希望边缘更柔和可以用高斯模糊或羽化处理一下掩码避免抠出来的物体边缘太生硬。接下来是合图。这一步有两个方案方案A遍历像素手动把掩码值写入原图像素的Alpha通道。代码逻辑直接适合想精确控制边缘的情况。方案B利用Canvas的globalCompositeOperation合成。先画原图再画一层用掩码生成的Alpha图层设置混合模式最后导出。代码简单但边界控制能力弱一些。我推荐方案A。虽然多写几行代码但你可以逐像素判断掩码值大于0.9的完全保留小于0.1的直接透明中间值做成半透明过渡。这样抠出来的边缘看起来非常自然。最后用canvas.toBlob导出成PNGPNG格式天然支持Alpha通道保存后背景就是透明的了。4. 动手搭一个零依赖的抠图页面4.1 目录结构与准备工作其实整个项目不需要任何构建工具不需要Node不需要Webpack。你要准备的就四个文件/rmbg index.html app.js model.onnx ort.jsort.js是从onnxruntime-web的npm包里拷贝出来的浏览器版本也可以直接引用CDN地址。但如果你追求完全离线最好把所有文件下载到本地包括ONNX Runtime的WASM文件。model.onnx就是之前量化好的40MB模型。把文件放好后用浏览器直接打开index.html理论上能跑但很多浏览器对file://协议下的Worker和WASM有限制所以最稳妥的方式还是起一个本地静态服务。这里不需要Python你用VSCode的Live Server插件或者装个npx serve几秒钟就能搞定。如果只是放在局域网里给其他电脑用任何静态服务器工具都行。4.2 核心页面的HTML和脚本逻辑HTML结构非常简单一个拖拽区域、一个隐藏Canvas、一个下载按钮div iddropZone 拖拽图片到这里或者点击选择图片 /div video idpreview muted autoplay/video canvas idcanvas styledisplay:none/canvas button iddownload styledisplay:none下载透明PNG/buttonJS的核心逻辑就是上一章讲的管线。需要注意两点第一拖拽事件需要e.preventDefault()阻止浏览器默认打开图片第二处理大图时最好给Canvas设置一个最大像素上限比如长边不超过2000px否则内存占用会很高。4.3 实现一个简单但完整的预处理函数这里分享一个我实际在用的预处理代码片段function preprocessImage(img, targetSize 512) { const canvas document.createElement(canvas); canvas.width targetSize; canvas.height targetSize; const ctx canvas.getContext(2d); // 先等比缩放 const scale Math.min(targetSize / img.width, targetSize / img.height); const w Math.round(img.width * scale); const h Math.round(img.height * scale); const dx Math.floor((targetSize - w) / 2); const dy Math.floor((targetSize - h) / 2); ctx.fillStyle #fff; ctx.fillRect(0, 0, targetSize, targetSize); ctx.drawImage(img, dx, dy, w, h); const imageData ctx.getImageData(0, 0, targetSize, targetSize); const data imageData.data; // 按NHWC转NCHW并归一化 const channels 3; const floatData new Float32Array(targetSize * targetSize * channels); const mean [0.485, 0.456, 0.406]; const std [0.229, 0.224, 0.225]; for (let i 0; i targetSize * targetSize; i) { const rgbaIndex i * 4; floatData[i] (data[rgbaIndex] / 255 - mean[0]) / std[0]; floatData[targetSize * targetSize i] (data[rgbaIndex 1] / 255 - mean[1]) / std[1]; floatData[2 * targetSize * targetSize i] (data[rgbaIndex 2] / 255 - mean[2]) / std[2]; } return new ort.Tensor(float32, floatData, [1, 3, targetSize, targetSize]); }这段代码的核心是按CHW顺序重新填充数据。很多人第一次跑通却得到全黑或全白的错误结果往往就是这一步的通道顺序写错了。5. 实测中的性能表现与避坑记录5.1 不同设备和浏览器的实际表现我在自己的ThinkPad上跑过一个基准INT8量化的U²-Net模型512x512输入8代i5 CPUChrome浏览器平均推理时间大约在1.2秒到1.8秒之间。如果不开启多线程速度会慢30%到50%但依然可用。换到Apple Silicon的MacBook上Safari和Chrome的表现都不错Safari偶尔会在加载WASM时多等一会儿但推理速度反而更稳定。Firefox也跑过基本没问题。最让我意外的是安卓手机上的Chrome以前觉得手机跑不了这种模型实测下来一张512x512的图推理时间在三秒左右可以接受。唯一的坑是iOS Safari。它在Canvas和WASM内存分配上限制比较严格大图片很容易导致内存崩溃。如果要做移动端最好在读取图片后先压一下尺寸限制最大分辨率。5.2 内存不足和大图处理的应对方案网页端跑AI模型最大的瓶颈不是算力而是内存。一个4000x3000的原图解码后RGBA数据就有48MB如果再把掩码和中间张量保存在内存里内存占用很容易突破1GB。所以我的处理策略是输入模型前强制缩放输出时再缩放回合理尺寸但最终导出的PNG最大边限制在2000px。这个分辨率对绝大多数社交媒体和电商场景都够用了。如果你确实需要处理超高分辨率图片可以考虑分块推理把大图切成多个小块分别处理掩码最后拼起来。不过会增加很多边界接缝处理逻辑整体复杂度上升不少。我的建议是除非业务必须否则别做2000px长边已经能满足98%的使用场景。5.3 ONNX Runtime在浏览器里的隐藏坑第一个坑WASM文件路径。onnxruntime-web加载时需要去找对应的.wasm文件如果你引用的JS文件是本地路径但WASM文件没有一起拷过来运行时会抛错。正确做法是配置ort.env.wasm.wasmPaths指向你存放WASM文件的目录。第二个坑跨域限制。模型文件、WASM文件都不能跨域加载。如果你把页面放在GitHub Pages模型放在另一个CDN上默认会被浏览器拦截。解决办法是让模型文件同样放在GitHub Pages或者配置对方CDN的CORS头。第三个坑多线程特性。onnxruntime-web支持多线程WASM但开启多线程需要响应头里设置Cross-Origin-Embedder-Policy和Cross-Origin-Opener-Policy。GitHub Pages默认不带这些头所以很多静态托管上的实际运行是单线程模式。单线程也能跑只是速度稍微慢一点不必太纠结。5.4 模型加载与用户等待体验40MB模型在宽带环境下加载很快但在弱网环境下可能要让用户等很久。我在页面上加了进度提示用一个XMLHttpRequest监听下载进度而不是让用户盯着一张空白页发呆。模型第一次加载后也可以用浏览器缓存第二次访问时直接命中缓存加载时间可以缩短到一两百毫秒。如果很在意首次加载体验还可以考虑把模型放在IndexedDB里或者做成Service Worker预缓存。但这些都是加分项不加也能用核心功能完全不受影响。6. 这个纯前端抠图方案还能怎么扩展6.1 从人像到通用物体的模型切换很多人以为自动抠图只能抠人其实换一个模型权重就能处理完全不同的物体。这个方案里我用的是通用分割模型换成专门训练的商品模型就能抠鞋子、衣服、电子产品。ONNX格式的好处就是模型文件即插即用你只需要保证输入输出的张量形状一致页面逻辑基本不用改。如果要自己训练定制模型可以先用带GPU的机器训练再转成ONNX量化部署到网页端。等于把训练和推理分离真正使用者那边永远感知不到GPU的存在。6.2 批量处理与Web Worker优化如果想一口气抠几十张图直接在页面里循环跑就行但为了防止UI卡死最好把推理放到Worker里。onnxruntime-web自带Worker支持在主线程创建ort.InferenceSession后传给WorkerWorker里循环处理ImageData主线程只负责进度更新。我试过批量处理100张商品图速度稳定页面也不卡。6.3 WebGPU会让这个方案更快最近onnxruntime-web也开始支持WebGPU执行后端。WebGPU能让浏览器直接调用GPU计算但不需要用户安装任何驱动只需要一个现代浏览器。未来模型可以做得更大、更精细同时依然保持打开网页就能用的体验。40MB这个体积在WebGPU面前还有进一步减小的空间说不定以后10MB模型就能达到今天40MB的效果。从我个人折腾的经验来看这个方案的甜点在于把复杂留给开发者把简单留给用户。用户不需要懂Python、不需要配置文件、不需要注册账号拿到页面拖一张图就能用。如果你也只是想快速解决抠图问题照着上面的思路搭一遍四十分钟左右就能得到一个属于自己、不传图、不花钱的离线抠图工具。我自己现在已经把它当成了一个常驻浏览器书签的工具偶尔给文章配图或者给朋友帮个忙拖进去几秒钟就出结果。