
简介面向算法工程师与部署开发者提供一套以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把SAMSegment Anything Model完整跑起来。它不是一份讲原理的PPT而是一套能编译、能出图、能接点提示和框提示的实战源码包。适合三类人被Python推理速度卡住、想摆脱Python运行时、或者要在工业设备上集成分割能力的开发者。我拆完这套项目最大的感受是部署SAM的坑不在模型本身而在格式转换和预处理对齐这两层下面按我复现的顺序把细节和踩坑都摊开讲。2. 选型与工程骨架为什么是ONNXOpenVINOC以及项目目录怎么读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或WindowsC侧依赖OpenVINO Runtime和CMakePython侧依赖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 EncoderViT把图像编码成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), boxesNone, masks(mask_input[0] if has_mask_input[0] 0 else None) ) low_res_masks, iou_predictions self.mask_decoder( image_embeddingsimage_embedding, image_peself.prompt_encoder.get_dense_pe(), sparse_prompt_embeddingssparse_embed, dense_prompt_embeddingsdense_embed, multimask_outputFalse, ) 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_version17, dynamic_axes{ point_coords: {1: num_points}, point_labels: {1: num_points}, } )这段代码有三个值得注意的地方。第一导出时喂进去的样例输入shape必须和C侧完全一致尤其是point_coords的第二维这里开的动态轴允许一次传入多个提示点。第二opset_version不要低于13OpenVINO对低版本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而不是老式moOpenVINO对ONNX的转换入口现在统一推荐用ovcOpenVINO 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再执行cmakeWindows上则是用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.datafloat()[0] px * scale_x; coords.datafloat()[1] py * scale_y; ov::Tensor labels(ov::element::f32, {1, 1}); labels.datafloat()[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左上角 - 前景 1box右下角 - 背景 0因为我导出的ONNX里point_coords开了dynamic_axes输入张量的第二维从1改成2不会报错C侧直接重设tensor shape即可。建议把单点和单框都做成可配置项argv传参就行test_app的main.cpp里就是这样组织的。5. 部署避坑五个必须提前知道的坑5.1 模型转换后输出全为0或全为常数现象同样一张test_image.jpgPyTorch跑出的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_widthscale_y 1024.0 / src_height把原图坐标乘上比例后再送入decoderfinal 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。解决彻底卸载旧版本只保留一个OpenVINOLinux用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跑同一个提示最后用下面这个快速脚本对比两边的maskimport 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(fIOU {iou:.4f}) # 判断标准单点提示下IOU应高于0.95为什么说单点提示下IOU要足够高因为SAM的decoder在确定性推理关闭multimask且不做随机采样时输出是确定的FP16带来的误差通常在千分之一量级。如果你复现时IOU掉到0.8以下那基本可以断定是预处理或坐标变换出了问题而不是模型量化的问题。精度确认通过后我还会固定test_image.jpg和同一个提示词跑一遍重复10次的推理检查每次输出是否一致排除OpenVINO在CPU上偶尔出现的非确定性问题。从那以后我每接到一个部署项目都会强制自己先花半小时把“Python基准结果”和“部署结果”的对比脚本搭好再开始调性能。这份SAM部署资源帮我验证了一件事大模型落地最难的不是把模型导出去而是让导出去的模型行为和原来一模一样。把这条对比基准线守住整个部署过程的进度就完全可控了希望帮到你。本文还有配套的精品资源点击获取