☰
Ultralytics 集成 SAM2:快速实现图像分割与半自动标注
2026/9/29 14:03:40 网站建设 项目流程

简介:这份资源面向计算机视觉方向的开发者与深度学习学习者,提供基于Ultralytics框架调用SAM2(Segment Anything Model 2)进行图像分割的模型权重与配套测试代码,可用于快速验证提示分割与全图分割等典型场景,适合具备一定PyTorch基础、希望上手SAM2实战的中高级读者。压缩包共7个文件,包含4个pt权重文件、2个Python脚本和1张示例图片,整体约690.55MB,其中权重覆盖不同规模版本,脚本分别对应提示式分割与全图分割两种调用方式,示例图片便于直接运行验证效果。目前已有1265人学习下载,说明该组合在社区中具备一定参考价值。读者可借助现成权重省去自行训练与转换的成本,通过脚本理解Ultralytics接口下SAM2的加载、推理与结果可视化流程,并以此为起点迁移到自定义数据集或下游分割任务中,快速搭建可复现的图像分割实验环境。

1. SAM2 遇上 Ultralytics:一条命令背后的分割能力边界

如果你最近在找图像分割的落地方案,大概率会同时刷到两个词:Ultralytics 和 SAM2。前者是 YOLO 系列背后的工程框架,后者是 Meta 的 Segment Anything Model 2。把这两个东西拼在一起,最直接的收益是:你不用再手写 SAM2 的推理循环、不用自己处理图像预处理和后处理,直接调用 Ultralytics 的统一 API 就能跑出分割掩码。这件事对做广告牌图像分割系统、医学图像分割预标注、甚至只是想把一张图里的物体抠干净的人来说,价值很明确——省掉至少两天的环境折腾和接口适配。但我要先把边界说清楚:Ultralytics 封装的是 SAM2 的推理能力,不是训练能力;它给你的是开箱即用的分割结果,不是让你从头训一个 SAM2。适合谁?适合需要快速验证分割效果、做数据预标注、或者把分割能力嵌进现有 Python 流水线的工程师。不适合谁?不适合想改 SAM2 网络结构、做量化模型部署到边缘设备的人——那得走另一条路。

2. 环境搭建与最小可跑通路径:ultralytics 安装到第一张掩码

2.1 安装 ultralytics 与 SAM2 权重准备

Ultralytics 的安装本身不复杂,但 SAM2 的权重文件需要单独下载,这一步很多人会卡住。我一般会先建一个干净的虚拟环境,避免和已有的 torch 版本打架。

# 创建虚拟环境,Python 3.10 是我实测最稳的版本 python -m venv sam2_env source sam2_env/bin/activate # Windows 用 sam2_env\Scripts\activate # 安装 ultralytics,它会自动拉取兼容的 torch 和 torchvision pip install ultralytics # 验证安装,确认版本和 CUDA 可用性 yolo checks

yolo checks会输出当前环境的关键信息,重点看三行:Ultralytics 版本、PyTorch 版本、CUDA 是否可用。如果 CUDA 显示不可用但你有 NVIDIA 显卡,大概率是 torch 装成了 CPU 版,需要去 PyTorch 官网找对应 CUDA 版本的安装命令重装。权重文件方面,Ultralytics 支持自动下载,但国内网络环境下自动下载经常超时。我的做法是手动下载sam2.1_t.pt(tiny 版,约 150MB)放到当前工作目录,然后代码里直接指定本地路径。tiny 版在单张 RTX 3060 上跑 1024x1024 图像大约 0.3 秒,精度对于预标注场景够用;如果做医学图像分割这种对边界敏感的任务,建议换sam2.1_b.pt或sam2.1_l.pt,显存占用会从 2GB 涨到 6GB 左右。

2.2 用三行代码跑通第一张图的自动分割

Ultralytics 把 SAM2 的调用压缩到了极简程度。下面这段代码是我在广告牌图像分割场景里验证过的,输入一张街景图,输出所有检测到的物体掩码。

from ultralytics import SAM # 加载本地权重,不写路径会自动下载 model = SAM("sam2.1_t.pt") # 自动分割模式:不指定 prompt,模型会分割全图所有物体 results = model("street_billboard.jpg") # 遍历每个掩码,打印面积和置信度 for i, mask in enumerate(results[0].masks.data): area = mask.sum().item() print(f"掩码 {i}: 面积 {area:.0f} 像素")

