☰
MMDetection导出ONNX避坑实战:Mask R-CNN部署全流程解析
2026/10/19 8:38:12 网站建设 项目流程

1. 先把问题说清楚:你到底卡在哪一步

最近好几个人跑来问我同一个问题:用 MMDetection 训练好的实例分割模型,比如 Mask R-CNN 或者 Cascade Mask R-CNN,在 PyTorch 里测精度一切正常,但是一到导出 ONNX 就翻车。有的报错直接抛出来,有的模型导出成功了但 ONNX Runtime 加载不了,有的加载能过但推理结果完全不对,还有的简直是薛定谔的报错——换个输入尺寸就崩。

先说结论:这个问题的根源,90% 不在“导出”这个动作本身,而在于你忽略了 MMDetection 模型结构和标准 PyTorch 模型之间的差异。剩下的 10%,是版本兼容性和自定义算子的处理方式问题。

这篇东西我会从环境配置、模型封装、算子兼容、输入输出对齐四个维度来拆解。不管你是刚把模型训完准备部署的算法工程师,还是被 MMDetection 坑了两天还没导出来的初学者,按着下面的步骤排查,大概率能在半小时内定位到问题。


2. 环境版本匹配:这是多数“导出失败”的第一杀手

2.1 版本错配为什么会导致导出崩溃

很多人在导出 ONNX 时遇到莫名其妙的错误,其实来源于版本之间 ABI 不兼容和算子注册表不匹配。MMDetection 是建立在 MMEngine 和 MMCV 之上的,而 MMCV 的编译版本又和 PyTorch 版本强绑定。一旦 MMCV 的编译版本跟你当前环境的 PyTorch 不一致,轻则无法加载模型权重,重则在导出过程中直接触发底层 C++ 异常。

我之前遇到一个典型的例子:某台机器上 PyTorch 是 1.13,但 MMCV 是用 2.0 的 wheel 装的,结果在torch.onnx.export时直接报cv2.error: OpenCV(4.7.0) ...,看起来像是图像处理库的问题,实际上底层是 MMCV 的 ROI align 自定义 CUDA 算子没有正确匹配 PyTorch 的算子注册表。

推荐的匹配组合大致如下(以稳定可用为优先):

工具推荐版本组合1推荐版本组合2说明
PyTorch1.13.12.0.11.x 和 2.x 导出的算子图略有差异
MMCV2.0.02.0.0建议用完整版而不是 lite
MMDetection3.0.03.0.03.x 系列 API 相对清晰
ONNX1.13.11.14.0太老或太新都有坑
ONNX Runtime1.14.01.16.0CPU 版本调试最方便

2.2 统一安装源的正确姿势

不要各装各的。MMDetection 3.x 依赖 MMCV 2.x,而 MMCV 2.x 不再像 1.x 那样提供一个mmcv-full全家桶,它的编译方式变化挺大。推荐的做法是直接用官方预编译包,同时严格锁定版本:

# 先确认自己的 CUDA 版本 python -c "import torch; print(torch.__version__, torch.version.cuda)" # 以 CUDA 11.8 为例,安装匹配的 MMCV pip install openmim mim install mmcv==2.0.0 # 再安装 mmdetection 3.0.0 pip install mmdet==3.0.0 # 检查环境匹配 python -c "from mmcv.ops import get_compiling_cuda_version, get_compiler_version; print(get_compiling_cuda_version()); print(get_compiler_version())"

注意:如果你本地的 PyTorch 是源码编译版本或者用了 nightly 版本,那 MMCV 的预编译包大概率不兼容,直接放弃预编译,改成源码编译。源码编译之前一定要设置好MAX_JOBS,否则机器内存小的话直接编译到 OOM。

2.3 一个小技巧:用独立的虚拟环境做导出

我最近一年养成的习惯:训练环境一个虚拟环境,导出和部署验证单独建一个虚拟环境。原因很简单,训练环境里往往装了一堆辅助库,比如albumentations、timm、einops之类,这种带有自定义autograd.Function的库在导出 ONNX 时偶尔会干扰算子追踪。独立环境里只装必要的依赖,导出成功率直线上升。

conda create -n onnx_export python=3.9 -y conda activate onnx_export pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu118 pip install mmcv==2.0.0 mmdet==3.0.0 onnx==1.13.1 onnxruntime==1.14.0

3. 导出前必须做好的三件事

3.1 理解 MMDetection 模型和标准 PyTorch 模型的差异

很多人直接在加载权重之后就把model扔给torch.onnx.export,然后开始怀疑人生。问题是 MMDetection 的model.forward()返回的是一个DetDataSample列表,包含了预测框、标签、分数和掩码,这跟 ONNX 导出期望的Tensor输出完全不一样。

