简介:面向算法工程师与部署开发者,提供一套以ONNX、OpenVINO和C++为技术栈的SAM分割万物模型部署实战工程。覆盖模型导出、格式转换、推理加速到本地应用调用的完整链路,源码与教程配套,适合想掌握深度学习模型工程化落地的中高级开发人群,解决从PyTorch模型到高性能本地应用之间的转化难题。
资源共23个文件,含4个C++源文件、5个头文件、4个Python脚本、7个txt说明,以及测试图像和README指南,压缩包仅2.22MB,结构紧凑,便于定位导出脚本、推理实现和CMake构建配置。已有221人学习下载。
实际收获包括:可直接复用的C++部署模板、ONNX导出与OpenVINO优化参数的具体调法、SAM任意分割的交互调用示例,以及CPU/GPU环境运行注意事项。项目内含推理执行器与测试应用模块,配合教程从环境安装、依赖准备到跑通示例的完整拆解,能有效减少试错成本,加速分割类项目工程化落地。
1. 算法部署不只是把模型喂给引擎:一份把SAM落进C++生产环境的完整链路
做图像分割的人这两年应该都被SAM刷过屏,但真正动手把它接进C++工程的人不多。这份资源解决的就是这件事:用ONNX做模型中间格式,用OpenVINO做推理优化和硬件调度,最后用C++把SAM(Segment Anything Model)完整跑起来。它不是一份讲原理的PPT,而是一套能编译、能出图、能接点提示和框提示的实战源码包。适合三类人:被Python推理速度卡住、想摆脱Python运行时、或者要在工业设备上集成分割能力的开发者。我拆完这套项目最大的感受是,部署SAM的坑不在模型本身,而在格式转换和预处理对齐这两层,下面按我复现的顺序把细节和踩坑都摊开讲。
2. 选型与工程骨架:为什么是ONNX+OpenVINO+C++,以及项目目录怎么读
2.1 三个技术栈各管哪一段
先把这个组合的职责边界理清楚。ONNX在这里不是部署终点,而是“模型交换格式”。PyTorch训练好的SAM权重不能直接给C++用,先通过torch.onnx.export导出成ONNX,相当于把模型结构、算子和权重打包成一份跨框架的标准描述。这一步解决的是“模型能不能被C++侧加载”的问题。
OpenVINO解决的是“加载之后怎么跑得快”。OpenVINO的Model Optimizer(或者新版叫ovc)会把ONNX解析并重新优化成IR格式(.xml权重图 + .bin权重数据),再通过推理引擎调度到CPU、集成GPU或VPU等硬件上。它对CPU的优化尤其明显,能在不损失精度的情况下把推理速度拉上一个台阶。
C++解决的是“怎么接进真实系统”。Python脚本跑推理适合验证,但到了产线、嵌入式设备或者需要和现有C++视觉管线合并的场景,C++是绕不开的选择。OpenVINO本身提供C++ API,所以C++侧可以直接用ov::Core、ov::CompiledModel做加载与推理,不需要启动任何Python子进程。
2.2 项目目录文件职责与准备环境
拿到压缩包解压之后,目录结构是这样的(核心文件保留原名):
data/ test_image.jpg # 测试输入图 original.png # 原始PyTorch SAM跑出的分割结果,用于对比 docs/ # 流程教程文档 cpp/ CMakeLists.txt # C++工程构建脚本 vino_executor/ # 基于OpenVINO的SAM执行器 cppsam/ # 采样、mask解码相关封装 test_app/ # 演示用的可执行入口 python/ utils.py # 图像预处理、坐标变换等工具 run_original_sam.py # 用PyTorch跑原始SAM,生成基准结果 export_model.py # 导出ONNX的脚本 requirements.txt # Python侧依赖 ONNXSam.py # SAM的ONNX版封装,供验证和对比 README.md我第一次拿到这份目录时觉得文件有点散,但实际捋下来逻辑是顺的:python目录负责“模型从PyTorch到ONNX”这一步,cpp目录负责“ONNX到OpenVINO再到C++推理”这一步,data里的original.png是专门留下来做精度对比的基准图。文档建议先跑通Python侧的导出和验证,再切换到C++,这个顺序我自己复现下来也是最低风险的路径。
2.3 环境检查与依赖安装
C++部署最怕环境漂移,所以先把软硬件前提钉死。这个项目面向的是x86_64平台上的Linux或Windows,C++侧依赖OpenVINO Runtime和CMake,Python侧依赖torch、onnx、onnxruntime、opencv-python、numpy。建议在干净的虚拟环境里先装Python依赖:
python -m venv sam_deploy_env source sam_deploy_env/bin/activate # Windows下为 sam_deploy_env\Scripts\activate pip install -r requirements.txtrequirements.txt里锁的是onnxruntime、opencv-python这类推理和图像处理库。装完后跑一句环境自检,确认torch能加载、onnxruntime能调用:
python -c "import torch, onnxruntime, cv2; print('torch', torch.__version__); print('ort', onnxruntime.__version__)"这一步不是走形式。我遇到过torch装成了CPU版、onnxruntime装错platform的情况,导出的ONNX在Python侧验证正常,到了C++侧行为异常,查了半天才发现是环境不一致。C++侧的环境检查也提前做:确认OpenVINO的CMake包能被找到,后面第4章的CMakeLists会直接用到这个find_package。
3. 模型导出:PyTorch权重转ONNX的关键配置
3.1 SAM模型结构复杂,导出时要拆开处理
SAM不是单一模型,而是三个组件的组合:Image Encoder(ViT,把图像编码成embedding)、Prompt Encoder(编码点/框/掩码提示)、Mask Decoder(解码出分割掩码)。导出时如果整个端到端导出,不仅输入输出接口复杂,C++侧还要处理多组动态维度,很难调。
项目里采用的做法是拆分导出:把图像编码器导成一个ONNX(输入是预处理后的1024×1024图像,输出是图像embedding),把Prompt Encoder + Mask Decoder这组导成另一个ONNX。这样C++侧可以先编码图像得到embedding,之后多次传入不同提示词,只需要跑轻量的decoder部分,不用反复推理重backbone。这个拆分思路在SAM工程部署里几乎是必选项。
3.2 export_model.py里的核心导出方式
ONNXSam.py是SAM的ONNX版封装,export_model.py负责实际的导出。核心逻辑是做一个包装模块,把SAM前向过程中“图像编码”和“掩码解码”分别暴露出来,然后调用torch.onnx.export。掩码解码部分导出时的关键代码大致长这样:
import torch import torch.nn as nn from ONNXSam import build_onnx_sam class MaskDecoderWrapper(nn.Module): def __init__(self, sam_model): super().__init__() self.prompt_encoder = sam_model.prompt_encoder self.mask_decoder = sam_model.mask_decoder def forward( self, image_embedding, # [B, 256, 64, 64] 图像特征 point_coords, # [B, N, 2] 提示点坐标(1024尺度) point_labels, # [B, N] 1表示前景,0表示背景 mask_input, # [B, 1, 256, 256] 上一轮掩码或全零 has_mask_input, # [B, ] 是否输入掩码 orig_size, # 原图尺寸 transform_matrix # 1024尺度与原图的坐标变换矩阵 ): sparse_embed, dense_embed = self.prompt_encoder( points=(point_coords, point_labels), boxes=None, masks=(mask_input[0] if has_mask_input[0] > 0 else None) ) low_res_masks, iou_predictions = self.mask_decoder( image_embeddings=image_embedding, image_pe=self.prompt_encoder.get_dense_pe(), sparse_prompt_embeddings=sparse_embed, dense_prompt_embeddings=dense_embed, multimask_output=False, ) return low_res_masks, iou_predictions model = build_onnx_sam("checkpoint.pt") wrapped = MaskDecoderWrapper(model) torch.onnx.export( wrapped, (torch.randn(1, 256, 64, 64), torch.randn(1, 1, 2), torch.randint(0, 2, (1, 1)).float(), torch.zeros(1, 1, 256, 256), torch.ones(1, 1), torch.tensor([512, 512]), torch.eye(3)[:2, :].unsqueeze(0)), "mask_decoder.onnx", input_names=[ "image_embedding", "point_coords", "point_labels", "mask_input", "has_mask_input", "orig_size", "transform_matrix" ], output_names=["low_res_masks", "iou_predictions"], opset_version=17, dynamic_axes={ "point_coords": {1: "num_points"}, "point_labels": {1: "num_points"}, } )这段代码有三个值得注意的地方。第一,导出时喂进去的样例输入shape必须和C++侧完全一致,尤其是point_coords的第二维,这里开的动态轴,允许一次传入多个提示点。第二,opset_version不要低于13,OpenVINO对低版本opset的支持虽然还行,但个别算子(比如aten::index、aten::where)在旧opset下容易转换失败,我一般直接用17。第三,mask_input和has_mask_input这两个占位输入不能省略,MaskDecoder的forward里会用到它们做迭代式mask refinement,即使第一次调用用不上,接口也得对齐。
3.3 导出完先做两项验证,别急着转IR
导出完成后直接转OpenVINO是有风险的,因为你不知道ONNX里的算子是不是都被正确trace下来。项目文档里也埋了这一手:Python目录下保留了ONNXSam.py和run_original_sam.py,就是让你在Python侧用onnxruntime对导出的ONNX先跑一遍。我复现时候的顺序是:
python run_original_sam.py --image data/test_image.jpg --point 320 240 "a dog" python utils.py --verify-onnx --onnx image_encoder.onnx --onnx mask_decoder.onnx --image data/test_image.jpg第一行是跑原始PyTorch版的SAM,生成original.png基准结果。第二行是用onnxruntime加载刚导出的两个ONNX,做同样的推理。验证内容有两个:输出张量的shape是否一致(image embedding必须是[1,256,64,64]),以及mask解码结果和PyTorch版本在肉眼上是否一致。这一步通过之后再进OpenVINO,能省掉后面大量排查时间。
4. 用OpenVINO转IR并搭建C++推理应用
4.1 ONNX转IR:用ovc而不是老式mo
OpenVINO对ONNX的转换入口,现在统一推荐用ovc(OpenVINO Converter),老教程里常写的mo.py在较新版本里已经逐步退场。转换命令很简单:
ovc image_encoder.onnx -o openvino_ir/ --compress_to_fp16 ovc mask_decoder.onnx -o openvino_ir/ --compress_to_fp16--compress_to_fp16这个参数值得说明:它会把权重从FP32压到FP16,模型体积减半,在CPU和GPU上通常只有极小的精度波动。SAM这种大模型(ViT-B/H系列)转IR之后体积明显减小,加载和初始化时间也更快。如果转换后出现精度异常,先把FP16关掉重转一次对比,这能帮你快速定位是量化精度问题还是算子转换问题。转换成功后openvino_ir目录下会生成.xml和.bin两个文件,C++侧加载时指向.xml即可。
4.2 C++工程组织:CMakeLists.txt的写法
cpp目录下的工程组织是按可维护性设计的:vino_executor目录里是封装好的SAM执行器,cppsam目录里是坐标变换、mask后处理这类辅助逻辑,test_app目录里是演示程序。CMakeLists.txt核心部分这样写:
cmake_minimum_required(VERSION 3.16) project(sam_vino_deploy LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # OpenVINO Runtime库 find_package(OpenVINO REQUIRED) # Sam执行器 add_library(vino_executor STATIC vino_executor/vino_executor.cpp ) target_link_libraries(vino_executor PUBLIC openvino::runtime) target_include_directories(vino_executor PUBLIC vino_executor) # 演示应用 add_executable(sam_test_app test_app/main.cpp cppsam/sam_utils.cpp ) target_link_libraries(sam_test_app PRIVATE vino_executor openvino::runtime)find_package(OpenVINO REQUIRED)是核心,它依赖OpenVINO环境变量设置正确。我在Linux上习惯这样配置:安装OpenVINO Runtime后,运行source /opt/intel/openvino/setupvars.sh再执行cmake,Windows上则是用openvino_env.bat设置环境。如果find_package找不到,八成不是CMakeLists的问题,而是OpenVINO环境没激活。
4.3 C++侧推理流程:加载、输入组织、输出解析
C++推理的核心流程写在vino_executor.cpp里,逻辑是:初始化ov::Core,分别编译image_encoder和mask_decoder两个模型,然后按顺序推理。关键代码结构如下:
#include <openvino/openvino.hpp> #include <opencv2/opencv.hpp> class SamVinoExecutor { public: SamVinoExecutor(const std::string& encoder_xml, const std::string& decoder_xml) { ov::Core core; encoder_ = core.compile_model(encoder_xml, "CPU"); decoder_ = core.compile_model(decoder_xml, "CPU"); } void infer(const cv::Mat& image, float px, float py) { cv::Mat resized = preprocess(image); // 转1024x1024并归一化 ov::Tensor input_tensor(ov::element::f32, {1, 3, 1024, 1024}); // 将resized数据memcpy到input_tensor.data(),注意HWC->CHW auto infer_req = encoder_.create_infer_request(); infer_req.set_input_tensor(input_tensor); infer_req.infer(); auto embedding = infer_req.get_output_tensor(); // [1,256,64,64] // 把提示点坐标从原图变换到1024尺度 float scale_x = 1024.0f / image.cols; float scale_y = 1024.0f / image.rows; // 点提示坐标 (px, py) 对应为 processed_prompt_x = px * scale_x ... // 组装decoder输入:embedding + prompt + 占位mask_input ov::Tensor coords(ov::element::f32, {1, 1, 2}); coords.data<float>()[0] = px * scale_x; coords.data<float>()[1] = py * scale_y; ov::Tensor labels(ov::element::f32, {1, 1}); labels.data<float>()[0] = 1.0f; // 1=前景点 auto decoder_req = decoder_.create_infer_request(); decoder_req.set_tensor("image_embedding", embedding); decoder_req.set_tensor("point_coords", coords); decoder_req.set_tensor("point_labels", labels); // mask_input、has_mask_input用全零/0填充 decoder_req.infer(); auto masks_out = decoder_req.get_output_tensor("low_res_masks"); // [1,1,256,256] // 双线性插值到原图尺寸,再套阈值得到二值mask } private: ov::CompiledModel encoder_; ov::CompiledModel decoder_; };这段代码里有几个容易被忽略的点。第一,OpenVINO的输入tensor默认是NCHW布局,而OpenCV读出的图像是HWC,转置和归一化必须在拷贝时一并处理,忘了转布局会直接得到乱码mask。第二,提示点坐标必须从原图尺度映射到1024尺度,否则语义上完全错位。第三,decoder的输入tensor名字要和导出ONNX时的input_names严格一致,set_tensor("image_embedding", ...)这个名字对不上就会在运行时抛异常。
4.4 点提示和框提示的C++实现要点
项目里同时支持点提示和框提示,框提示的本质和点提示一致:把box的前景点和背景点组合成Sparse Prompt输入。SAM官方实现里,一个框会转化为两个点(两个角点),一个标为前景,一个标为背景。OpenVINO的C++实现可以复用同一个decoder模型,只是把point_coords张量从[1,1,2]改成[1,2,2],point_labels对应填[1,0]即可:
ov::Tensor coords(ov::element::f32, {1, 2, 2}); ov::Tensor labels(ov::element::f32, {1, 2}); // box左上角 -> 前景 1,box右下角 -> 背景 0因为我导出的ONNX里point_coords开了dynamic_axes,输入张量的第二维从1改成2不会报错,C++侧直接重设tensor shape即可。建议把单点和单框都做成可配置项,argv传参就行,test_app的main.cpp里就是这样组织的。
5. 部署避坑:五个必须提前知道的坑
5.1 模型转换后输出全为0或全为常数
现象:同样一张test_image.jpg,PyTorch跑出的mask正常,转成OpenVINO IR之后推理结果是一整块0或者一整块255。
原因:最常见是输入图像预处理不一致。PyTorch侧默认做了标准化(mean=[123.675, 116.28, 103.53],std=[58.395, 57.12, 57.375]),C++侧如果只做了简单的/255归一化,模型拿到的是完全不同的数值分布,分割结果自然报废。
解决:把预处理逻辑单独抽成函数,Python和C++共用同一套参数。我在vino_executor里把这个逻辑写死后,再也没出现过“转换后模型变傻”的情况。
5.2 提示点坐标没换算,分割结果完全错位
现象:点在图像左上角,分割区域却跑到右下角;或者框选的位置和分割结果完全对不上。
原因:SAM的prompt编码是在1024×1024尺度上计算的,而用户点的坐标是原图尺度(比如1280×720)。如果直接拿原图坐标送进prompt encoder,位置到模型里就会被当成大图上的点,语义完全错乱。
解决:推理前先算好scale_x = 1024.0 / src_width,scale_y = 1024.0 / src_height,把原图坐标乘上比例后再送入decoder,final mask再映射回原图尺寸。
5.3 推理速度比预期慢很多,只有几十毫秒却仍不达标
现象:image encoder在CPU上推理耗时400-500ms,整体管线跑不动。
原因:SAM的image encoder是ViT结构,计算量巨大。OpenVINO即使做了优化,纯CPU跑ViT-B的image encoder也不便宜。而且很多实现每改一次提示就重新推理整个encoder,浪费严重。
解决:把图像编码和提示解码拆开执行,只在图像变化时重新跑encoder,改点坐标只重跑轻量的decoder,耗时可以从几百毫秒降到几毫秒。
5.4 OpenCV与C++版本不匹配导致编译期报错
现象:cmake阶段提示找不到OpenVINOConfig.cmake,或者编译时一堆undefined reference。
原因:OpenVINO的Runtime库版本和安装方式不一致,常见是系统里装了多个版本,环境变量被旧版本抢先;Windows上还有个高发原因是OpenVINO的bin目录没加到PATH,运行时找不到openvino.dll。
解决:彻底卸载旧版本,只保留一个OpenVINO,Linux用setupvars.sh、Windows用openvino_env.bat重新激活终端;找包失败时可以直接指定OpenVINO_DIR环境变量指向安装目录,我在这上面至少折腾过三四回。
5.5 多提示词时shape没对齐,推理直接崩溃
现象:多传几个点提示后程序抛异常,报tensor shape mismatch。
原因:导出ONNX时dynamic_axes只开了point_coords和point_labels的num_points维度,但C++侧传了box之后又叠加了点,组合起来维度变换没走统一封装。
解决:所有提示都统一转成“点坐标张量+标签张量”,不要单独处理box逻辑,全部转换成点序列再送入decoder。
6. 端到端验证技巧:用test_image.jpg跑通全流程,确认精度没有悄悄丢失
部署完成的最后一公里,是证明C++跑出来的结果和PyTorch原始结果一致性够高。项目里的data/original.png就是为此准备的。我的做法是:先用run_original_sam.py在Python侧生成原始分割结果,再用sam_test_app跑同一个提示,最后用下面这个快速脚本对比两边的mask:
import cv2 import numpy as np mask_pt = cv2.imread("output/ref_mask.png", 0) > 127 mask_cpp = cv2.imread("output/vino_mask.png", 0) > 127 inter = np.logical_and(mask_pt, mask_cpp).sum() union = np.logical_or(mask_pt, mask_cpp).sum() iou = inter / union print(f"IOU = {iou:.4f}") # 判断标准:单点提示下IOU应高于0.95为什么说单点提示下IOU要足够高?因为SAM的decoder在确定性推理(关闭multimask且不做随机采样)时,输出是确定的,FP16带来的误差通常在千分之一量级。如果你复现时IOU掉到0.8以下,那基本可以断定是预处理或坐标变换出了问题,而不是模型量化的问题。精度确认通过后,我还会固定test_image.jpg和同一个提示词,跑一遍重复10次的推理,检查每次输出是否一致,排除OpenVINO在CPU上偶尔出现的非确定性问题。
从那以后我每接到一个部署项目,都会强制自己先花半小时把“Python基准结果”和“部署结果”的对比脚本搭好,再开始调性能。这份SAM部署资源帮我验证了一件事:大模型落地,最难的不是把模型导出去,而是让导出去的模型行为和原来一模一样。把这条对比基准线守住,整个部署过程的进度就完全可控了,希望帮到你。
本文还有配套的精品资源,点击获取