ONNX Runtime部署ModNet人像抠图:从图像到实时视频的完整实战

📅 发布时间:2026/9/2 4:15:49
ONNX Runtime部署ModNet人像抠图:从图像到实时视频的完整实战 简介一份基于MODNet与ONNX的Python部署方案面向需要图像、视频及摄像头实时抠图的开发者解决无需trimap即可自动分离前景与背景的落地问题。资源共11个文件压缩包26.29MB包含Python主程序main.py、image_display.py、ONNX格式模型、若干png/jpeg测试图片与视频样本便于直接运行与结果对比。目前已有1941人学习下载适合具备Python与深度学习推理基础、希望快速上手MODNet的读者。压缩包内目录划分清晰images/提供测试图、output/存放处理结果、onnx_model/保存模型、video/放入测试视频可完整体验从模型加载、预处理到推理输出的流程。MODNet在发丝级细节上表现优秀适用于视频编辑、虚拟背景、直播等场景同时需注意CPU执行较慢推荐配置GPU以提升实时处理性能。 做这个ModNet部署的起因其实挺实际——有朋友问能不能搞一套轻量级的人像抠图方案别上分割模型那套重型框架就想要个能在普通电脑上跑起来、既能处理单张图也能实时处理视频和摄像头画面的工具。我调研了一圈最后选定ModNet作为核心模型推理引擎用ONNX Runtime语言自然就是Python一把梭到底。先说结论这套方案完整跑通了三个场景——单张人像图抠图、离线视频逐帧抠像、摄像头实时抠像。整个工程下来核心就是一个Python类加一个ONNX模型代码量不大。但对新手来说坑确实不少尤其是模型转换和预处理这两块网上资料说法不一我踩了一圈坑才把流程理顺。下面按我自己的实操路径来写从方案选型到环境搭建再到三个场景的代码实现最后是性能和常见问题。适合想做AI抠图的朋友参考也适合第一次接触ONNX部署的同学看看PyTorch模型是怎么变成跨平台推理服务的。1. 项目概述与方案选型1.1 matting 和 segmentation 的区别为什么选 ModNet先澄清一个容易混淆的概念。图像分割segmentation输出的是硬掩码每个像素要么是前景要么是背景像素值就是0或1而matting输出的是alpha matte取值范围在0到1之间连续变化专门用来刻画头发丝、半透明纱巾这类边缘细节。换句话讲分割给的是“这是人”的判断matting给的是“这个像素有多像人”的置信度。做抠图合成时alpha值直接参与前景背景的加权混合边缘自然就柔和很多。ModNet这个名字是Matting Object Detection Network的缩写2020年发布核心卖点就是轻量实时。整模型基于MobileNetV2主干权重文件只有二十多MB输入分辨率512x512在普通CPU上单帧推理能控制在100毫秒以内。相比U2Net、BackgroundMattingV2这些模型ModNet在速度和体积上优势明显而且官方直接给了训练好的肖像抠图权重不需要自己准备数据集训练拿来就能用。1.2 为什么选 ONNX Runtime Python模型确定之后部署框架的选择其实没必要纠结。ONNX Runtime是微软开源的推理引擎支持PyTorch、TensorFlow等主流框架导出的ONNX模型跨平台跑Windows、Linux、macOS通吃。用它的理由很简单第一ONNX是模型交换格式以后换推理后端不用重新导出第二ONNX Runtime对CPU推理做了深度优化还有OpenVINO、TensorRT这类加速插件可以切换第三Python接口成熟配合OpenCV处理图像简直顺手整个推理链路写起来非常短。有人可能会问为什么不直接用PyTorch推理说实话如果只是自己跑着玩PyTorch完全够用。但如果考虑部署到别的机器、嵌入到别的项目总不能每台机器都装一套PyTorch环境。ONNX Runtime是个独立的推理库安装包小、依赖少部署成本低一个量级。而且ONNX Runtime在CPU上的推理速度通常比PyTorch的CPU模式还要快这点我在后面实测数据里会体现。2. 环境准备与模型获取2.1 环境依赖安装整个项目依赖非常克制三个包就够pip install onnxruntime opencv-python numpy如果你用的是conda环境建议先建一个干净环境再装避免和别的项目冲突conda create -n matting python3.9 conda activate matting pip install onnxruntime opencv-python numpy版本方面没有特殊要求onnxruntime 1.14以上的版本都行opencv推荐4.5以上。唯一要注意的是Python版本不要太新3.10以下最稳妥有些旧版onnxruntime对3.11的兼容有点问题。我最早在Python 3.11上装onnxruntime 1.13踩过坑编译报错换3.9就一路顺畅。2.2 模型下载与 ONNX 转换ModNet官方仓库GitHub上的ZhuGeKexin/ModNet提供了PyTorch训练的checkpoint文件主要有两个权重文件训练场景特点modnet_photographic_portrait_matting.ckpt静态图像单帧推理结构简单modnet_webcam_portrait_matting.ckpt摄像头/视频包含时序分支输出隐藏状态这里有个重要的选择点。webcam版权重包含一个V分支时序分支推理时需要把上一帧的hidden state作为额外输入这样能保证视频帧之间的alpha平滑不闪烁。但引入时序状态会让ONNX导出和部署复杂不少得维护两个输入输出。如果只是做本地工具、对画面稳定性要求不是极端苛刻直接用photographic权重逐帧推理就够了省心。我实际测下来静态权重跑视频会有轻微的边缘抖动但离远了看不明显多数场景能接受。我在项目里用的是photographic权重。导出ONNX的脚本核心片段如下import torch from src.models.modnet import ModNet model ModNet(backbonemobilenetv2, pretrainedTrue).eval() ckpt torch.load(modnet_photographic_portrait_matting.ckpt, map_locationcpu) model.load_state_dict(ckpt[state_dict]) dummy_input torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy_input, modnet.onnx, input_names[input], output_names[output], opset_version11, dynamic_axes{input: {0: batch}, output: {0: batch}} )导出时opset_version建议11以上低于11某些算子可能不被支持。dynamic_axes把batch维度设置成动态这样导出后的模型既能跑单张图也能跑batch推理灵活性大一些。注意如果加载权重时遇到键名不匹配检查一下ckpt[state_dict]里的key是否带module.前缀带的话用load_state_dict(new_ckpt, strictFalse)也能过。我第一次转就栽在这上面调了半天才发现是权重键名前缀的问题。3. 核心代码预处理、推理、后处理3.1 预处理细节最容易翻车的地方预处理是整个部署流程里最容易被忽视也最容易出错的部分。ModNet的训练管道里图像会先缩放到512x512然后除以255归一化到[0,1]再按ImageNet数据集的均值方差做标准化均值是[0.485, 0.456, 0.406]标准差是[0.229, 0.224, 0.225]。这套标准化参数来自ImageNet分类任务的统计值很多视觉模型都在用但如果你忘了做这一步或者参数写错输出的alpha会歪得离谱人物的边缘糊成一片。另一个坑是通道顺序。OpenCV读图默认是BGR顺序而ModNet训练时用的是RGB顺序所以预处理时一定要做一次通道翻转。我封装了这样一个预处理函数def preprocess(self, bgr_img): h, w bgr_img.shape[:2] # 保持长宽比的 letterbox 缩放 scale 512 / max(h, w) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(bgr_img, (new_w, new_h)) canvas np.zeros((512, 512, 3), dtypenp.uint8) y_off (512 - new_h) // 2 x_off (512 - new_w) // 2 canvas[y_off:y_off new_h, x_off:x_off new_w] resized # BGR - RGB归一化标准化 img canvas[:, :, ::-1].astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std img img.transpose(2, 0, 1)[None, ...] return img.astype(np.float32), scale, x_off, y_off, (h, w)我选择了letterbox方式而不是直接拉伸到512x512原因是直接拉伸会让宽幅照片里的人脸变形影响抠图质量。letterbox会在保持长宽比的前提下缩放到512内的最大尺寸四周用0像素填充。填充的这部分在推理时会被网络判定为背景所以后处理阶段必须把padding区域裁掉否则生成的alpha边缘会有一圈黑框。这个逻辑看着简单实际代码里忘了处理padding是新手最常见的错误之一。3.2 推理与后处理alpha合成全流程推理本身很简单ONNX Runtime的接口就一个run方法。后处理的关键在于把输出从512x512恢复回原图分辨率然后做前景背景合成。class MattingEngine: def __init__(self, onnx_path): so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session ort.InferenceSession( onnx_path, so, providers[CPUExecutionProvider] ) self.input_name self.session.get_inputs()[0].name def predict_alpha(self, bgr_img): tensor, scale, x_off, y_off, (h, w) self.preprocess(bgr_img) out self.session.run(None, {self.input_name: tensor})[0] alpha out[0, 0] # 输出形状是 [1, 1, 512, 512] # 裁掉 padding 区域 new_h int(h * scale) new_w int(w * scale) alpha alpha[y_off:y_off new_h, x_off:x_off new_w] alpha cv2.resize(alpha, (w, h), interpolationcv2.INTER_LINEAR) return np.clip(alpha, 0, 1) staticmethod def composite(bgr, alpha, bg_bgrNone): a alpha[..., None] if bg_bgr is None: b, g, r cv2.split(bgr) a8 (a * 255).astype(np.uint8) return cv2.merge([b, g, r, a8]) # BGRA 透明背景 bg cv2.resize(bg_bgr, (bgr.shape[1], bgr.shape[0])) fused bgr * a bg * (1 - a) return fused.astype(np.uint8)这段代码是整个项目的核心骨架。predict_alpha接收一帧BGR图像返回和原图同尺寸的alpha矩阵值域在0到1之间。composite函数做两件事如果不提供背景图就输出带透明通道的BGRA四通道图用于保存PNG如果提供了背景图就做一个标准的alpha blending背景自动缩放成和前景一样大小。alpha blending的公式就是像素级加权求和前景乘以alpha再加上背景乘以(1-alpha)理解成两张图叠在一起就好。有个小细节需要注意推理输出的alpha数值不一定严格在0~1区间内会有略微的越界合成前做一次np.clip防止后面的uint8转换产生溢出噪点。这个不处理的话极少数像素颜色会异常偏亮或偏暗虽然一般人不注意但强迫症患者看不下去。4. 三种场景的完整实现4.1 图像 matting静态图抠图图像抠图是最直接的场景读图、推理、合成、保存四步搞定。def image_matting(img_path, save_path, onnx_pathmodnet.onnx): engine MattingEngine(onnx_path) img cv2.imread(img_path) alpha engine.predict_alpha(img) rgba engine.composite(img, alpha) # 透明背景 cv2.imwrite(save_path, rgba)这里有个使用习惯要提一下。cv2.imwrite保存四通道PNG时是按BGRA顺序写入的但很多看图软件和网页默认按RGBA解析有的软件打开颜色会偏色。跨平台使用建议在保存前转成RGBArgba_bgra engine.composite(img, alpha) rgba cv2.cvtColor(rgba_bgra, cv2.COLOR_BGRA2RGBA) cv2.imwrite(output.png, rgba)这样导出的PNG在正常浏览器、看图工具里颜色就正确了。图像场景下我对精度比较满意头发丝区域虽然比专业商业软件略逊一筹但胜在速度快一张1920x1080的图从读入到保存大概三百毫秒批量抠图完全可行。4.2 视频 matting离线视频抠像视频matting本质就是逐帧调用图像matting但有一个绕不开的问题视频文件是三通道的没法直接保存透明通道。解决办法是给每一帧合成到纯色背景或者自定义背景图上然后写回视频。def video_matting(src, dst, onnx_pathmodnet.onnx, bg_pathNone, solid_bg(0, 255, 0)): engine MattingEngine(onnx_path) cap cv2.VideoCapture(src) fps cap.get(cv2.CAP_PROP_FPS) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer cv2.VideoWriter( dst, cv2.VideoWriter_fourcc(*mp4v), fps, (width, height), ) bg cv2.imread(bg_path) if bg_path else None frame_idx 0 while True: ret, frame cap.read() if not ret: break alpha engine.predict_alpha(frame) if bg is not None: frame engine.composite(frame, alpha, bg) else: solid np.full_like(frame, solid_bg) frame engine.composite(frame, alpha, solid) writer.write(frame) frame_idx 1 cap.release() writer.release()实际跑视频时几个参数要提前想清楚。视频编码器我用的是mp4v输出文件是MP4格式兼容性最好如果你要输出带透明通道的序列帧比如后续进AE合成改成保存PNG序列帧就行。另一个优化点是视频处理是按真实帧率逐帧跑的普通1080p视频在CPU上大约每秒处理6到8帧意味着25fps的视频一秒钟内容要跑4秒左右建议处理前先用低分辨率版本测试流程通不通避免浪费大量时间跑完才发现合成参数不对。4.3 摄像头 matting实时预览摄像头场景最考验速度也最影响体验。目标只有一个让预览画面看起来流畅延迟在可接受范围内。def camera_matting(cam_id0, onnx_pathmodnet.onnx, bg_pathNone): engine MattingEngine(onnx_path) cap cv2.VideoCapture(cam_id) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_AUTOFOCUS, 0) bg cv2.imread(bg_path) if bg_path else None while True: ret, frame cap.read() if not ret: break frame cv2.flip(frame, 1) # 镜像更符合自拍习惯 alpha engine.predict_alpha(frame) if bg is None: solid np.full_like(frame, (120, 120, 220)) result engine.composite(frame, alpha, solid) else: result engine.composite(frame, alpha, bg) cv2.imshow(Matting, result) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()摄像头画布我固定到640x480这个分辨率做实时推理是个平衡点。分辨率再高CPU推理时间就压不住画面会明显卡顿再低的话边缘细节损失严重抠图质量肉眼可见下降。640x480在普通笔记本CPU上大概能跑到12到15帧虽然没有满帧丝滑但作为工具够用了属于“能明显感觉有点卡但不影响使用”的范畴。把摄像头输入分辨率限制在640x480还有一个隐藏好处——推理前的letterbox缩放开销也一并省了不少。摄像头画面通过cap.set设置分辨率后读出来的帧本身就是接近4:3的比例缩放时变形很小。5. 性能优化与常见问题5.1 实时性能优化三板斧摄像头场景帧率不够最直接的办法不是换模型而是改推理输入尺寸。ModNet输入默认512x512如果把letterbox的目标尺寸从512降到384甚至320推理时间几乎能砍半。我在384x384输入下实测帧率能到22左右边缘质量虽有下降但摄像头预览画面本来就小感知不明显。第二个优化手段是换执行提供程序。如果你用的是Intel CPU可以改用OpenVINO做推理后端session ort.InferenceSession( onnx_path, providers[OpenVINOExecutionProvider, CPUExecutionProvider] )OpenVINO对Intel平台有专门的算子优化同样条件下推理速度通常能再提升30%到50%。注意安装时要先装好openvino包pip install openvino。如果是NVIDIA显卡还可以用TensorRT EP但配置要复杂一些摄像头场景我不推荐因为TensorRT加载引擎的初始化时间比较长。第三板斧是跳帧。摄像头高帧率其实是一种资源浪费因为视觉连续帧之间变化很小alpha结果也基本一致。可以每两帧或每三帧才推理一次中间跳过的帧直接复用上一次的alpha结果。加上这个策略显示器刷新率是60Hz的情况下预览看起来依然连贯但CPU占用率能降一个台阶。5.2 常见问题排查与避坑记录我整理了一份自己踩坑过程中的排查清单按遇到概率排了序现象可能原因解决办法输出alpha全黑或全白预处理没做RGB转换或均值方差不对检查是否有BGR2RGB、是否正确归一化人物边缘有一圈黑/白边框letterbox的padding区域没裁干净后处理时按scale和offset裁剪视频保存后画面颜色发怪保存PNG透明图时BGRA/RGBA顺序不对用cv2.cvtColor转成RGBA再保存摄像头预览画面卡顿严重输入分辨率太高或未开启任何加速降到640x480、换OpenVINO、跳帧加载ONNX时报opset错误导出的opset版本太老导出时opset_version11或更高摄像头画面不停闪烁自动曝光/自动白平衡在波动使用cap.set关闭自动曝光和自动对焦视频每帧alpha边缘抖动明显逐帧独立推理缺少时序一致性换官方video权重或对alpha做时间维平滑关于最后一个问题多说两句。如果视频抠像的画面稳定性是刚需可以考虑对alpha做指数移动平均用前一帧的alpha和后一帧的alpha加权alpha_smooth 0.7 * alpha_current 0.3 * alpha_prev这样一个简单的后处理就能把边缘的帧间闪烁压下去。我试过0.5/0.8之类的权重0.7/0.3这个比例比较均衡既保留了响应速度又不会让动作出现拖影。权重调得越大越平滑但人物快速移动时会产生类似“残影”的效果需要根据实际场景微调。6. 写在最后的一点经验整套流程跑完最大的感受是模型选型和部署框架确实重要但真正决定项目能不能落地的往往是预处理细节和场景适配。ModNet本身很成熟ONNX Runtime也很稳定中间的坑几乎都在“图像怎么喂进去、输出怎么用起来”这些看起来不起眼的环节上。这也是为什么我强烈建议如果你第一次做ONNX部署不要一上来就端到端跑一个大工程先拿一张图把预处理、推理、后处理的全链路调通再去扩展视频和摄像头。另外ModNet的权重在室内光线均匀、主体居中的场景下表现最好逆光、全身照、复杂的多人场景效果会打折。如果你的业务场景比较特殊比如要抠商品而不是人像建议自己去采集数据微调模型ModNet的可训练性还是挺好的。这套基于ONNX Runtime的部署代码可以原样复用换成你自己的权重文件就行这也是当初选择ONNX格式的核心价值——模型和推理框架解耦后续迭代完全不用动工程代码。本文还有配套的精品资源点击获取