这段代码的逻辑是:SAM类封装了图像预处理、模型推理、掩码后处理全流程。results[0].masks.data是一个形状为(N, H, W)的张量,N 是分割出的物体数量。面积计算用mask.sum()是因为掩码是二值化的,求和就是像素计数。参数方面,model()调用时可以传imgsz控制推理分辨率,默认 1024,调小到 512 能提速约 40% 但小物体分割会变差;conf参数控制置信度阈值,默认 0.25,做医学图像分割时我一般调到 0.5 减少假阳性。注意:自动分割模式对遮挡严重的场景表现一般,比如广告牌被树枝挡住一半,SAM2 可能会把树枝和广告牌分成两个掩码而不是一个完整物体。

2.3 用点提示和框提示做交互式分割

自动分割只是开胃菜,SAM2 真正的强项是 promptable segmentation——你给一个点或一个框,它把对应的物体抠出来。这在医学图像分割里特别有用,比如医生点一下肿瘤区域,模型自动扩展出完整边界。

from ultralytics import SAM model = SAM("sam2.1_t.pt") # 点提示:在坐标 (500, 300) 处给一个前景点 results = model("ct_scan.jpg", points=[[500, 300]], labels=[1]) # 框提示:用边界框指定目标区域 results = model("ct_scan.jpg", bboxes=[[200, 150, 600, 450]]) # 保存分割结果可视化 results[0].save("output_mask.png")

points参数接受一个列表,每个元素是[x, y]坐标;labels对应每个点的类型,1 表示前景(要分割的物体),0 表示背景(排除的区域)。框提示的bboxes格式是[x1, y1, x2, y2]。这里有个血泪经验:坐标必须基于原图尺寸,如果你先做了 resize 再传坐标,分割结果会完全错位。我一般会在预处理阶段记录缩放比例,推理时把坐标映射回原图。另外,多个点提示可以组合使用,比如一个前景点加一个背景点,能显著提升边界精度。save()方法会把原图、掩码、边界框叠加后输出,方便快速检查效果。

3. 参数调优与批量推理:从单张图到生产流水线

3.1 控制掩码质量的四个关键参数

Ultralytics 的 SAM2 接口暴露的参数不多,但每一个都直接影响分割结果。我把它们整理成表,方便你按场景调。

参数默认值作用调参建议
imgsz1024推理分辨率小物体多调到 1280,速度优先调到 640
conf0.25置信度阈值医学图像调到 0.5,广告牌分割保持 0.25
retina_masksFalse是否输出高分辨率掩码需要像素级精度时设为 True,显存增加约 30%
points/bboxesNone提示信息交互式分割必传,自动分割不传

retina_masks=True这个参数值得单独说。默认情况下,SAM2 输出的掩码是 256x256 的,然后上采样到原图尺寸,边缘会有锯齿。开启 retina_masks 后,模型会在更高分辨率上生成掩码,边缘更平滑,但推理时间增加约 20%。我做广告牌图像分割系统时,因为要精确计算广告牌面积,这个参数是必开的。conf阈值的影响很直接:调高会减少误检但可能漏掉小物体,调低则相反。蒙特卡罗方法在图像分割里常用来做不确定性估计,但 Ultralytics 接口没有直接暴露这个能力,如果你需要,得在拿到掩码后自己加后处理。

3.2 批量推理与结果导出

生产环境不可能一张一张跑,批量推理是刚需。Ultralytics 支持传入文件夹路径或图像列表,内部会自动批处理。

from ultralytics import SAM from pathlib import Path model = SAM("sam2.1_t.pt") # 批量推理整个文件夹 image_dir = Path("./billboard_images") results = model(image_dir, batch=4, imgsz=1024, retina_masks=True) # 导出每个掩码为 PNG,文件名带序号 for idx, r in enumerate(results): for j, mask in enumerate(r.masks.data): # mask 是 tensor,转 numpy 后保存 import numpy as np from PIL import Image mask_np = (mask.cpu().numpy() * 255).astype(np.uint8) Image.fromarray(mask_np).save(f"./masks/{idx}_{j}.png")

batch=4表示一次处理 4 张图,具体能开多大取决于显存。RTX 3060 12GB 上,imgsz=1024加retina_masks=True时 batch 开到 4 是安全的,再大就 OOM。导出掩码时注意:mask.cpu().numpy()得到的是 0/1 浮点数组,乘以 255 转成 uint8 才能被 PIL 正确保存。如果你需要 COCO 格式的标注文件,Ultralytics 的results[0].save_txt()可以输出 YOLO 格式的边界框和类别,但 SAM2 没有类别信息,所有物体都是同一类,需要自己接一个分类模型做后处理。

