简介:快速目标分割(FastSAM)是一套面向计算机视觉与图像处理领域学习者的技术资源包,聚焦如何对任意目标进行像素级高效分割,在保持高精度的同时显著缩短推理时间,适合需要落地实时分割方案的研究人员、算法工程师及高年级学生。包内共 53 个文件,以 Python 源文件为主,涵盖模型搭建、解码器、提示处理等核心模块;同时附有大量 PNG/JPG 分割效果示例图,便于直观对比不同输入下的输出效果;另有 Markdown、PDF 文档说明、YAML/TXT 配置与依赖清单,压缩包总体积约 39.52MB。目前已有 532 人学习下载,可应用于自动驾驶、视频监控、医疗影像分析等典型场景。借助这份资源,读者不仅能从代码层面理解 FastSAM 的轻量化网络设计与算法流程,还可以利用配套的示例脚本与 Gradio 交互演示快速跑通推理。在此基础上,无论是二次开发、替换数据集,还是接入自有项目,都能获得直接的代码参考与可视化验证素材。
1. 快速目标分割是什么:一次前向拿到全部mask,凭什么说它快
做图像分割的人多少都体验过SAM的“慢”:一张图扔进去,先跑ViT编码器出image embedding,再靠点选或框选反复解码,单次交互虽然准,但想在视频流或大批量离线数据上用它筛选目标,算力根本扛不住。FastSAM的思路完全不同——它把SAM的“先编码、再按提示解码”改成“一次前向,把所有可能的实例mask全部吐出来”,核心是一套YOLOv8-seg改造的检测分割网络,所有候选目标一次性生成,再由你后续决定留谁。这个取舍让它在一张1080p图上用普通GPU能跑到几十毫秒级别,比SAM原生流程快一到两个数量级,同时也意味着对提示的使用方式变了:不再是“给个点才出mask”,而是“先给一堆mask,再按你的条件挑”。
适合读这篇文章的人,多半是手里攒着大量图片要自动抠目标、或者要在视频流里实时分割的从业者,而不是只想点几个点做交互标注的用户。FastSAM的价值不是替代SAM的精度,而是把“分割”从离线交互工具变成可批量、可实时、可做数据预标注的工程手段。下面我按自己实际部署的路径,从原理选型、代码落盘、mask后处理到推理加速,把整条链路讲透。
2. 为什么FastSAM能比SAM快那么多:把Transformer编码器换成CNN检测头
2.1 先看架构拆解:yield一切实例的策略到底省了什么
SAM的速度瓶颈在图像编码器:ViT-H/B对一张图做特征提取,本身就要几百毫秒到秒级,而且后续每次点选、框选都要重新走解码器。FastSAM把这条链路倒过来,先不做任何“语义理解”的编码,而是直接用CNN检测分割网络(基于YOLOv8-seg)把图上所有可能的实例区域找出来。
具体分三段:
- 骨干网络用YOLOv8-seg的Backbone,负责生成多尺度特征图;
- Neck用FPN/PAN结构把浅层细节和深层语义融合;
- Head同时输出三样东西:box坐标、实例置信度、以及每个实例对应的mask原型(prototype mask)。推理时对每个检测框生成一个mask,这一步叫“实例分割头”。
所以FastSAM的“快速”本质是:它把分割当作检测的附属产物,不单独做像素级全局编码。实战里最直观的感受是,不管图上有没有提示,你都会拿到N个候选mask,数量取决于检测阈值和NMS设置,而不是取决于你点了几个点。这种“yield all instances”的策略让它在“我不知道我要分割什么”的批量场景中特别好用。
2.2 提示的机制变了:everything模式、点选和文本筛选的实现原理
FastSAM虽然也支持点选和框选提示,但实现方式和SAM完全不一样。SAM的提示是在解码阶段介入,而FastSAM是在拿到全部候选mask之后做“过滤/重排”。我常用的三种方式:
- 无提示模式(everything):直接设置
model(image, retina_masks=True, conf=0.4, iou=0.9),拿到全部候选,适合批量预标注、相册分类、商品抠图这种“全部都要”的场景; - 点提示:给一个坐标点,FastSAM会计算点到每个候选mask的距离,把距离最近且置信度最高的那个mask选出来——注意它不会像SAM那样真的“生长”一个mask,而是在已有候选里做选择;
- 文本提示:配合CLIP,对每个候选mask裁剪后做CLIP文本相似度排序,选出匹配文本的目标。这块依赖CLIP权重文件,离线环境要提前准备好。
这三个模式对应的是不同的工程架构:点提示和文本提示实质上是后处理逻辑,不需要重新跑神经网络。这也是为什么FastSAM在“先全量分割,再按需求筛选”的场景里格外有价值——你可以把候选mask缓存下来,反复换提示词而不用重新推理。
2.3 选型边界:不是所有场景都适合FastSAM
如果你要做的是医学影像中单个微小病灶的精细分割,FastSAM的mask边缘会比SAM粗糙,因为原型mask的尺寸是固定的(原型分辨率通常只有128x128),上采样回原图后细节必然损失。我的经验是:目标尺寸在32x32像素以上、不需要亚像素级边缘的场景,FastSAM完全可打;但精细到血管、细胞膜、裂缝的走向,还是老老实实用SAM或专门的分割模型。
另一个边界是密集小目标:图上一两百个类似物体挨在一起时,FastSAM的检测组件会先漏掉一部分,因为YOLO系模型对密集相似小目标的召回天然弱于Transformer检测器。这时候可以调低conf阈值(比如从0.4调到0.25),但代价是候选mask数量翻倍,后处理压力和内存占用同步上升。
3. 把FastSAM跑起来:环境准备、最小推理脚本与参数调优
3.1 环境安装与权重准备:用Ultralytics还是用原版Release
FastSAM官方仓库提供的是基于ultralytics框架修改的版本,但我实际部署时更推荐直接用官方Release里的FastSAM-x.pt权重配合ultralytics主库跑——主库更新频率高,bug修复快,而且API更稳定。安装命令:
# 创建干净环境(Python 3.9-3.11均可) conda create -n fastsam python=3.10 -y conda activate fastsam # 安装依赖 pip install ultralytics==8.0.196 onnxruntime-gpu opencv-python pycocotools注意不要装最新版ultralytics(8.2+)配合FastSAM的官方权重,因为YOLOv8-seg的输出结构在8.1版本前后有调整,直接用最新的库跑老权重,偶尔会出现mask通道数不匹配的报错。锁定8.0.196是我踩过坑后推荐的版本,后面要转TensorRT时,这个版本导出的ONNX结构也更干净。
权重下载很简单:
from ultralytics import YOLO model = YOLO("FastSAM-x.pt")第一次运行会自动下载权重到当前目录,但国内网络环境下载慢,我一般会手动下载后放到路径下。x版本是最大模型,精度最高;如果你在边缘设备或只做粗分割,FastSAM-s.pt速度更快,精度损失约2-3个点,可以按需选。
3.2 最小推理脚本:一次性输出全部mask和可视化
直接给一份可以跑通的脚本:
from ultralytics import FastSAM from ultralytics.models.fastsam import FastSAMPrompt import cv2 # 1. 加载模型 model = FastSAM("FastSAM-x.pt") # 2. 推理:retina_masks=True表示输出高分辨率mask,不压缩到检测框尺寸 results = model("bus.jpg", device="cuda:0", retina_masks=True, imgsz=1024, conf=0.4, iou=0.9) # 3. 创建提示处理对象(必须传原始图像路径+推理结果) prompt_process = FastSAMPrompt("bus.jpg", results) # 4. 生成全量mask输出(everything模式) masks, boxes, scores = prompt_process.everything_prompt() # 5. 可视化并保存 annotated_img = prompt_process.plot(annotations=masks, output_path="bus_fastsam_output.jpg")这段代码的核心逻辑是:FastSAM负责加载权重和推理,FastSAMPrompt专门处理提示和mask后处理。everything_prompt()返回三个值:masks是二维矩阵,每一行是一个候选目标的mask(0/1二值),boxes对应检测框,scores是置信度。
参数说明:
imgsz=1024:推理分辨率。高分辨率保细节,但速度线性下降。批量处理建议从640起步,效果不够再往上升;retina_masks=True:不设的话,mask会被压缩到检测框大小再resize回原图,边缘会有明显锯齿;设了直接输出原图尺寸mask,代价是显存多占30%;conf=0.4:前景置信度阈值。批量分割时0.3-0.4是个好起点,太低会出一堆背景碎块,太高会漏掉低对比度目标;iou=0.9:NMS阈值。注意FastSAM默认NMS阈值偏大,因为同一目标可能存在多个合理的mask形状,太紧的NMS会把同一棵树的不同枝干合并掉。
3.3 点选和文本提示怎么做:一句代码切换交互方式
只在某一个目标上做分割时,用点提示比全量分割后自己筛要快:
# 点提示:坐标是归一化后的(x, y),范围0-1 point = [0.5, 0.4] # 图像中心偏上 point_label = [1] # 1表示前景点,0表示背景点 prompt_process = FastSAMPrompt("bus.jpg", results) masks, boxes, scores = prompt_process.point_prompt(points=[[point]], labels=[point_label]) # 文本提示:支持多个候选词,自动做CLIP匹配 prompt_process = FastSAMPrompt("bus.jpg", results) masks, boxes, scores = prompt_process.text_prompt(text_prompt="a person")点提示和文本提示的核心都是先做一次全图推理拿到候选mask,再按坐标距离或CLIP语义相似度做过滤。所以这两种“提示”并不会比everything模式快,真正的价值在于:你只需要写一句过滤逻辑,不用自己写mask筛选代码。
4. 把mask变成能用的标签:fastmask转JSON、COCO格式与批量处理要点
4.1 从mask矩阵到标注文件:RLE编码与JSON导出
FastSAM返回的mask是numpy二值矩阵,直接存成图片格式做标注的话,文件体积大且后续不好检索,所以我一般转成COCO格式的RLE编码存储。
import numpy as np import json from pycocotools import mask as maskUtils # masks: (N, H, W) 的0/1矩阵,来自everything_prompt()返回值 def masks_to_coco_json(masks, scores, boxes, image_id, category_id=1): annotations = [] for idx, m in enumerate(masks): # 用RLE压缩存储,pycocotools会自动选择压缩方式 rle = maskUtils.encode(np.asfortranarray(m.astype(np.uint8))) bbox = [int(boxes[idx][0]), int(boxes[idx][1]), int(boxes[idx][2] - boxes[idx][0]), int(boxes[idx][3] - boxes[idx][1])] annotation = { "id": idx, "image_id": image_id, "category_id": category_id, "segmentation": { "size": [m.shape[0], m.shape[1]], "counts": rle["counts"].decode("utf-8") }, "bbox": bbox, "area": int(maskUtils.area(rle)), "score": float(scores[idx]) } annotations.append(annotation) return annotations转RLE这一步是必须做的,原因有三个:
- 原图尺寸的mask矩阵直接存JSON,一张1080p图的单目标mask就是几百万个0/1值,一个多目标场景可能几十MB;
- RLE对连续区域的压缩率极高,复杂多边形反而压缩率低,但目标分割的mask基本都是连续区域;
- pycocotools是后续做mAP评估、数据增强、模型训练的标准格式,转过去之后整个标注管道就通了。
4.2 批量分割的工程框架:多进程怎么用才不会内存爆炸
批量处理大量图片时,FastSAM最隐蔽的坑是GPU显存占用会随图片累积。原因是FastSAMPrompt对象持有原图引用和推理结果引用,如果在循环里不释放,上一张图的候选mask会一直挂在内存里。我一般这么写:
import glob from ultralytics import FastSAM from ultralytics.models.fastsam import FastSAMPrompt import gc model = FastSAM("FastSAM-x.pt") image_paths = glob.glob("/data/images/*.jpg") # 批量处理:每张图只保留RLE结果,不保留可视化 with open("all_annotations.jsonl", "w", encoding="utf-8") as f: for idx, img_path in enumerate(image_paths): # 直接传路径而不是传numpy数组,模型内部会自己读图并管理生命周期 results = model(img_path, device="cuda:0", retina_masks=True, imgsz=1024, conf=0.4, iou=0.9, verbose=False) prompt_process = FastSAMPrompt(img_path, results) masks, boxes, scores = prompt_process.everything_prompt() # 导出成RLE后立刻释放图像相关引用 annotations = masks_to_coco_json(masks, boxes, scores, idx) for ann in annotations: f.write(json.dumps({"image": img_path, **ann}) + "\n") # 显式释放大对象 del masks, boxes, scores, prompt_process, results gc.collect()这里两个关键工程习惯:
- 不要用
cv2.imread读图再传给模型,直接传路径。Ultralytics内部会自己管理图像的生命周期,传numpy数组时,内部的图像引用会残留在results对象里,释放不干净; gc.collect()在循环里每张图调一次是合理的,因为mask矩阵动辄几十MB,Python的引用计数在循环体结束后会立即释放,但numpy的底层内存不一定立刻还给系统,显式清理能防止内存水位持续上涨。
4.3 候选mask太多怎么办:面积过滤与置信度二次筛选
全量分割一张街景图可能出200-400个候选mask,但大部分是背景碎块、树影、车窗反光等无关内容。我常用的筛选策略:
# 面积过滤:去掉太小和太大的mask def filter_masks_by_area(masks, min_area_ratio=0.0005, max_area_ratio=0.95): h, w = masks.shape[1], masks.shape[2] total_area = h * w keep = [] for i, m in enumerate(masks): area = m.sum() ratio = area / total_area if min_area_ratio < ratio < max_area_ratio: keep.append(i) return masks[keep] # 置信度过滤:分位数截断比固定阈值更适应不同场景 scores_filtered = np.array(scores) threshold = np.percentile(scores_filtered, 30) # 去掉置信度最低的30%经验上min_area_ratio=0.0005可以过滤掉绝大多数由噪声产生的碎块,但注意如果图上真有极小的目标(比如远处的行人、小动物),这个值要再降一档。分位数截断比固定阈值更好用,因为不同图像的检测置信度分布差异极大,固定阈值容易在白天场景过收、夜晚场景过放。
5. FastSAM避坑指南:我实际踩过的五个坑与解决方案
5.1 坑一:点提示坐标系搞反,导致选中的mask总是偏的
现象:用point_prompt输入一个坐标点,返回的mask不是目标物体,而是旁边或完全无关的背景区域。
原因:FastSAM的点提示需要归一化坐标(0-1范围),但很多人在用cv2或PIL拿到的是像素坐标,直接传进去就会偏。而且不同版本之间坐标轴的缩放方式也有差异,0.5和0.4这种值在1024x1024和原图分辨率之间换算会有细微偏差。
解决:统一在推理前做归一化,不要用原图尺寸直接算。如果是从plot()可视化结果上点击取点,注意可视化输出的坐标系和推理图可能不同。
# 正确做法:用推理尺寸的坐标除以推理尺寸 x_norm = x_pixel / result_img_width y_norm = y_pixel / result_img_height5.2 坑二:mask边缘锯齿严重,尤其在斜边和小目标上
现象:横幅、招牌、屋顶斜边这些位置的mask边缘是明显的阶梯状,看着很“劣质”。
原因:FastSAM的原型mask分辨率固定,上采样回原图时使用了双线性插值,斜边的抗锯齿只能靠插值算法硬撑,没有SAM那种基于ViT特征的细节重建能力。
解决:唯一有效的补救是在后处理阶段对mask边界做平滑——用cv2.GaussianBlur对mask做3x3模糊再二值化,能让阶梯感减轻一半以上。注意不要用腐蚀膨胀,那会破坏边缘位置的精度。这个坑是结构性的,所以选型时就要评估自己对边缘精度的容忍度。
5.3 坑三:全量分割时,小目标被NMS合并丢失
现象:图上有几十只鸟或一群人站得很近,分割结果里只有一部分目标,剩下的是“合并体”——好几个目标共用一个mask。
原因:FastSAM的NMS基于box的IoU,当多个目标的检测框高度重叠时,置信度低的框连同它的mask会一起被抑制。对于密集小目标,YOLO系检测的默认NMS策略天然吃亏。
解决:把iou参数从0.9改成0.7会好很多,但代价是同一目标可能出现多个mask(比如一辆车的前脸和车身被拆开)。更有效的做法是:检测阶段用低conf确保召回,后处理阶段再做一次mask级别的去重——如果两个mask的IoU超过0.9,只保留置信度高的那个。
5.4 坑四:转TensorRT时输出名字和形状与原生PyTorch不一致
现象:把FastSAM导出成ONNX后,加载到TensorRT推理,输出层解析的结果和PyTorch推理对不上,mask形状完全乱套。
原因:Ultralytics导出ONNX时,输出的原始张量是(1, 84, 8400)或(1, 116, 8400)这样的组合格式——前4个是box,后面是mask系数和类别分数——需要后处理拆分。但FastSAM的mask部分用的是原型掩膜组合方式,和普通YOLOv8-seg的解析不完全一样,直接套通用YOLO的解析代码会出错。
解决:导出ONNX时加opset=12,然后自己写拆分层。具体拆法:
# ONNX输出的transpose后: (1, 84, num_anchors) # 前4位:cx, cy, w, h # 后80位:把mask原型和分数分开 box = pred[:, :4, :] mask_coeff = pred[:, 4:4+32, :] # 32个mask原型系数 scores = pred[:, 4+32:, :]这一步没有捷径,必须手动对齐模型结构和输出通道数。建议导出前用onnxruntime跑一次,打印出实际输出shape,再写解析逻辑。
5.5 坑五:CPU推理慢到怀疑人生,但GPU利用率又上不去
现象:CPU上跑一张512x512图要3-5秒,GPU上跑虽然快但显存占用高、利用率只有30-50%。
原因:FastSAM的推理流程里,everything_prompt()的后处理(mask上采样、NMS、坐标转换)全部在CPU上执行。GPU推理本身很快,但每张图的候选mask都要从显存拷回CPU,这个过程是串行的,GPU在等待中大量空闲。
解决:把后处理也搬到GPU上。用GPU做mask上采样:
# 在GPU上进行mask上采样,避免CPU拷贝 mask_tensor = torch.from_numpy(masks).to("cuda:0") mask_upsampled = torch.nn.functional.interpolate( mask_tensor.unsqueeze(0).float(), size=(img_h, img_w), mode="bilinear" ).squeeze(0)这一步带来的速度提升通常比换更大的GPU还明显,尤其是当候选mask数量多的时候。批量推理场景下,建议把整批图片的所有候选mask拼成一个batch一次性上采样,比逐张处理快很多。
6. 用TensorRT把FastSAM压进毫秒级:从ONNX导出到FP16推理的完整流程
如果FastSAM只用PyTorch推理,一张1080p图在RTX 3090上大约需要30-50ms,这已经能满足很多实时场景。但如果你要跑视频流或嵌入式设备,NVIDIA Jetson Orin这类平台,TensorRT的FP16推理能把延压到10ms以内。我分享一下自己踩过的落地路径。
首先要导出干净的ONNX:
# 用固定尺寸导出ONNX,TensorRT不支持完全动态的输入尺寸 yolo export model=FastSAM-x.pt format=onnx imgsz=640 opset=12 simplify=Trueimgsz=640是个折中:检测头在这个分辨率下对中小目标足够友好,同时TensorRT的优化效果最好。如果坚持要1024输入,显存占用和延迟几乎翻倍。
然后转成TensorRT引擎,我的参数如下:
trtexec --onnx=FastSAM-x.onnx \ --saveEngine=FastSAM_x_fp16.engine \ --fp16 \ --minShapes=images:1x3x640x640 \ --optShapes=images:4x3x640x640 \ --maxShapes=images:8x3x640x640几个关键选择:
--fp16开启半精度,在Jetson上能再叠加--stronglyTyped精度策略,但PC上的TensorRT不需要;--minShapes/--optShapes/--maxShapes三个参数允许batch size动态变化,视频流单帧和离线批量可以共用同一个engine;- MIG或并行推理时,
--workspace默认值就够,不用再调。
坑在解析层:TensorRT的输出顺序和PyTorch不同,需要自己写后处理。常见做法是给模型加一个自定义的nms插件或者直接用TensorRT的efficientNMS插件接在输出层后面,但FastSAM的mask组装发生在nms之前,所以不能直接用通用插件。我的方案是不在TensorRT里做nms,让模型输出原始张量,nms和mask组装全部在TensorRT之外用CUDA核函数或者普通Python做——因为main目标是把网络前向压到极致,不等于整个pipeline都要塞进TensorRT。
一个值得用的细节:trtexec生成的engine默认只优化静态输入,如果你要的是高速视频流,建议用--inputIOFormats=fp16:chw把输入格式压到FP16,虽然预处理要多写一步FP16转换,但总延迟能进一步压低。
最后说两个实战习惯。第一个是保底方案:TensorRT engine和输入分辨率绑定,换分辨率就必须重新构建,所以上线前想清楚部署场景的分辨率,别在客户现场才改。第二个是验证习惯:每次改后处理参数,先跑同一张基准图的mask对比,肉眼确认和PyTorch原版输出一致再交出去。我吃过一次亏,TensorRT版本的解析代码写错了mask通道顺序,导致输出的mask全部上下翻转,还以为是模型问题,浪费了两天。
如果能重来一次,我会在最开始就把“PyTorch推理结果”存成标准JSON,TensorRT版本每次改动跑完自动和基准JSON对比mIoU,低于0.95就不允许上线。这个习惯帮我拦住过至少三次回归。希望这些记录能帮你绕开我踩过的坑。
本文还有配套的精品资源,点击获取