资讯动态

SAM3 C++部署实战:基于ONNX Runtime实现三种提示词分割

发布时间:2026/8/26 8:32:40 来源:尧图企业网站定制
简介图像分割是计算机视觉中的基础任务传统模型受限于固定类别而SAM系列模型通过提示词机制实现了任意目标的分割能力。在实际工程落地中如何将这类模型高效集成到C服务端是一个常见挑战。ONNX Runtime作为跨平台推理引擎以其轻量、高性能、多硬件支持等优势成为替代LibTorch的优选方案。本文从部署视角出发介绍SAM3模型的ONNX导出、C环境搭建、文本/点/框三种提示词的张量构造与坐标映射以及动态输入、后处理、并发优化等关键细节。内容涵盖从原理到代码的完整链路帮助开发者在纯C环境中快速跑通SAM3推理流程实现自然语言、鼠标点击或目标框驱动的高精度分割能力。 最近在折腾图像分割这块被SAM3的提示词玩法吸引住了。刚好手头有个C项目要把分割能力做成服务端接口不能指望Python环境于是顺着“SAM3 C ONNX Runtime”这条路把推理流程完整捋了一遍。整个过程踩了不少坑但搞通之后再用文本、点、框去指定“要分割什么”体验真的不一样。如果你也需要在C里跑SAM系列模型或者正被提示词编码、动态输入、后处理这些细节卡住这篇内容应该能帮你省下不少时间。下面我就按实际动手的顺序从方案选型到环境搭建再到三种提示词的实现和常见坑位完整拆一遍。1. 整体方案与设计思路1.1 为什么选SAM3而不是其他分割模型传统语义分割模型是固定类别训练时有什么标签推理时只能出什么标签。你不可能让一个只学过“人、车、路”的模型突然给你分一只猫。SAM系列把这件事变成了“分割一切”你给我一个提示词不管是点、是框、还是文本描述我都能在任意图像里把目标抠出来。SAM3相比前代最大的变化是文本提示的使用被提到了和点、框同等的位置不再是实验性的附加功能这也让“用自然语言控制分割”真正能落地到图像理解流程里。从部署角度看SAM3虽然内部结构比普通分割模型复杂但它一样可以导出成ONNX在端侧和服务端都能跑。ONNX格式的好处在于它把模型的计算图、权重、输入输出定义都打包好目标平台只需要一个Runtime就能加载不需要依赖PyTorch全家桶。对C服务来说这意味着环境干净、启动快、依赖少也更方便和现有业务模块集成。1.2 为什么用ONNX Runtime而不是LibTorch如果你的项目已经重度使用PyTorch且服务器资源不敏感用LibTorch无可厚非。但实际部署中我更推荐ONNX Runtime理由很简单第一ONNX Runtime的体积比LibTorch小不少二进制分发特别友好第二它对CPU和GPU都做了大量图优化比如算子融合、内存复用性能不一定比PyTorch差第三它在Windows、Linux、macOS、Android、iOS上都有统一接口一套代码到处编译。尤其当你要做纯C服务时LibTorch会用到的libtorch库动辄几个GB而ONNX Runtime动态库通常几十MB到一百多MB差异非常明显。再加上SAM3这类模型往往包含Vision Transformer、Mask Decoder、文本编码器结构复杂用ONNX Runtime直接推理不需要感知模型的内部细节只要把输入输出张量处理好剩下的交给Runtime。1.3 C承载整个推理管线的优势为什么非要用C而不是继续用Python写个HTTP服务因为我这里的分割能力不是独立工具而是要嵌进一个已有C框架里的底层模块后续要接到音视频流处理、实时交互等场景。C可以直接管理线程、显存、CPU内存还能利用零拷贝接口把图像数据直接喂给模型减少不必要的拷贝。对延迟敏感的应用来说这些优化是Python比较难做到的。所以整体架构就是用Python或其他工具从SAM3导出ONNX模型然后用C写推理管线最终对外暴露一个简单接口输入图像、提示词类型、提示词内容输出mask。下面我会把这个流程一步步拆开讲。2. 部署前准备模型准备与工程搭建2.1 获取与导出SAM3 ONNX模型标题里说的SAM3目前没有官方统一命名的正式发布多数是对应社区复现或内部实验版本。但无论模型叫什么能不能导出ONNX取决于模型实现里是否包含动态控制流、自定义算子等障碍。如果你手上有PyTorch权重导出过程一般分两步import torch from your_sam3_model import Sam3Model model Sam3Model.from_pretrained(your_checkpoint) model.eval() dummy_image torch.randn(1, 3, 1024, 1024) dummy_point_coords torch.zeros(1, 5, 2) dummy_point_labels torch.zeros(1, 5, dtypetorch.int64) dummy_text_tokens torch.zeros(1, 77, dtypetorch.int64) torch.onnx.export( model, (dummy_image, dummy_point_coords, dummy_point_labels, dummy_text_tokens), sam3.onnx, input_names[image, point_coords, point_labels, text_tokens], output_names[masks, iou_predictions], dynamic_axes{ image: {2: height, 3: width}, point_coords: {1: num_points}, point_labels: {1: num_points}, text_tokens: {1: text_len}, masks: {2: mask_height, 3: mask_width}, }, opset_version17 )这里必须强调ONNX导出时尽量把batch维度固定为1因为SAM模型的prompt编码器对batch维度比较敏感。后续输入组织都按单样本处理请求并发用多会话或多线程来处理即可。导出完成后建议先用Python ONNX Runtime做一次推理确认onnx模型输入输出正确再进入C开发。否则问题混在一起很难定位是模型导出问题还是C调用问题。2.2 C工程依赖与构建环境C侧需要准备的核心依赖就两个ONNX Runtime C库OpenCV用于图像读取、resize、Mat转TensorONNX Runtime的C库可以从官方GitHub Release里下载对应的zip包里面有include目录和动态库。Windows下是onnxruntime.dllLinux下是libonnxruntime.so。注意确认是CPU还是GPU版本GPU版本还要带上CUDA、cuDNN相关依赖。项目结构我习惯这样组织sam3_onnx/ ├── CMakeLists.txt ├── third_party/ │ ├── onnxruntime/ │ └── opencv/ ├── src/ │ ├── sam3_segmentor.h │ ├── sam3_segmentor.cpp │ ├── main.cpp └── models/ └── sam3.onnxCMakeLists.txt里需要引入ONNX Runtime和OpenCV。ONNX Runtime没有提供标准的CMake config一般手动指定头文件路径和库路径cmake_minimum_required(VERSION 3.16) project(sam3_onnx) set(CMAKE_CXX_STANDARD 17) set(ORT_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/onnxruntime/include) set(ORT_LIB_DIR ${CMAKE_SOURCE_DIR}/third_party/onnxruntime/lib) set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/third_party/opencv/lib/cmake/opencv4) find_package(OpenCV REQUIRED) add_executable(sam3_demo src/main.cpp src/sam3_segmentor.cpp) target_include_directories(sam3_demo PRIVATE ${ORT_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS}) target_link_directories(sam3_demo PRIVATE ${ORT_LIB_DIR}) target_link_libraries(sam3_demo PRIVATE onnxruntime ${OpenCV_LIBS})如果你用的是GPU版ONNX Runtime还需要链接额外库同时注意运行时把CUDA相关dll放到可执行文件目录下。2.3 一个最小的分割类设计我把分割能力封装成Sam3Segmentor对外只暴露三个方法bool loadModel(const std::string modelPath); cv::Mat segment(const cv::Mat image, const PromptData prompt);其中PromptData是一个struct用来统一表达三种提示词struct PromptData { enum PromptType { TEXT, POINT, BOX, TEXT_AND_BOX }; PromptType type POINT; std::string text; std::vectorcv::Point2f points; std::vectorint labels; // 1 前景, 0 背景 cv::Rect2f box; cv::Mat mask; // 当前帧的mask用于后续多轮修正 };这个设计的好处是不管上游调用方给的是文本、点还是框最终都转化成同一种内部表示再走同一个推理函数。后面如果要做多轮交互只需要把上一轮mask或新提示点叠加进PromptData代码改动很小。3. 核心实现三种提示词的处理与推理3.1 理解SAM3的输入输出结构拿到导出的ONNX模型后先用Python打印一下输入输出确认具体shape。通常SAM类模型的输入包括image: float32 [1,3,H,W]图像经过归一化和resizepoint_coords: float32 [1,N,2]点坐标point_labels: float32或int64 [1,N]点语义标签text_tokens: int64 [1,L]文本token idsorigin_image_size: int64 [2]原始图像高宽用于把mask映射回原图输出一般是masks: float32 [1,1,H,W] 或 [1,M,H,W]iou_predictions: float32 [1,M]如果你的SAM3模型文本编码是单独前处理好的embedding那么输入可能直接多一个text_embedding: float32 [1,D]这种情况下你需要在C里单独调用一个文本编码模型或者把文本编码也放进同一个ONNX图。我建议尽量把文本token输入放进SAM3模型里这样C侧只需要处理tokenizer不需要再维护一个embedding模型。3.2 文本提示词的tokenizer与张量构造这是C实现里最容易被忽略的部分。SAM3内部一般会加载一个CLIP或者BERT类的文本编码器而ONNX输入通常是整型的token ids不是自然字符串。所以C侧必须实现一个轻量tokenizer把“a red apple”这种输入转成token id数组。一个简单做法是直接复用HuggingFace FastTokenizer的ONNX版本或者用SentencePiece的C库。如果你希望工程里少点第三方依赖还可以写一个最小词表查询表把常见单词和标点映射到固定id。注意文本长度必须固定到模型支持的max length一般是77。不足部分用0填充超出部分截断。示例代码std::vectorint64_t tokenize(const std::string text, int max_len) { std::vectorint64_t tokens(max_len, 0); // 这里简化处理按空格拆分查词表 std::istringstream iss(text); std::string word; int idx 0; while (iss word idx max_len) { auto it vocab.find(word); tokens[idx] (it ! vocab.end()) ? it-second : vocab[[UNK]]; } tokens[max_len - 1] vocab[[EOS]]; return tokens; }这个tokenzier要求词表里有[CLS],[SEP],[PAD],[UNK]等特殊token。实际项目中建议直接用完整的BPE分词否则文本提示词的泛化能力会明显下降。文本输入构造好之后维度应该是{1, 77}类型是int64。ONNX Runtime里用Ort::Value::CreateTensor传入。这里要特别注意ONNX Runtime对int64的输入必须把数据放到std::vectorint64_t中不能简单用int数组。3.3 点与框提示词的坐标映射点提示和框提示最坑的地方在坐标映射。图像输入模型前通常会被resize到1024x1024那么用户在原图上选的点也必须按相同的比例映射到1024x1024空间。否则模型拿到的是错位坐标分割结果自然完全不对。我的做法是记录原始图像尺寸和resize后的尺寸计算缩放比例float scale_x 1024.0f / orig_cols; float scale_y 1024.0f / orig_rows; std::vectorfloat mapped_points; for (const auto p : prompt.points) { mapped_points.push_back(p.x * scale_x); mapped_points.push_back(p.y * scale_y); }如果模型还会做长边缩放加padding那么坐标映射还要额外减去padding偏移。这块建议在导出模型时就不要内嵌resize和padding逻辑而是把图像原图输入模型或者在预处理时把坐标变换写死保持C侧可控。框提示往往用矩形的左上角和右下角两个点表示。在SAM系列里这两个点和点提示共存时label分别赋值2和3代表左上角和右下角。即使模型只给了框通常也要拆成两个点坐标输入。这一点很容易漏导致框提示没有生效。一个完整的框转点逻辑大概是这样float x1 prompt.box.x * scale_x; float y1 prompt.box.y * scale_y; float x2 (prompt.box.x prompt.box.width) * scale_x; float y2 (prompt.box.y prompt.box.height) * scale_y; std::vectorfloat coords {x1, y1, x2, y2}; std::vectorint64_t labels {2, 3};如果同时有多个正负点也是类似地拼成一个二维数组N表示总点数。3.4 ONNX Runtime会话创建与推理C端加载模型和创建会话是性能关键点。推荐的会话配置如下Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Env env(ORT_LOGGING_LEVEL_WARNING, sam3); Ort::Session session(env, modelPath.c_str(), session_options);SetGraphOptimizationLevel建议设为ORT_ENABLE_ALLRuntime会尽可能融合算子。SetIntraOpNumThreads要根据CPU核数和并发请求数微调并不是越大越好超过一定线程数反而会因线程切换产生额外开销。推理时需要把OpenCV的Mat转成浮点Tensor并做归一化。SAM系列通常要求像素值归一化到0~1且通道排列为RGB如果OpenCV读出来是BGR还要先cvtColor转成RGB。下面是输入Tensor构造和Run调用的简化代码std::vectorint64_t image_shape {1, 3, 1024, 1024}; Ort::Value image_tensor Ort::Value::CreateTensorfloat( memory_info, image_data.data(), image_data.size(), image_shape.data(), image_shape.size()); std::vectorint64_t coords_shape {1, num_points, 2}; Ort::Value coords_tensor Ort::Value::CreateTensorfloat( memory_info, coords_data.data(), coords_data.size(), coords_shape.data(), coords_shape.size()); std::vectorint64_t labels_shape {1, num_points}; Ort::Value labels_tensor Ort::Value::CreateTensorint64_t( memory_info, labels_data.data(), labels_data.size(), labels_shape.data(), labels_shape.size()); std::vectorint64_t text_shape {1, text_len}; Ort::Value text_tensor Ort::Value::CreateTensorint64_t( memory_info, text_tokens.data(), text_tokens.size(), text_shape.data(), text_shape.size()); std::vectorOrt::Value inputs; inputs.push_back(std::move(image_tensor)); inputs.push_back(std::move(coords_tensor)); inputs.push_back(std::move(labels_tensor)); inputs.push_back(std::move(text_tensor)); std::vectorconst char* input_names {image, point_coords, point_labels, text_tokens}; std::vectorconst char* output_names {masks, iou_predictions}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names.data(), inputs.data(), inputs.size(), output_names.data(), output_names.size());session.Run返回的是std::vectorOrt::Value里面的Tensor可以直接用GetTensorMutableDataT()拿到数据指针。注意不要在Run调用前把输入vector释放掉input_names需要指向以\0结尾的字符串。3.5 后处理从mask logit到二值掩码模型输出的masks通常是浮点logits范围不在0~1。后处理要做两步先做Sigmoid再根据阈值二值化。阈值一般取0.0因为Sigmoid(0)0.5正好是中间值你也可以根据实际效果微调。Sigmoid计算很简单float* mask_data outputs[0].GetTensorMutableDatafloat(); int64_t mask_size outputs[0].GetTensorTypeAndShapeInfo().GetElementCount(); std::vectorcv::Mat channels; // 把mask_data重新组织成多通道Mat再逐像素做sigmoid for (int64_t i 0; i mask_size; i) { float val mask_data[i]; mask_data[i] 1.0f / (1.0f std::exp(-val)); }如果输出mask尺寸是1024x1024而原图不是这个尺寸还需要用cv::resize把mask映射回原图尺寸。如果是框提示还可以顺带用cv::boundingRect从mask上提取目标框返回给外部使用。注意输出mask是单通道符合CV_32FC1二值化后建议转成CV_8UC1方便后续做轮廓检测或透明抠图。4. 性能优化与工程落地注意点4.1 图像编码器与提示编码器解耦SAM3这类模型很重但如果你的应用是“同一张图多次换提示词”最优做法是把图像编码单独跑一次把中间图像特征缓存起来。提示词每次只影响提示编码部分和mask decoder部分不需要重新过一遍ViT主干。具体做法是把ONNX分成两个模型或者利用ONNX Runtime的partial graph能力只从某个中间节点开始运行。如果导出的模型是完整的图你也可以在导出时拆开。拆分的标准用法是图像编码模型输入image输出image_embedding提示mask解码模型输入image_embedding、point_coords、point_labels、text_tokens输出masks这样同一张图多次交互时图像编码只跑一次推理延迟可以从几百毫秒降到几十毫秒。在C里封装时就可以用两个Ort::Session去做。4.2 动态shape与内存复用ONNX Runtime对动态shape是支持的但每次输入大小变化可能会导致Runtime重新分配内存或走不同优化路径。建议在服务初始化时就固定一个最大分辨率比如1024x1024所有输入图像都resize到固定尺寸。这样虽然增加了一些预处理时间但胜在稳定。另外ONNX Runtime允许复用输入输出Tensor的缓冲区。在C里可以预先分配好std::vectorfloat并循环使用避免每次推理都重新new。Ort::Value::CreateTensor在传入外部数据指针时不会管理这个指针的内存所以只要确保外层vector的存活时间覆盖整个推理周期即可。4.3 并发请求与线程安全如果你的服务要同时处理多个请求不能简单地在同一个Ort::Session上并发调用Run。ONNX Runtime的Session对象不是完全线程安全的更稳妥的方案是维护一个Session池每个请求从池里取一个空闲Session使用。也可以用多个ONNX Runtime环境每个线程一个会话。虽然内存占用会翻倍但避免了锁竞争。实际操作中我常用ThreadLocal来保存线程私有的Session这样既安全又能在高并发下获得不错的吞吐量。代价就是模型权重会被加载多份内存会多出几个GB看你能接受多少。4.4 CPU与GPU推理的性能权衡如果图像分辨率大、模型结构复杂CPU推理可能每帧要2到5秒这对实时交互不可接受。GPU版本在显存足够的情况下单帧可以压到100毫秒以内。C侧切换GPU很简单只需要在创建SessionOptions时追加CUDA provider选项OrtCUDAProviderOptions cuda_options; session_options.AppendExecutionProvider_CUDA(cuda_options);注意GPU推理的显存占用通常比PyTorch低一些因为ONNX Runtime会做显存复用。但也要留意Ort::Session的创建耗时首次加载时会触发CUDA上下文初始化时间可能较长建议在服务启动时预加载不要放到请求里去创建。5. 常见问题与解法5.1 输入维度对不上典型报错是shape mismatch或者input size is different。多半是因为动态轴没有设置对或者在C里构造Tensor时shape写错。排查方法很简单先用Pythononnxruntime跑一遍打印每个输入的真实shape再对照C里的std::vectorint64_t。特别是point_coords的最后一维2很容易在初始化时少写。还需要注意ONNX Runtime的输入顺序必须和模型导出时定义的input_names一致。session.Run里的输入name数组必须严格匹配模型里input_names的排序。很多坑都出在模型输入顺序是image, point_coords, point_labels, text_tokens但C里传成image, point_labels, point_coords, text_tokens导致数据错位。5.2 文本提示词几乎不生效如果文本提示词加进去后mask和纯点提示没区别基本可以确定tokenizer有问题。比如没有正确添加[CLS]起始token或者text_tokens的pad位置乱填了一个不在词表里的token id。另外文本长度必须固定到模型训练时的长度如果你填77但模型实际期望是128结果也会错乱。有一个调试技巧在Python里用和C完全相同的tokenizer去编码同一段文本比对生成的token ids是否一致。不一致就逐位找差异一般能快速定位。特别要小心单词大小写和空格处理很多tokenizer是区分大小写的。5.3 mask输出位置整体偏移或尺寸不对这通常和坐标映射有关。你在原图上点的位置经过缩放后没有正确映射到模型输入空间。常见错误是把原图先resize到1024时没有保留长宽比而是直接拉成正方形。SAM系列一般是在原图保持长宽比的情况下padding到正方形所以坐标映射必须相应做padding偏移。建议在预处理时写一个统一的letterbox函数记录scale和pad_x、pad_y坐标映射用mapped_x orig_x * scale pad_x; mapped_y orig_y * scale pad_y;后处理时再用同样的参数把mask裁剪回原图区域。这样就能消除偏移问题。5.4 运行速度太慢CPU占用却不高如果CPU占用不高但速度慢大概率是ONNX Runtime没用满线程或者模型里有太多小算子导致线程等待。先看SetIntraOpNumThreads是否设置合理再看有没有设置SetExecutionMode(ExecutionMode::ORT_PARALLEL)。另外把session_options.SetGraphOptimizationLevel开到最大很多情况下能把推理耗时缩短20%以上。如果你用的是GPU版本发现GPU利用率不高则要检查图像预处理是否变成了瓶颈。OpenCV的resize和cvtColor本身也吃CPU可以在多线程里做预处理或者改用IPP加速版OpenCV。5.5 部署到Linux后找不到onnxruntime动态库Windows上编译通过Linux运行时提示找不到libonnxruntime.so这是典型的动态库搜索路径问题。解决方式有两种# 临时指定 export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/your/path/onnxruntime/lib # 或在CMake时设置RPATH set_target_properties(sam3_demo PROPERTIES BUILD_RPATH /your/path/onnxruntime/lib)更推荐在启动脚本里统一设置LD_LIBRARY_PATH因为RPATH在面对不同版本库时更容易出问题。6. 实测经验一套可复用的提示词组合策略最后再分享一个实际项目里摸索出来的小技巧。不要把三种提示词当成互斥选项它们可以组合使用。比如用户输入“画面中的红色水杯”同时用检测框圈一个候选区域模型通常会同时参考文本和框的信息分割稳定度高很多。这在交互式分割里非常实用。我在C接口里做了一版组合PromptData文本给空字符串时只用点和框文本非空时自动拼上文本token。实测下来文本描述有歧义时加一个点提示能大幅减少误分割。比如“右边那个人”这种带位置信息的描述如果只给文本模型没法知道右边是屏幕右边还是图像语义右边但如果传一个点基本就能锁定目标。调用时大致流程是获取图像和用户输入。如果用户有框则框内裁剪区域作为参考可选做一次检测预处理。将所有提示词转成内部PromptData。调用segment接口返回mask。可选择基于mask再做边缘平滑。这块的完整代码量不小但核心逻辑就是前面讲的坐标映射和Tensor构造。把每一步拆开测先单点、再单框、再单文本最后组合起来问题定位会清晰很多。我个人在实际项目中最深的体会是SAM3的C部署难度不在模型本身而在提示词的处理细节上。文本tokenizer、坐标映射、shape对齐、动态轴设置每一个环节都可能让分割结果变得毫无意义。但只要把这些细节做成可测试的模块后续维护和扩展就会非常舒服。如果你也准备用C接SAM3建议先把Python侧的各个输入输出打印清楚再动手写C能少走一大半弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价