3.3 和 YOLO 检测器串联做两阶段分割

单独用 SAM2 做自动分割有个问题:它不知道物体是什么,只能分割出「有东西的区域」。实际项目中,我一般会先用 YOLO 做检测,拿到边界框后再送给 SAM2 做精细分割。这样既有了类别信息,又有了像素级掩码。

from ultralytics import YOLO, SAM # 第一阶段:YOLO 检测广告牌 det_model = YOLO("yolov8n.pt") det_results = det_model("street.jpg", conf=0.4) # 提取检测框 boxes = det_results[0].boxes.xyxy.cpu().numpy().tolist() # 第二阶段:SAM2 根据框做分割 sam_model = SAM("sam2.1_t.pt") seg_results = sam_model("street.jpg", bboxes=boxes) # 此时 seg_results[0].masks 和 boxes 一一对应 print(f"检测到 {len(boxes)} 个广告牌,分割出 {len(seg_results[0].masks.data)} 个掩码")

这个串联方案的关键在于:YOLO 的检测框直接作为 SAM2 的 prompt,省去了手动点选。boxes是[x1, y1, x2, y2]格式的列表,SAM2 会为每个框生成一个掩码。注意检测框的质量直接影响分割效果——如果 YOLO 漏检了,SAM2 也分割不出来;如果框偏了,掩码也会偏。我一般会把 YOLO 的conf设低一点(0.3 左右),宁可多检几个框,后面再用规则过滤。这个两阶段方案在广告牌图像分割系统里跑下来,单张图端到端约 0.5 秒,比纯 SAM2 自动分割快 30%,而且结果更可控。

4. 避坑与排查:SAM2 在 Ultralytics 里最容易翻车的五个地方

4.1 掩码全黑或全白

现象:推理跑通了,但保存出来的掩码要么全黑要么全白,没有任何有效分割。原因通常是图像预处理阶段出了问题。Ultralytics 内部会把图像归一化到 0-1 并调整尺寸,如果你的输入图像本身是 16 位灰度图(医学图像常见),归一化后会丢失动态范围。解决方法是先把图像转成 8 位 RGB,用cv2.normalize拉伸对比度后再送入模型。另一个可能原因是conf阈值设得太高,所有掩码都被过滤了,把conf降到 0.1 试试。

4.2 显存溢出但不知道哪一步爆的

现象:跑批量推理时突然 OOM,但单张图没问题。原因通常是retina_masks=True和batch同时开大。SAM2 的 retina masks 会在解码器阶段生成高分辨率特征图,显存占用和batch * imgsz^2成正比。解决方法是先用batch=1加retina_masks=True跑通,记录显存占用,再逐步加 batch。如果显存实在不够,把imgsz从 1024 降到 768,掩码质量下降有限但显存省一半。

4.3 点提示坐标对不上

现象:传了points=[[500, 300]],但分割出来的物体不在预期位置。原因几乎总是坐标基准不一致。Ultralytics 期望的坐标是基于原图尺寸的,但如果你在送入模型前做了 resize 或 crop,坐标就偏了。解决方法是把所有预处理操作记录下来,推理时把坐标反向映射回原图坐标系。我一般会在代码里写一个scale_coords函数专门处理这件事,避免手动算错。

4.4 自动分割把背景也分出来了

现象:自动分割模式下,模型把天空、路面、墙面这些背景区域也分割成了掩码。原因是 SAM2 的自动分割模式本质上是在全图找「有意义的区域」,它没有类别概念,背景只要纹理复杂就会被当成物体。解决方法是不要依赖纯自动分割,要么用点/框提示指定目标,要么接一个 YOLO 检测器先过滤出感兴趣区域。如果非要用自动分割,可以在后处理阶段按面积和位置过滤——比如广告牌分割场景里,只保留面积在 5000 到 50000 像素之间、且位于图像上半部分的掩码。

4.5 模型加载慢或下载卡住

现象:第一次运行SAM("sam2.1_t.pt")时卡住不动,或者报连接超时。原因是 Ultralytics 默认从 GitHub 下载权重,国内网络环境不稳定。解决方法是手动下载权重文件放到当前目录,代码里直接写本地文件名。如果不知道权重文件的确切下载地址,可以先在能访问的机器上跑一次SAM("sam2.1_t.pt"),然后把缓存目录(通常在~/.cache/ultralytics/下)里的文件拷过来。注意权重文件名必须和代码里写的一致,改名字会导致找不到文件。