此外,MMDetection 的模型内部还存在大量的控制流分支,比如if self.with_mask:、以及测试阶段的nms后处理逻辑。这些在 PyTorch 里都能正常跑,但torch.onnx.export遇到dict类型输出直接就会报错。

正确的做法:写一个WrapModel类,把推理路径上需要的部分单独提出来,只保留前向计算过程,手动指定输出张量。

import torch from mmdet.apis import init_detector class InstanceSegmentationONNXWrapper(torch.nn.Module): def __init__(self, model): super().__init__() self.model = model def forward(self, img): results = self.model.inference(img, batch_mode=False) # 根据你的需要提取 bboxes、labels、masks bboxes = results.pred_instances.bboxes labels = results.pred_instances.labels masks = results.pred_instances.masks return bboxes, labels, masks

3.2 正确设置动态轴和输入输出格式

实例分割模型输入一般是(N, 3, H, W),其中 H 和 W 在推理时往往会变化。导出时如果不设置动态轴,模型就被固定在了训练尺寸上,部署时换个分辨率就报维度不匹配。动态轴设置需要同时考虑images和masks两个对象:

torch.onnx.export( wrapper, example_input, "mask_rcnn.onnx", opset_version=11, input_names=["images"], output_names=["boxes", "labels", "masks"], dynamic_axes={ "images": {0: "batch_size", 2: "height", 3: "width"}, "boxes": {0: "num_boxes"}, "masks": {0: "num_boxes"}, }, )

这里有个容易忽略的点:opset_version不是越高越好。opset 11 用了很久,兼容性最好;opset 13 开始支持ReduceL1等新算子,但有些推理引擎支持不到位;opset 16 及以上对Resize算子的坐标变换模式改动很大,反而容易导出成功但推理结果跟 PyTorch 对不上。

3.3 关闭不需要的运行时组件

MMDetection 推理时默认会把数据预处理、归一化、NMS 全部揉进去。导出时建议把这些全部剪掉:

  • 设置model.eval()
  • 设置cfg.model.test_cfg里的nms保留,但score_thr可以先设为0.0,后续在后处理时控制
  • 关闭model.forward中不必要的show_result逻辑
  • 如果用了batch_mode=True,导出阶段强制改成False,避免引入额外的 padding 分支

实操案例:有一次我怎么导都报Expected all tensors to be on the same device,查了半天发现是 wrapper 里读取的结果labels是 CPU tensor,而bboxes是 CUDA tensor。统一套一个.to(device)就解决。


4. 两个最常见报错和四个解决套路

4.1 报错:TracerWarning: Output number 1 is a dict...

这个报错几乎只要导出 MMDetection 就一定会碰到,原因是模型输出的results.pred_instances是InstanceData对象,带有属性字典的特征。ONNX 追踪器只能接受Tensor、tuple、list等基本数据结构。

解决方案就是在 wrapper 里彻底拆开,明确返回(bboxes, labels, masks)三个 Tensor。如果你只需要 mask 结果做展示,bbox 和 labels 也必须保留输出,因为 mask 分支的计算图依赖它们共同的特征。

4.2 报错:Unsupported operator: onnx::RoIAlign

这个报错是实例分割模型特有的。Mask R-CNN 系列在 mask 分支之前有一个RoIAlign算子,它是 MMCV 里注册的自定义 CUDA 算子,PyTorch 原生的 ONNX 导出器不认识它。

处理方法有两种。第一种最简单粗暴:使用 MMDeploy 提供的自定义算子库,提前将自定义算子注册到 ONNX 命名空间:

pip install mmdeploy

然后在导出之前先调用:

import mmdeploy.apis.onnxruntime as ort_apis # 或者直接用 mmdeploy 自带的导出工具 python -m mmdeploy.tools.export \ configs/mmdet/instance-seg/instance-seg_onnxruntime_static.py \ checkpoints/mask_rcnn.pth \ demo.jpg \ --work-dir work_dir

第二种是手动写 ONNX 符号函数注册。以 RoIAlign 为例,就是把 MMCV 的自定义算子通过register_extra_symbolics映射到 ONNX 的标准 RoIAlign 上:

from mmcv.ops import roi_align import torch.onnx.symbolic_helper as sym_help def roi_align_symbolic(g, input, rois, spatial_scale, pooled_height, pooled_width, sampling_ratio, aligned): return g.op("com.microsoft::RoIAlign", input, rois, spatial_scale_f=spatial_scale, pooled_height_i=pooled_height, pooled_width_i=pooled_width, sampling_ratio_i=sampling_ratio, aligned_i=aligned) torch.onnx.register_custom_op_symbolic("mmcv::roi_align", roi_align_symbolic, 11)

