☰
SAM2实战指南:用Ultralytics实现自动/框选/点选分割
2026/9/25 22:45:48 网站建设 项目流程

简介:面向需要快速上手SAM2的计算机视觉开发者,这套资源基于Ultralytics框架封装了Segment Anything Model 2的完整测试代码与预训练权重。压缩包共7个文件,包含4个pt格式的模型权重(覆盖tiny、base、small、large等不同规格)、2个Python脚本以及1张jpg测试图片,包体约690.55MB。脚本分别对应基于提示词(点、框)的交互式分割与全图自动分割两种典型用法,并配有可直接运行的样例图片,便于理解SAM2在图像分割任务中的调用方式与输出效果;测试代码也给出了不同交互条件下的提示示例,方便对比分割结果。不同规格的pt权重可满足速度与精度兼顾的需求,Python脚本结构清晰,便于在此基础上替换输入图片或调整参数做进一步实验。目前已有1265人浏览学习,适合正在研究视觉分割模型或需要基于Ultralytics进行二次开发的开发者参考。

1. 为什么SAM2值得动手跑一遍:提示词分割不是噱头

去年我在处理一套街景数据时,最费时的不是算法选型,而是给广告牌做手工标注。后来换成SAM2配Ultralytics框架,才发现这个模型根本不用微调——给个框、点几下,就能把目标从背景里干净地切出来。它属于图像分割领域里“分割一切”的思路,在医学图像分割、遥感目标切分这类场景里同样能直接复用,输出的是实例级别的mask,不是分类概率。适合两类人:一是手上没标注数据想快速出mask的算法工程师,二是想用大模型生成训练集再蒸馏给轻量模型的开发。下面的笔记直接把可跑代码、参数调法和踩坑记录给你,照着就能复现。

2. 模型骨架与Ultralytics封装:先弄清楚SAM2在切什么

2.1 三段式结构:图像编码器、记忆注意力、mask解码器

SAM2延续了“prompt-driven”的设计,但内部结构比第一代SAM复杂不少。整条链路分为三块:Hiera图像编码器负责把输入图变成多尺度特征;记忆注意力模块让模型可以跨帧引用信息;mask解码器把特征和用户的点/框提示融合,输出分割结果。理解这三块,是后面调参数的前提。

Hiera编码器的核心是保留归纳偏置的层次化Transformer。它不像ViT那样一上来就把图切成均匀patch,而是模仿卷积网络做逐步下采样,浅层处理局部纹理,深层处理语义。这样设计的好处是计算量比纯ViT小,同时还能拿到多尺度特征。对于图像分割来说,多尺度特征很重要——广告牌这种大目标需要深层语义,而医学图像里细小的血管则需要浅层纹理。

记忆模块和mask解码器是SAM2区别于传统分割模型的关键。记忆模块维护一个特征缓存,把当前帧的视觉特征写入,后续推理时再读出,形成上下文。即使在单张图像推理时,这个模块也在工作,只是没有跨帧记忆而已。解码器输出时,如果开启多mask模式,会一次给3个候选mask,分别对应整体轮廓、局部细节和边缘锐化三个不同“理解”,然后用IoU预测头给每个候选打分。这个设计解决了一个经典问题:同一个提示词,可能对应多个合理分割结果。

2.2 检查点怎么选:sam2_t、sam2_s、sam2_b、sam2_l的取舍

Ultralytics把官方权重转成了统一的.pt格式,命名规则很直接:t是tiny,s是small,b是base,l是large。参数量从几千万到两亿多不等。我自己的经验是:如果只是做数据标注或快速验证,b就够了,速度和精度都比较均衡;如果对边缘质量要求高、有GPU且不赶时间,选l;t和s适合在CPU或老显卡上做demo,精度差距在精细边缘上体现得最明显。

模型加载通过统一入口完成:

from ultralytics.models.sam.build import build_sam model = build_sam("sam2_b.pt")

build_sam会自动处理两件事:一是从指定路径加载权重,二是把模型切换到推理模式。第一次运行时如果本地没有对应文件,会尝试从远端下载权重。这里有个值得注意的点:文件名里的b代表base,不是batch,别和训练时的batch size混淆。

加载完之后,建议显式指定推理设备:

model.to("cuda" if torch.cuda.is_available() else "cpu")

很多人在这一步翻车,后面避坑章节会展开讲。现在只要记住:模型加载后立即指定设备,后面所有操作都在这个设备上进行,不要中途切换。

3. 用Ultralytics跑通三类分割:自动、框选、点选

3.1 ultralytics安装与环境检查

先装环境。Ultralytics的SAM2支持从8.3.x版本开始,安装时用pip拉最新版就好:

pip install -U ultralytics python -c "import ultralytics; print(ultralytics.__version__)"

如果是在国内网络环境,可以加镜像源加速。安装完成后,建议顺手验证一下依赖的torch和opencv版本,SAM2对torch版本有一定要求,太老的版本会出莫名其妙的算子错误。另外提醒一句:如果你的项目里已经装过旧版ultralytics,直接用-U覆盖可能出现依赖冲突,稳妥做法是先建一个独立虚拟环境再装。

3.2 自动分割:全图直接出所有mask

自动分割适合“我不知道图里有什么,先把所有区域切出来”的场景。它会在图上铺一个密集点阵,以每个点为中心做推理,再把结果合并去重。代码非常短:

import cv2 from ultralytics.models.sam import SamAutomaticMaskGenerator from ultralytics.models.sam.build import build_sam model = build_sam("sam2_b.pt") model.to("cuda") mask_generator = SamAutomaticMaskGenerator( model, points_per_side=16, # 每边采样点数,16×16=256个起始点 pred_iou_thresh=0.80, # 模型自估IoU阈值,低于此值直接丢弃 stability_score_thresh=0.85, # 稳定性分数阈值,过滤边缘抖动大的mask min_mask_region_area=100, # 小于100像素的碎片直接清除 ) img = cv2.imread("street.jpg") # OpenCV读入的是BGR,Ultralytics内部处理 result = mask_generator.generate(img) for i, r in enumerate(result): print(f"mask {i}: bbox={r['bbox']}, iou={r['predicted_iou']:.2f}, " f"stability={r['stability_score']:.2f}, area={r['area']}")

这段代码的关键在SamAutomaticMaskGenerator的参数。points_per_side=16意味着生成16×16共256个起始点,点数越多,小目标越容易被发现,但推理时间成倍增加。pred_iou_thresh是模型对自己输出质量的预估分,低于0.8的候选直接丢弃;stability_score_thresh更微妙——它通过对mask做微小扰动来测试稳定性,边缘模糊或者和背景纠缠不清的区域,这个分数会很低。min_mask_region_area是纯后处理,把碎片清理掉,避免后续标注时被小噪点干扰。

实测下来,这套默认参数对中等尺寸目标效果不错,但对小目标或细长结构不太友好,第4章会专门讲怎么调。

3.3 框选分割:给一个bbox,切出目标

自动分割拿到的是全图所有目标,但如果我只想切某一个具体物体,框选更直接。所谓框选,就是给定目标的左上和右下坐标,让模型只在这个范围内推理:

import cv2 from ultralytics.models.sam import SamPredictor from ultralytics.models.sam.build import build_sam model = build_sam("sam2_b.pt") model.to("cuda") predictor = SamPredictor(model) img = cv2.imread("billboard.jpg") predictor.set_image(img) # 编码特征会缓存,后续predict直接复用 masks, scores, logits = predictor.predict( boxes=[[100, 80, 500, 360]], # xyxy绝对坐标 multimask_output=False, # 只返回最优的一个mask ) print(masks.shape) # (1, 高, 宽) print(f"mask score: {scores[0].item():.3f}")

这里最需要注意的是boxes的格式。它接收的是xyxy绝对像素坐标,不是YOLO习惯用的xywh,也不接受归一化坐标。很多从目标检测转过来的人在这里栽跟头——拿YOLO的标注格式直接喂进去,结果mask和位置完全对不上。multimask_output=False让模型挑自认为最好的一个mask返回,如果设为True则会返回3个候选,需要自己看哪个更合适。

3.4 点选分割:正负点交互,细化边缘

框选适合目标边界比较规整的情况,但如果两个物体靠得很近,或者目标边缘特别复杂,点选更精确。点选的核心是:给一个前景点,模型往外生长出mask;如果生长过头了,再给一个背景点把多余部分“按回去”:

import cv2 from ultralytics.models.sam import SamPredictor from ultralytics.models.sam.build import build_sam predictor = SamPredictor(build_sam("sam2_b.pt")) predictor.set_image(cv2.imread("cell.jpg")) masks, scores, logits = predictor.predict( points=[[120, 150], [185, 220]], # 第一个是前景点,第二个是背景点 labels=[1, 0], # label=1表示前景, 0表示背景 multimask_output=False, )

labels数组和points一一对应,1代表“这里是我要的目标”,0代表“这里不是”。这个机制在处理粘连目标时特别有用——比如医学图像分割里两个细胞挤在一起,第一个点落在目标细胞上,第二个点按在相邻细胞上,模型就会在两个点的博弈中把边界收敛到合适的区域。点选也是SAM2系列模型交互性能最好的方式,因为它最接近训练时的prompt分布。

4. 避坑:SAM2实战中的5个高频翻车点

4.1 device参数导致推理全部落在CPU上

现象:模型明明to("cuda")了,但推理时显存占用为0,torch.cuda.is_available()也是True,速度慢得离谱。

原因:Ultralytics的SAM2实现在内部有多处torch.device的隐式推断。如果你在set_image时传入的设备与模型当前设备不一致,某些中间结果会被自动搬回CPU。常见的触发场景是:先用CPU加载模型做了一次推理,之后又model.to("cuda"),但SamPredictor内部的某些缓存张量还留在CPU上。

解决:构建模型后立即指定设备,set_image和predict之间不要切换设备。如果已经出了这个问题,最简单的办法是销毁SamPredictor重新构建,不要试图手动搬缓存张量,因为内部结构复杂,容易漏掉某个变量。

4.2 小目标整体消失,只剩背景碎片

现象:对一张包含多个小广告牌、小目标的街景图做自动分割,结果小目标一个都没出来,反而把墙面裂缝、树叶阴影切出一堆碎片。

原因:默认的pred_iou_thresh=0.80和stability_score_thresh=0.85偏向保守。小目标的区域小,边缘像素比例高,模型自估IoU和稳定性分数天然偏低,阈值一卡就直接被过滤掉了。

解决:把两个阈值降下来:

mask_generator = SamAutomaticMaskGenerator( model, points_per_side=32, # 提高采样密度 pred_iou_thresh=0.60, # 降低自估IoU门槛 stability_score_thresh=0.75, # 降低稳定性门槛 min_mask_region_area=50, # 只清理小于50像素的碎片 )

调低阈值后碎片会增加,这是正常现象,后续通过面积过滤和人工筛选来解决。小目标场景的核心原则是:先让模型“多切”,再用后处理“精选”,而不是一上来就用高阈值把候选全杀掉。

4.3 boxes格式混淆:把xywh和归一化坐标喂进去

现象:框选分割时,mask位置完全错乱,有时mask甚至跑到了图像外面。

原因:Ultralytics的predict(boxes=...)接收的是绝对像素坐标xyxy,而YOLO标注、标注平台导出数据通常是xywh归一化格式。直接混用必然翻车。

解决:写一个统一的转换函数:

def yolo_to_sam_boxes(boxes_norm, img_w, img_h): """把归一化的xywh转成SAM2需要的xyxy绝对坐标""" boxes_xyxy = [] for x_c, y_c, w, h in boxes_norm: x1 = (x_c - w / 2) * img_w y1 = (y_c - h / 2) * img_h x2 = (x_c + w / 2) * img_w y2 = (y_c + h / 2) * img_h boxes_xyxy.append([int(x1), int(y1), int(x2), int(y2)]) return boxes_xyxy

从那以后,我每次喂box给SAM2之前,都会打印一下坐标的最大最小值,确认没超出图像尺寸,再做后续处理。

4.4 循环推理时显存一路暴涨,最后OOM

现象:写了个循环遍历一批图片,每张图都做一次框选分割。跑了几十张后,显存占用从3GB涨到8GB,最终OOM。

原因:每张图都重新调用build_sam构建了一次模型,旧模型没有被释放。更隐蔽的情况是:set_image本身会缓存图像特征,如果每次循环都新建predictor,旧缓存全部泄漏。

解决:模型和predictor都只构建一次,循环里只更新图像和prompt:

model = build_sam("sam2_b.pt").to("cuda") predictor = SamPredictor(model) for img_path in image_list: img = cv2.imread(img_path) predictor.set_image(img) # 只更新缓存,不重建模型 masks, scores, _ = predictor.predict( boxes=current_boxes, multimask_output=False ) # 处理masks...

这看起来是小事,但在批量处理数据集时,代码写得不谨慎,轻则显存溢出,重则拖垮整台机器。

4.5 结果直接商用,license没理清

现象:项目上线前法务审核,发现代码库的license不兼容,差点重写推理部分。

原因:SAM2官方权重是Apache 2.0,但Ultralytics的代码库是AGPL-3.0。这意味着如果你在商业产品里直接调用Ultralytics的build_sam和SamPredictor,整个项目的开源义务可能被触发。

解决:商用场景两条路——一是只使用官方发布的权重和官方的sam2推理代码,保持Apache 2.0的兼容性;二是接受AGPL约束,把周边代码开源。技术方案本身没有好坏,但license必须在动手前确认,否则后面全是坑。

5. 把mask变成可训练数据:RLE转COCO标注并过滤低分样本

5.1 自动分割输出的数据结构

用SAM2做数据标注管线的核心目标,是生成可以喂给YOLO等目标检测/分割模型的训练集。SamAutomaticMaskGenerator.generate返回的是一个字典列表,每个字典里segmentation字段比较特殊——当启用了crop策略时,它是pycocotools格式的uncompressed RLE;没启用时则是二值数组。RLE是一种压缩编码,不能直接当数组用,必须先解码。

5.2 RLE解码、polygon提取与样本过滤

下面这段代码把自动分割结果转成COCO格式的annotation,同时按分数和质量过滤:

import cv2 import numpy as np from pycocotools import mask as mask_utils def rle_to_polygon(rle, image_shape): """RLE转polygon,返回COCO segmentation字段需要的扁平坐标""" mask = mask_utils.decode(rle) # 解码为二值mask mask = (mask > 0).astype(np.uint8) contours, _ = cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) polygons = [] for contour in contours: contour = contour.flatten().tolist() if len(contour) >= 6: # 至少3个点,过滤几乎空的轮廓 polygons.append(contour) return polygons def masks_to_coco(result, image_id, iou_thresh=0.75, min_area=100): """把SAM2自动分割结果转成COCO标注""" coco_annotations = [] for i, r in enumerate(result): # 核心过滤:低IoU和碎片不进入训练集 if r["predicted_iou"] < iou_thresh: continue if r["area"] < min_area: continue polygons = rle_to_polygon(r["segmentation"], r.get("original_size")) if not polygons: continue coco_annotations.append({ "id": i, "image_id": image_id, "category_id": 1, "bbox": [float(v) for v in r["bbox"]], # SAM输出的是xyxy "segmentation": polygons, "area": float(r["area"]), # 用SAM算好的面积,更精确 "iscrowd": 0, }) return coco_annotations

转换代码本身的坑不多,最值得留意的是过滤策略。我之前直接拿全量结果导入标注平台,结果模型把墙面、车窗都切了出来,标注师花了大量时间去删无效样本。加上iou_thresh和min_area过滤后,有效样本比例从六成提升到九成以上。

还要注意一点:r["bbox"]是SAM2输出的绝对坐标,格式是[x1, y1, x2, y2],而COCO要求的bbox是[x, y, width, height]。我在第一版代码里直接沿用,导致后续训练指标全乱了。要么转换,要么在训练时用能接受xyxy的dataset类。强烈建议在写入JSON之前统一为COCO标准格式:

x1, y1, x2, y2 = r["bbox"] coco_bbox = [x1, y1, x2 - x1, y2 - y1]

6. 让分割更快更准的三个习惯,和一次教训

最后一个章节,分享三个能让SAM2落地更顺的技巧。

第一个习惯:输入图先压长边到1024再做推理。SAM2内部虽然会做缩放,但外部先缩放可以让自动分割的crop策略更稳定,还能明显提速。比如一张4000×3000的街景图,直接推理可能要十几秒,缩到1024后基本在1秒内出结果。压缩时用OpenCV的INTER_AREA插值,质量损失最小。

第二个习惯:阈值调整不是拍脑袋,而是看分数分布。每跑完一次自动分割,先打印所有mask的predicted_iou和stability_score分布,再决定阈值卡在哪里。下面的参数表是我在几个项目里试出来的起点,具体场景在此基础上浮动:

场景pred_iou_threshstability_score_threshpoints_per_side
大块目标(广告牌、楼栋)0.72 - 0.800.85 - 0.908 - 16
小目标/密集目标0.55 - 0.700.75 - 0.8532 - 64
医学图像细结构0.50 - 0.650.75 - 0.8532

第三个习惯:连续帧场景复用上一帧的mask中心点作为下一帧的提示词。逐帧全图自动分割不仅慢,而且帧间mask抖动明显。取上一帧目标mask的中心坐标,作为下一帧predict的points输入,配合labels=[1],速度和稳定性都会好很多。这是SAM2官方在视频场景的推荐用法之一,在Ultralytics里同样适用。

最后说一次真实教训。给一个合同项目做户外广告牌分割数据时,我图省事,把SAM2自动分割的输出全量导入标注平台。结果模型把墙面纹理、电线阴影、车窗反光全切了出来,标注师清理了两天,还漏了不少垃圾样本混进了训练集。后来我花了半小时统计分数分布,发现stability_score低于0.8的样本几乎都是边缘模糊或背景纠缠的无效mask。从那以后,每次做分割数据集都强制走一遍“先看分数分布、再定阈值、再导入标注”的流程,再也没有翻过车。希望这篇笔记能帮你少踩几个我踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询