简介:面向计算机视觉研究与工程开发者的快速目标分割(FastSAM)完整实现包,针对图像与视频中任意目标分割的实时性与精度平衡问题,适合具备一定深度学习基础、需要快速落地分割算法的中高级开发者。压缩包共53个文件,约39.52MB,涵盖Python源码、预训练模型配置(yaml)、示例图片(png/jpg)、说明文档(md/pdf)及许可证等,其中推理、解码、提示模块划分清晰,便于按需复用与二次开发。已有532人学习下载。资源内不仅提供FastSAM核心算法实现与Gradio交互演示,还附带多场景示例图片及异常检测、显著性目标等效果展示,可帮助读者快速复现分割流程,理解动态阈值与轻量网络优化策略;配合代码与文档,能直接用于自动驾驶、视频监控、医疗影像等场景的算法选型与定制改造。
1. 快速目标分割是什么:不是又一个 SAM,而是把 SAM 压到能跑生产的速度
做视觉检测落地的人大概都有同感:SAM 出来的时候效果惊艳,但真拿到产线上用,单张图几百毫秒到一秒多的推理速度,直接让“交互式分割”变成了“演示级功能”。工业场景里我们要的是快速目标分割,一张图进来几十毫秒出掩码,要么接机械臂抓取,要么做质检 ROI 提取,没有人会对着屏幕慢慢点 prompt。FastSAM 就是冲着这个矛盾来的——它把 SAM 的 Transformer 主体换成了 YOLOv8-seg 的卷积结构,用传统目标检测的流程先找框、再对每个框生成掩码,在 CPU 上能做到每秒几张图,GPU 上配合 TensorRT 能跑到实时。
这个方案能解决什么问题?简单说,就是把“分割”从离线分析工具变成在线视觉系统的第一环。适合三类人:一是做工业质检想快速圈出缺陷区域的,二是做边缘设备视觉方案需要目标分割但不具备大显存条件的,三是刚接触分割模型、想在本地快速跑通效果的。这篇文章围绕 fastsam c++ tensorrt 这条主线,把模型原理、导出流程、C++ 工程化落地和调参避坑完整过一遍,目标是让读者跟着做完后,能自己把 FastSAM 部署到实际项目里。
2. FastSAM 的模型结构与推理路径:为什么它比 SAM 快这么多
2.1 从 SAM 到 FastSAM:把 prompt 分割改成实例分割
理解 FastSAM 之前,先要清楚 SAM 的完整链路。SAM 需要两个输入:一张图像和一个 prompt(点、框或掩码)。图像先过 ViT 编码器生成 image embedding,prompt 再过 prompt encoder,两者在 mask decoder 里融合,输出多个掩码和对应的 confidence。这个链条中,ViT 的全局注意力是大算力消耗点,一张 1024x1024 的图像嵌入计算在普通 GPU 上就要几百毫秒。
FastSAM 的设计思路是彻底绕开 prompt 这个交互过程。它用 YOLOv8-seg 的 C2f 卷积结构做 backbone,直接输出一组候选框、每个框的类别和对应的掩码系数。推理时只需输入图像,输出就包含“哪个位置有东西、是什么类别、长什么样”的完整信息。本质上,FastSAM 是一个实例分割模型,而不是交互式分割模型。这意味着它是为批处理、全自动管线设计的,不是给人在图上点点的。
这里有一个关键差异值得注意:FastSAM 输出的掩码质量不如 ViT 编码器加 mask decoder 的组合精细。尤其在小目标或遮挡严重的区域,FastSAM 的掩码边缘会出现锯齿或空洞。但它带来的收益是推理链路从两段式变成单段式,没有 prompt 编码环节,也没有迭代 refinement,计算图短了,工程化难度也随之下降。用在大分辨率图或视频流上,这个取舍是划算的。因为实例分割输出可以直接接下游的 ROI 提取、缺陷统计、目标计数,不需要人为参与。
2.2 输出格式拆解:不要把检测框和掩码数组搞混
FastSAM 的输出其实就是 YOLOv8-seg 的典型输出:一组检测结果加一组随框附带的掩码。具体结构为:对于每个检测到的目标,输出包含 box 坐标(x1, y1, x2, y2)、置信度分数、类别 ID,以及一个由掩码系数和 prototype mask 线性组合得到的二进制掩码。
在 Python 端使用 Ultralytics 推理时,返回的结果对象包含boxes和masks两个属性。masks是二值化后的掩码,每个目标的掩码大小与原始图像尺寸相同(如果设置了retina_masks=True),或者是固定的 640x640(默认)。这里容易踩坑的是:掩码数组是布尔类型,不是浮点数概率图,拿去存成 PNG 时要注意类型转换。
C++ 端处理输出的逻辑要区分两个阶段。第一阶段是网络输出的原始张量,形状为[1, 总候选数, 4 + 1 + 类别数 + 掩码系数数],这是未经过 NMS 的密集预测。第二阶段是后处理后得到的最终结果。TensorRT 推理时,你拿到的是第一个阶段的原始输出,需要在 C++ 里自己完成 NMS、掩码系数与 prototype 的矩阵乘法、以及阈值过滤。这不难,但必须提前设计好内存布局,否则后处理的耗时会超过网络本身。
常见做法是定义两个输出张量:一个用于检测头输出(box、score、class、mask coefficients),一个用于 prototype masks(形状为[1, 32, 160, 160]),后者要与掩码系数做矩阵乘法生成最终掩码。prototype 数量和掩码系数维度是网络超参,默认配置下通常为 32 维。这个数字如果记错,后处理时的矩阵维度就会对不上,程序会直接崩溃。
3. 本地跑通 FastSAM:从安装到导出 ONNX 的最小实验
3.1 用 Python 先验证模型效果,再谈部署
先把环境搭起来。我一般用一个干净的 conda 环境,不跟其他项目混装依赖。Python 版本建议选 3.9 或 3.10,Ultralytics 对这两个版本支持最稳。安装命令如下。
conda create -n fastsam_demo python=3.10 -y conda activate fastsam_demo pip install ultralytics opencv-python onnx onnxsim安装完成后,写一个最小推理脚本验证模型能否正常工作。Ultralytics 库内部已经集成了 FastSAM 的支持,不需要额外改装。
from ultralytics import FastSAM from ultralytics.models.fastsam import FastSAMPrompt import cv2 model = FastSAM("FastSAM-s.pt") img = cv2.imread("demo.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = model(img, device="cuda:0", retina_masks=True, conf=0.4, iou=0.9) prompt_process = FastSAMPrompt(img, results) anns = prompt_process.everything_prompt()这段代码里,FastSAM-s.pt是小型权重,适合先验证流程。everything_prompt()的含义很直接:不提供任何 prompt,让模型全图输出所有检测到的目标的掩码。conf=0.4是置信度阈值,低于这个值的检测框会被丢弃;iou=0.9是 NMS 的 IoU 阈值,值越大保留的重复框越多,掩码覆盖会更完整但也会更杂。如果发现图上的目标被漏检,先把conf调低到 0.25 试试;如果发现同一物体上堆叠了多个掩码,把iou调低到 0.7 或 0.5 会好一些。
这一步的目的一是确认模型在你的机器上能跑,二是观察不同阈值对输出的影响。记录下你在验证集上认为“可用”的那组conf和iou值,后面 C++ 部署时要保持一致的阈值逻辑,否则两边效果会对不上。
3.2 导出 ONNX:把 PyTorch 模型变成部署中间格式
FastSAM 是 PyTorch 模型,生产环境通常不会直接用 PyTorch 跑推理。常见做法是先导出 ONNX,再由 ONNX 转成 TensorRT 引擎。Ultralytics 提供了导出接口,命令如下。
yolo export model=FastSAM-s.pt format=onnx opset=12 simplify=True这里的opset值得注意。TensorRT 8.x 对 ONNX opset 的兼容范围一般是 11 到 17,opset=12 兼容性最好,既不缺少新算子,也不会因为版本过高导致部分老算子被错误映射。simplify=True会调用 onnx-simplifier 对计算图做常量折叠和冗余节点删除。这一步对后续 TensorRT 转换帮助很大,能减少转换时的未支持算子数量和显存占用。
导出完成后,使用 onnxruntime 做一次基准验证,确认导出的模型和 PyTorch 原模型输出一致。
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("FastSAM-s.onnx", providers=["CUDAExecutionProvider"]) input_name = sess.get_inputs()[0].name input_shape = sess.get_inputs()[0].shape print(input_name, input_shape) dummy_input = np.random.randn(1, 3, 640, 640).astype(np.float32) outputs = sess.run(None, {input_name: dummy_input}) for i, out in enumerate(outputs): print(f"output {i}: shape {out.shape}")这段代码的作用有两个。一是确认导出后的输入输出名称和形状,这在后续 TensorRT 的 C++ 绑定中要直接使用。二是用随机张量跑一次推理,验证模型在 ONNX Runtime 下不报错。注意这里输出张量会有多个,取决于导出时的配置。FastSAM 的 ONNX 输出包含检测分支和掩码分支,具体数量和名称可以在打印结果中确认,每个引擎都不一定相同,所以把这一步的打印信息保存下来,后面 C++ 代码里需要写死这些名字和维度。
3.3 导出时的常见失败:opset 和动态尺寸怎么选
导出过程不会总是顺利。常见一个问题:导出时报Unsupported operator错误。解决办法是修改 opset 版本,或者升级 ultralytics 到最新版,因为新版本会使用更新版本的算子。如果升级后错误仍存在,检查模型文件中是否有自定义算子。FastSAM 基于 YOLOv8-seg,理论上没有自定义算子,出现这个错误大概率是 opset 版本太低。
另一个高频问题是动态尺寸。导出时默认的输入是[1, 3, 640, 640]固定尺寸。如果你的业务图分辨率多变,可以尝试导出动态 H、W,命令加dynamic=True。但我的建议是,在 TensorRT 部署阶段不要用动态尺寸,除非你的业务确实需要。动态尺寸会让 TensorRT 在推理时做输入尺寸切换,优化器会为多个 shape 生成多个优化方案,显存占用上升,推理速度下降。固定 640x640 输入,预处理阶段用 letterbox 把图像等比缩放到这个尺寸,是业界最稳的做法,也是 FastSAM 在训练时就使用的尺寸。
4. TensorRT 部署 FastSAM 的工程化路径:C++ 推理与后处理
4.1 从 ONNX 到 TensorRT 引擎:trtexec 参数与显存控制
在 C++ 工程中集成 TensorRT 前,先把 ONNX 转成 TensorRT 引擎文件(通常后缀.engine)。命令行转换和代码转换都可行,工程期我会先用trtexec把参数验证清楚,再写进 C++ 代码。命令如下。
trtexec --onnx=FastSAM-s.onnx \ --saveEngine=FastSAM-s.engine \ --fp16 \ --minShapes=images:1x3x640x640 \ --optShapes=images:1x3x640x640 \ --maxShapes=images:1x3x640x640这段命令里--fp16是关键。TensorRT 开启 FP16 后,在多数 NVIDIA 显卡上能让推理速度翻倍以上。但注意,FP16 对某些算子的精度有影响,如果后处理时发现掩码质量明显下降,可以去掉--fp16跑一版 FP32 引擎对比。生产环境通常的做法是两种精度都生成,做一次批量测试来决定。
--minShapes/--optShapes/--maxShapes三个参数用于固定输入形状。由于这里都填了1x3x640x640,TensorRT 不会为动态尺寸做任何额外优化,引擎体积更小,加载更快。如果你的显存有限(比如 4GB 以下),可以考虑在转换时加上--workspace=2048限制工作区大小,避免转换过程显存溢出。
转换过程中输出大量日志,重点看最后几行的[Detected 1 inputs and 2 outputs]以及Engine generated in ...。如果转换失败,基本是 ONNX 导出时的问题,比如算子不兼容、opset 过旧,回头重新导出即可。这个步骤的目标是得到一个固定输入、固定输出的.engine文件,后续 C++ 推理不再需要任何 ONNX 相关的库。
4.2 C++ 加载引擎:一个能直接抄的推理类骨架
TensorRT C++ API 读引擎并做推理,核心代码量其实不大。下面是一个最小可用的类骨架,包含引擎加载、输入输出绑定和单张图推理的完整流程。
#include <fstream> #include <vector> #include <cuda_runtime_api.h> #include <NvInfer.h> using namespace nvinfer1; class FastSAMInferencer { public: bool loadEngine(const std::string& enginePath) { std::ifstream file(enginePath, std::ios::binary); file.seekg(0, std::ios::end); size_t size = file.tellg(); file.seekg(0, std::ios::beg); std::vector<char> blob(size); file.read(blob.data(), size); file.close(); runtime = createInferRuntime(gLogger); engine = runtime->deserializeCudaEngine(blob.data(), size, nullptr); context = engine->createExecutionContext(); return engine != nullptr; } bool inference(const float* input, size_t inputSize, std::vector<float*>& outputs, const std::vector<size_t>& outputSizes) { // 分配 GPU 显存 void* buffers[3]; cudaMalloc(&buffers[0], inputSize * sizeof(float)); cudaMemcpy(buffers[0], input, inputSize * sizeof(float), cudaMemcpyHostToDevice); for (int i = 0; i < outputs.size(); ++i) { cudaMalloc(&buffers[i + 1], outputSizes[i] * sizeof(float)); } // 推理 context->setTensorAddress("images", buffers[0]); context->setTensorAddress("output0", buffers[1]); context->setTensorAddress("output1", buffers[2]); context->enqueueV3(stream); // 拷贝回主机 cudaMemcpy(outputs[0], buffers[1], outputSizes[0] * sizeof(float), cudaMemcpyDeviceToHost); cudaMemcpy(outputs[1], buffers[2], outputSizes[1] * sizeof(float), cudaMemcpyDeviceToHost); return true; } private: IRuntime* runtime; ICudaEngine* engine; IExecutionContext* context; cudaStream_t stream; };这段代码使用的setTensorAddress是 TensorRT 8.5 之后推荐的绑定方式,相比旧的enqueue加setBinding方式更直观。需要特别留意的是"images"、"output0"、"output1"这几个名称,它们必须与引擎中实际名称完全一致。名称可以在导出 ONNX 时打印,也可以用engine->getIOTensorName(i)遍历得到。代码中没有处理错误释放,实际项目中 GPU 显存和 stream 都要在析构函数中释放,否则长时间运行会累计显存碎片。
4.3 预处理与后处理:letterbox 与掩码重建是正确率的分水岭
预处理阶段,图像需要经过 letterbox 等比缩放后填充到 640x640,然后做归一化。这一步直接影响检出率,如果直接用cv::resize把非 640x640 的图拉成 640x640,会让目标比例失真,小目标检测率明显下降。
void letterbox(const cv::Mat& src, cv::Mat& dst, int targetSize = 640) { float scale = std::min(targetSize * 1.0f / src.cols, targetSize * 1.0f / src.rows); int newW = round(src.cols * scale); int newH = round(src.rows * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH), 0, 0, cv::INTER_LINEAR); dst = cv::Mat(targetSize, targetSize, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(dst(cv::Rect((targetSize - newW) / 2, (targetSize - newH) / 2, newW, newH))); }114, 114, 114是 YOLO 系列训练时使用的填充色,不要随意改。缩放插值用INTER_LINEAR即可,不需要换更高级的插值。归一化方面,常见做法是把像素减去 0 再除以 255,或者使用 ImageNet 均值方差。FastSAM 是基于 COCO 训练的,训练时用的是简单的除以 255,不涉及 ImageNet 归一化。如果你用了 ImageNet 均值方差去做预处理,模型输出会完全乱掉。
后处理需要重建掩码。网络输出的 raw mask 不是一张可以直接用的二值图,需要将检测头的掩码系数与 prototype mask 做矩阵乘法。流程为:第一步,对检测头的输出做置信度过滤;第二步,对剩余框做 NMS;第三步,将过滤后的框对应的掩码系数与 prototype 矩阵相乘;第四步,用 sigmoid 激活且阈值化,并恢复到原始图像坐标。最后一步的坐标映射最容易被忽略。letterbox 在图上加了边距,后处理时掩码坐标必须减去边距偏移并除以缩放系数,才能对齐到原始图像。如果漏了这一步,掩码位置会整体偏移,看起来像检测框和分割区域错位。
5. FastSAM 部署避坑与性能排查:TenosrRT 场景下最容易翻车的五个问题
5.1 引擎加载成功后推理结果全零?先查输入 buffer 的名称和维度
现象:TensorRT 引擎加载无报错,但推理后输出数值全部为 0 或接近 0。
原因:输入张量的名称绑定错误,或者输入 buffer 的维度与引擎期望不一致。TensorRT 8.5 之后,如果某个张量绑定地址为 null 或尺寸不匹配,部分引擎会静默输出 0 而不是报错。
解决:在代码中遍历所有 I/O 张量名称并打印,确认"images"的实际名称。另外打印context->getTensorShape("images").d[i]检查维度是否为[1, 3, 640, 640]。如果维度中出现了动态值(如 -1),说明引擎没有完全固定 shape,需要回到 trtexec 阶段补全 min/opt/max shape 参数。
5.2 FP16 引擎掩码明显变差?不要盲目关掉 FP16
现象:同一模型 FP32 引擎掩码质量正常,FP16 引擎掩码出现大量空洞、边缘破碎、目标丢失。
原因:FP16 的精度范围有限,尤其是掩码分支中的 sigmoid 激活和矩阵乘法对精度敏感。某些目标的置信度恰好落在阈值附近时,FP16 引擎的输出可能会波动。
解决:先用性能测试确认 FP16 的实际收益。有些 GPU(如 Tesla T4、RTX 3090)对卷积算子的 FP16 加速明显,但掩码重建涉及的矩阵乘法未必快很多。我的做法是保留两版引擎:视觉验证阶段用 FP32,正式跑稳定流程时用 FP16 并做全量回归测试。如果回归测试的掩码质量差异在可接受范围内,才最终切换 FP16。
5.3 C++ 后处理掩码坐标偏移:忘了 letterbox 的边距
现象:检测框位置准确,但掩码区域整体向右下偏移,偏移量随目标在图像中的位置变化。
原因:网络输入是 letterbox 后的 640x640 图,网络输出的掩码坐标对应的是这张填充后的图。后处理时直接按 640 尺寸解析掩码,没有把边距和缩放比例换算回原图坐标,导致坐标错位。
解决:后处理阶段记录 letterbox 产生的参数:缩放比例 scale 和填充偏移 padX、padY。掩码的每个像素坐标映射公式为origX = (maskX - padX) / scale,origY = (maskY - padY) / scale。这一步不是可选项,是必须做的,否则下游 ROI 提取拿到的区域是错的。
5.4 推理耗时远超预期:先看 NMS 是不是没实现好
现象:GPU 利用率正常但整体耗时高,单张图推理 40ms,后处理却要 80ms。
原因:TensorRT 只负责网络推理,后处理在 CPU 上完成。如果掩码重建直接用循环逐像素处理,或者 NMS 用三层嵌套循环写 O(n^3) 复杂度,后处理时间会轻松超过网络推理时间。
解决:把 NMS 改用向量化的方式,或者直接用 TensorRT 的 EfficientNMS 插件在 GPU 上完成。掩码重建用矩阵乘法而不是逐像素循环。C++ 中可以用cv::Mat::mul配合 precomputed 系数一次性完成,效率远高于遍历像素。对于 640x640 输入,优化好的后处理应该控制在 5ms 以内。
5.5 多线程推理时显存不足或线程崩溃
现象:开了 4 个线程同时跑推理,程序报显存分配失败或 cudaErrorIllegalAddress,最后进程崩溃。
原因:每个线程独立创建了IExecutionContext,但没有共享ICudaEngine。TensorRT 的 engine 是线程安全的,可以多线程共享,execution context 是线程独立的。显存不足往往是因为每个 context 都重复分配了输入输出缓冲,或者没有正确释放旧的 buffer。
解决:全局只加载一个 engine,每个线程创建独立的IExecutionContext和对应的显存缓冲。上下文的数量需要控制在显存允许范围内。对于 640x640 输入,每个 context 大约占用 200~500MB 显存,根据显卡容量计算上限。线程数不要盲目跟 CPU 核数保持一致,以 GPU 利用率曲线为准,一般 2~4 个线程即可把消费级 GPU 跑满。
6. 性能调优与验证技巧:用 TensorRT 把推理推到实时之后该做什么
模型跑通只是第一步,把性能调到稳定可用的状态需要几轮验证。以一张 1080p 输入图为基准,我的调优步骤是:先量化网络耗时,再量化后处理耗时,最后优化整体管线。网络耗时用cudaEvent前后打点测量,后处理耗时用std::chrono测量。两者分开统计,不要混在一起看总帧率,否则无法定位瓶颈在哪。
如果网络耗时高,优先看显卡型号和 FP16 是否开启。在 RTX 3060 上,FP16 开启后 FastSAM-s 的网络推理时间大约能从 40ms 降到 18ms 上下。FP32 转 FP16 不掉点的情况下,这一步收益最大。如果后处理耗时长,优先把掩码重建改成 GPU 计算。使用 TensorRT 的 INetworkDefinition 把掩码重建部分也放进引擎里,或者使用 CUDA kernel 实现 sigmoid 加阈值化,降低 CPU 侧的处理压力。
验证阶段建议准备三类测试图。第一类是标准场景图,验证模型基准精度;第二类是含大量小目标的密集场景图,验证 NMS 的阈值是否合适;第三类是边缘情况,比如全黑图、过曝图、纯色图,确保后处理不会因为候选框为 0 或候选框过多而崩溃。每类图至少 50 张,统计掩码的平均 IoU 和置信度分布,对比 PyTorch 原模型的输出。这里我的习惯是保存一份 Python 端的标准输出作为 golden,C++ 端把输出存成相同的格式后逐像素比对,误差在 1% 以内就算通过。
还有一个经常被忽略的环节是显存复用。输入输出的 buffer 不要在每帧推理时重新分配,而是在初始化阶段一次性分配并复用。这个改动看似小,但对稳定运行的影响很大。帧率图上如果出现周期性掉帧,大概率是显存分配导致的延迟。最后,我个人的习惯是保留一份 trtexec 转换命令的脚本和 PyTorch 推理脚本,每次换显卡或换模型版本时,完整跑一遍从验证到 C++ 集成的流程,而不是只改一个路径参数。换设备后引擎必须重新生成,旧设备的 engine 文件在另一张卡上并不能直接复用。
FastSAM 的工程化难度相对 SAM 已经低了一个级别,但它的精度上限也摆在那里。小目标、遮挡和复杂边缘场景下,它仍然会出现掩码不完整的问题,这一点需要在使用前就跟需求方说清楚。希望这份部署笔记能帮你少走一些弯路,少踩一些我在 TensorRT 集成时踩过的坑。
本文还有配套的精品资源,点击获取