注册完别忘了测一下 ONNX Runtime 是否支持这个算子。如果你用的是 CPU 版 ONNX Runtime,Microsoft 的com.microsoft::RoIAlign是支持的;如果是其他推理引擎,就要提前确认。

4.3 报错:Constant folding过程中出现维度断言失败

这种问题在动态尺寸导出时特别常见。因为 ONNX 导出时,torch.onnx会对部分常量进行折叠计算。如果模型中用了F.interpolate,且目标尺寸来自张量的shape,那么在追踪时得到的shape是一个符号值,编译器无法在编译期确定。于是它在折叠阶段尝试把这当作常量,然后维度断言就挂了。

解决套路:把F.interpolate改为torch.nn.functional.interpolate并传入具体尺度因子,或者在导出时固定输入尺寸(第一次先用固定尺寸导通,再改动态尺寸)。注意如果你的 mask 分支里有paste_mask之类的操作,逻辑类似,也要处理。

4.4 报错:ONNX Runtime: Got invalid dimension for input images

模型导出成功了,但用 ONNX Runtime 推理时输入维度对不上。这种情况一般是训练时用的batch_size=2,而导出的 ONNX 把 batch 维固定成了 2。如果你没有设置动态轴,或者设置了dynamic_axes但没有把boxes、masks的第一个维度关联上,就会这样。