5. 进阶技巧:用 SAM2 做半自动标注流水线

5.1 从掩码反推标注文件

做数据标注时,SAM2 最大的价值不是直接产出标注,而是把标注效率提升 5 到 10 倍。我的做法是:先用 SAM2 自动分割出候选掩码,人工在可视化界面里筛选和修正,最后导出成 COCO 或 YOLO 格式。下面这段代码把掩码转成 COCO 格式的 polygon 标注。

import cv2 import numpy as np import json from ultralytics import SAM model = SAM("sam2.1_t.pt") results = model("image.jpg", points=[[400, 300]], labels=[1]) # 提取掩码并找轮廓 mask = results[0].masks.data[0].cpu().numpy().astype(np.uint8) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 转成 COCO polygon 格式 segmentation = [] for contour in contours: contour = contour.squeeze().tolist() if len(contour) >= 3: # 至少三个点才能构成多边形 segmentation.append([coord for point in contour for coord in point]) coco_annotation = { "image_id": 1, "category_id": 1, "segmentation": segmentation, "area": float(mask.sum()), "bbox": list(map(float, results[0].boxes.xyxy[0].cpu().numpy())) if results[0].boxes else [] } print(json.dumps(coco_annotation, indent=2))

cv2.findContours的第二个参数RETR_EXTERNAL只提取最外层轮廓,避免孔洞产生多余多边形。CHAIN_APPROX_SIMPLE会压缩水平、垂直和对角线方向的冗余点,减少 polygon 点数。注意contour.squeeze()之后如果只剩一个点或两个点,说明掩码太小或太细,这种标注质量不高,建议人工复核。area字段用掩码像素和而不是多边形面积,因为多边形近似会引入误差。

5.2 用 SAM2 做视频分割的帧间传播

SAM2 相比 SAM1 最大的升级是支持视频分割。Ultralytics 接口虽然没有直接暴露视频 API,但你可以逐帧推理,然后用掩码做帧间关联。我的做法是:在第一帧用点提示分割出目标,后续帧用上一帧的掩码边界框作为 prompt,这样目标移动时也能跟住。

import cv2 from ultralytics import SAM model = SAM("sam2.1_t.pt") cap = cv2.VideoCapture("video.mp4") # 第一帧手动指定目标 ret, first_frame = cap.read() results = model(first_frame, points=[[500, 300]], labels=[1]) prev_mask = results[0].masks.data[0].cpu().numpy() frame_idx = 1 while True: ret, frame = cap.read() if not ret: break # 用上一帧掩码的边界框作为当前帧的 prompt ys, xs = np.where(prev_mask > 0) if len(xs) > 0: bbox = [xs.min(), ys.min(), xs.max(), ys.max()] results = model(frame, bboxes=[bbox]) if len(results[0].masks.data) > 0: prev_mask = results[0].masks.data[0].cpu().numpy() frame_idx += 1 if frame_idx % 30 == 0: print(f"已处理 {frame_idx} 帧")

这个方案在目标运动不剧烈时效果不错,但如果目标快速移动或遮挡,边界框会跟丢。改进方法是结合光流做运动补偿,或者每隔 10 帧重新用点提示校正一次。注意prev_mask的尺寸必须和当前帧一致,如果视频分辨率变了,需要先 resize 掩码。这个技巧在医学图像分割的时序数据(比如超声视频)里特别有用,能省掉大量逐帧标注的时间。

5.3 我踩过的最大一个坑

最后说一个我花了整整一个下午才排查出来的问题。有次做广告牌图像分割系统,发现同一张图用model()直接推理和用model.predict()推理,结果居然不一样。后来翻 Ultralytics 源码才发现,model()调用时默认会做 letterbox 填充,而model.predict()在某些版本里不会。这个差异导致图像宽高比变化,掩码位置偏移了十几个像素。我的习惯是:不管用哪个接口,都在推理前手动把图像 resize 到 1024x1024 并记录缩放比例,推理后再把掩码映射回原图尺寸。这样无论框架内部怎么处理,结果都是可控的。做分割这件事,坐标变换的每一步都要有日志,不然出了问题就是黑匣子,连后悔药都没得吃。希望帮到你。

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

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

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

立即咨询