我建议导出完先做一个本地快速验证:

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("mask_rcnn.onnx", providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name # 模拟一张 800x1200 的输入 dummy = np.random.rand(1, 3, 800, 1200).astype(np.float32) outputs = sess.run(None, {input_name: dummy}) for out in outputs: print(out.shape)

这一步能一次性暴露维度、类型、算子支持三大类问题。

4.5 实操总结表

现象根因处理方案
导出时TracerWarning且输出是 dictMMDetection 返回值是DetDataSample写 wrapper,返回多个 Tensor
报RoIAlign不支持自定义 CUDA 算子未注册用 MMDeploy 或手动注册符号
导出成功但推理维度错误dynamic_axes 设置不完整补全 boxes/masks 的维度映射
推理结果全零或全黑输入数据没有做归一化检查图像预处理是否和训练一致

5. 导出成功后依然不能用的三个深层原因

5.1 Mask 后处理导致的图结构损坏

ONNX 导出成功的模型,在 ONNX Runtime 上可能出现输出尺寸正常、但 mask 内容完全不对的状况。我排查过多次,根因是实例分割模型中的mask分支输出的是 logits,而不是二值掩码。在 PyTorch 推理阶段,MMDetection 内部会对 mask logits 做 sigmoid 处理,然后阈值化得到最终掩码。如果在导出时没有把这个操作放到计算图里,那 ONNX 的输出就是裸的 logits,后处理时看起来就是一个全灰或者全黑的图。

把 sigmoid 和阈值化加进 wrapper:

def forward(self, img): results = self.model.inference(img, batch_mode=False) masks = results.pred_instances.masks masks = torch.sigmoid(masks) masks = (masks > 0.5).float() return results.pred_instances.bboxes, results.pred_instances.labels, masks

5.2 预处理参数不对齐导致精度塌方

这也是一个高频问题。MMDetection 训练时对图像做了 normalize,用的是 ImageNet 的 mean 和 std。部署时如果用 ONNX Runtime 直接喂原始图像,数值范围都没对上,推理结果自然崩。正确做法是在导出前把归一化参数固化进模型的预处理模块,或者在自己的推理代码里对齐:

mean = np.array([123.675, 116.28, 103.53], dtype=np.float32) std = np.array([58.395, 57.12, 57.375], dtype=np.float32) img = img.astype(np.float32) for i in range(3): img[..., i] = (img[..., i] - mean[i]) / std[i]

5.3 输出坐标和原图的映射关系

一张 800x1200 的图,模型内部的尺度是 800x1200,但如果你的部署端输入做了 resize,输出框的坐标也跟着变了。MMDetection 测试时的test_cfg里有一个rcnn后处理参数,这个参数会影响最终框和 mask 的尺度。导出时如果使用了batch_mode=False但内部仍然有rescale逻辑,那输出框坐标和原图对不上。

在部署端最简单的做法:输入图像直接 resize 到模型需要的尺寸,不要保留原图长宽比,这样输出坐标就是输入图像坐标系,省去反向映射的麻烦。当然这会牺牲一点精度,但部署逻辑简单不少。


6. 覆盖场景更广的替代路线

如果你的模型结构比较新,比如用了Mask2Former这种 Transformer 架构,或者加了额外的注意力模块,导出 ONNX 的难度会大不少。这时候不建议继续死磕torch.onnx.export,而是可以考虑以下两条路线。

第一条是用 MMDeploy 的完整导出工具链。它对 MMDetection 3.x 常见模型的适配比较完善,自动处理了 roi align、sigmoid 后处理、NMS 导出等底层细节,支持导出到 ONNX 后继续转 TensorRT。如果你的部署环境能用 TensorRT,这条路的性能和稳定性都会更好。

第二条是如果目标推理端不强制 ONNX,直接用 PyTorch 的 TorchScript 导出,配合 LibTorch 做 C++ 推理。TorchScript 对 MMDetection 这类动态结构模型的兼容性比 ONNX 好不少,因为它的图 IR 保留了更多的控制流信息。缺点是 LibTorch 部署体积比较大,而且不是所有移动端平台都支持。

我实际经验是:如果目标平台是服务端 GPU,优先走 MMDeploy + TensorRT;如果是 CPU 推理或者边缘盒子,ONNX + ONNX Runtime 够用;如果是移动端,要么接受 ONNX 转 NCNN 的算子丢失风险,要么重写一个轻量的 mask 后处理。


7. 现场实录:一次完整的排查过程

做一个典型的案例还原。背景:Mask R-CNN,MMDetection 3.0 + MMCV 2.0,训练完成,精度 mAP 37。导出时第一步就报错——torch.onnx.export直接抛了TracerWarning: Output #0 is a dict。

这个阶段很多人会以为只是 warning,忽略掉继续跑,然后卡在后面的报错。正确的处理是马上中断,先写 wrapper。

我写了一个最简单的 wrapper:

class SimpleWrapper(torch.nn.Module): def __init__(self, model): super().__init__() self.model = model self.model.eval() def forward(self, img): data = dict(inputs=[img], data_samples=None) result = self.model.inference(data, batch_mode=False) return result.pred_instances.bboxes, result.pred_instances.labels, result.pred_instances.masks

然后在导出脚本里加载权重:

model = init_detector(config_path, checkpoint_path, device="cuda:0") wrapper = SimpleWrapper(model) example_input = torch.rand(1, 3, 800, 1200).cuda() torch.onnx.export(wrapper, example_input, "mask_rcnn_v1.onnx", opset_version=11)

这次导出通过了,但用 ONNX Runtime 加载时直接报Type Error: Unsupported operator ...。定位了一下,是 mask 分支的paste_mask操作变成了 ONNX 不支持的TensorIterator。解决方法是把 mask 后处理从模型里拆出来,在 wrapper 里用 Python 写死:

def forward(self, img): data = dict(inputs=[img], data_samples=None) result = self.model.inference(data, batch_mode=False) bboxes = result.pred_instances.bboxes labels = result.pred_instances.labels mask_logits = result.pred_instances.masks # 把 mask logits 直接输出,不做后处理 return bboxes, labels, mask_logits

这样导出的 ONNX 就只包含骨干网络、RPN、RoIAlign 和 mask 分类头,后处理全部放到部署端代码里。运行时只在 CPU 上做 sigmoid 和阈值化,速度损失可以忽略不计。

最后一步验证输出形状:

result = sess.run(None, {input_name: np.random.rand(1, 3, 800, 1200).astype(np.float32)}) for r in result: print(r.shape)

输出结果正常。这个模型后续在 ONNX Runtime 上 CPU 推理单帧耗时约 420ms,比 PyTorch 的 GPU 推理慢不少,但稳定性和可部署性基本达标。


8. 常见问题速查表

报错/现象原因解决办法
Output #0 is a dict没写 wrapper,直接导出原模型写一个返回多个 Tensor 的包装类
Unsupported operator RoIAlign自定义算子未注册用 MMDeploy 或手动注册 ONNX 符号
推理结果全黑mask logits 未做 sigmoid在 wrapper 里加 sigmoid + 阈值化
帧率极低后处理逻辑被导进了 ONNX把后处理拆出去
动态输入报维度错误dynamic_axes 没设全完整设置四个维度的映射
换一台机器就报 CUDA 错误环境版本不一致用虚拟环境固定依赖版本

9. 关于这个踩坑过程,最后说几句

我自己前前后后踩了两轮这种导出坑。第一轮是不熟悉 MMDetection 的数据流结构,照着文档写了个静态输入的导出脚本,结果导出成功但运行时完全不能用。第二轮是环境版本没对齐,浪费了整整一个下午去排查一个其实是 MMCV 编译版本不匹配的问题。

后来总结下来,最省时间的做法反而是“先看数据流,再写导出脚本”。先理清楚模型输入到底要什么格式、输出会返回什么结构,再动手写 wrapper。很多人在 PyTorch 里跑得好好的,就对模型内部结构毫无戒备,导出时候才被各种隐藏逻辑绊倒。希望这篇记录能让你少走一点我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询