news 2026/10/12 1:44:20

YOLOv8结合SAM实现开集实例分割的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv8结合SAM实现开集实例分割的工程实践

简介:一套面向计算机视觉研究与工程实践的资源,将Meta推出的SAM分割模型与YOLOv8检测框架相结合,专为需要实现开集实例分割与目标检测的场景而设计,适合算法工程师、科研人员和有一定基础的视觉学习者。压缩包共6个文件,包括3个Python脚本、2个Jupyter Notebook和1个Markdown说明文档,整体仅1018KB,轻量却覆盖了核心实现与演示。已有948人学习,这样的热度也从侧面反映出项目对实践者的适用性。源码针对多目标检测、单/多目标SAM分割、掩码可视化等任务提供具体实现;Jupyter Notebook分步演示从模型加载到推理的完整流程,说明文档则涵盖环境配置与运行方式。项目还融入了数据准备、模型微调、推理与评估等环节,能帮助使用者理解如何通过算法组合应对训练集中未曾出现的新类别实例,掌握开集分割的工程化思路,提升模型在真实场景中的泛化能力与鲁棒性。

1. 开集实例分割:为什么要把 SAM 和 YOLOv8 绑在一起用

当你第一次遇到"模型没见过这个类别,但也要把它抠出来"的需求时,YOLOv8 的固定类别头基本是无解的——只能重新标注、重新训练。而 SAM 虽然什么都能分割,但它不知道"哪个物体是你要的"。把 YOLOv8 的检测框作为 prompt 喂给 SAM,让检测器告诉 SAM"看哪里",SAM 负责"抠多细",这就是开集实例分割最务实的组合拳。这份项目源码把整条链路打包了:检测、分割、可视化、转标注一条龙。适合想快速落地开集检测+分割的工程师,也适合刚入门想搞清楚两个模型怎么配合的新手。

2. 组合思路与架构设计:检测器给提示,分割器出掩码

2.1 为什么是"开集":YOLOv8-seg 的边界在哪

YOLOv8 官方其实自带分割能力,很多人拿到这个项目的第一反应是"为什么不直接用 YOLOv8-seg"。这里面的核心差异就在"开集"二字。YOLOv8-seg 是一个封闭集模型:训练时类别是固定的,推理时类别头输出的概率分布就锁定在这些类上。遇到没见过的物体,它要么直接不检测,要么强行分到某一个已知类别里。对于工业场景——比如质检线上出现的新缺陷、果园里出现的新品种果实——封闭集是处理不了的。

SAM 的训练目标是"给任意 prompt,分割出 prompt 对应的目标区域",它不需要类别概念。所以无论是见过的还是没见过的物体,只要 prompt 给得准,它都能输出精细掩码。两个模型接起来,YOLOv8 承担"找目标"的职责,SAM 承担"抠轮廓"的职责,未知类别的分割能力就这样补上了。

从工程角度看,这种组合还有一个隐性收益:你不需要重新训练检测器。原来 YOLOv8 已经能稳定检测的类别,行为完全不变;对新增的未知类别,检测器负责给出一个粗略的框,SAM 负责把边界补精细。这种"检测 + 分割"联合框架非常适合快速迭代的场景,新增一类目标时,只需要验证检测器能不能框住它,而不用重新做一套分割数据。

2.2 整体数据流:检测框如何变成 SAM 的 prompt

整个推理链路拆成三段看。第一阶段是检测:输入一张图,YOLOv8 输出每个目标的边界框(xyxy)、类别和置信度。第二阶段是过滤:按置信度阈值过滤低质量框,保留可靠的框,YOLOv8 内部自带 NMS,所以这一步通常只做阈值过滤就够了。第三阶段是分割:把过滤后的框作为 box prompt 传入 SamPredictor 的预测接口,SAM 输出对应的实例掩码,最后叠加可视化。

这里有一个关键机制值得花时间理解:SAM 的图像编码是"一次编码、多次复用"的。SamPredictor 的 set_image() 方法会先把整张图像送入 Image Encoder,生成一个高维图像嵌入并缓存在内存里。之后每个检测框只需要走轻量的 Prompt Encoder + Mask Decoder,耗时极短。所以正确的做法是:一张图只 set_image 一次,然后循环所有检测框。

不少初学者把 set_image 放在循环里,对每个框重新编码一次图像,速度直接慢 5~10 倍,显存也容易爆。这个坑我复现过一次之后就再也没犯过——看显存占用曲线就能发现问题,ViT 编码器吃掉的显存占了整个流程的 80% 以上。

2.3 两种串联方案:box prompt 与 mask prompt 的取舍

项目源码里主推的是 box prompt 路线,直接把 YOLOv8 输出的 xyxy 框传给 predict()。这也是我在落地项目里用得最多的方案,代码简单、对框的质量不敏感,框稍微偏一点 SAM 也能稳住边缘,因为 SAM 的 prompt 编码器对 box 有较强的鲁棒性。

另一种路线是 mask prompt:第一次分割完成后,把生成的粗掩码作为二次 prompt 再喂给 SAM,精细度通常会更好,但对前一次掩码的质量有依赖,多一次推理,代码也要多写一段回填逻辑。实测在轮廓复杂、目标重叠的场景下,二次迭代能带来约 2~3 个点的 mask IoU 提升;在简单场景下基本没区别。

方案prompt 类型单目标耗时掩码精细度代码复杂度
box prompt检测框 [x1, y1, x2, y2]低高低
mask prompt前一次分割掩码高更高中

我一般建议先跑 box prompt 做基线,验证整体流程能跑通之后,再针对边缘要求极高的业务升级成 mask prompt。反过来直接上 mask prompt 的做法,新人很容易在调试阶段分不清问题出在检测还是分割,排查成本很高。

3. 环境搭建与工程复现:从裸机到跑通 demo

3.1 版本兼容矩阵与安装命令

这个组合项目对版本比较敏感,ultralytics 的 API 在 8.x 版本里变动过多次,SAM 官方库的依赖也可能和 torch 版本冲突。我实测可用的组合如下:

依赖库版本说明
ultralytics8.2.0YOLOv8 推理与权重管理
segment-anything1.0SAM 官方实现
torch2.1.2CUDA 12.1 下编译
opencv-python4.9.0.80图像读写与可视化
numpy1.26.4数组处理

创建虚拟环境并安装依赖:

conda create -n sam_yolo python=3.10 -y conda activate sam_yolo pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics==8.2.0 segment-anything==1.0 opencv-python==4.9.0.80 numpy==1.26.4

安装完成后先验证一个又一个 import 是否正常,再下载两个模型权重:YOLOv8 官方权重(yolov8s.pt)和 SAM 的官方权重(sam_vit_b_01ec64.pth)。SAM 的 vit_b 权重体积约 375MB,yolov8s.pt 约 22MB,两个文件放到项目的 weights 目录下就行。注意权重文件不要放在中文路径下,SAM 加载权重时对路径编码有概率翻车。

3.2 项目目录结构与核心文件职责

解压源码包后,目录结构大致如下:

sam_yolo_project/ ├── main.py # 推理主入口:检测+分割+可视化 ├── utils/ │ ├── prompt.py # 检测框转 SAM prompt 格式 │ ├── visualize.py # 掩码叠加与结果保存 │ └── labels.py # COCO 格式标注导出 ├── config.yaml # 置信度/阈值/模型路径等配置 ├── weights/ # 模型权重存放目录 └── data/ ├── images/ # 测试图像 └── outputs/ # 可视化结果与标注文件

config.yaml 是项目的"总控台",所有可调参数都集中在里面。给一个最小可用的配置模板:

detector: model_path: weights/yolov8s.pt conf: 0.25 iou: 0.45 max_det: 100 sam: checkpoint: weights/sam_vit_b_01ec64.pth model_type: vit_b device: cuda pred_iou_thresh: 0.88 stability_score_thresh: 0.95 multimask_output: false preprocess: image_size: 640 use_half: true

配置里的参数会在 main.py 启动时统一加载,后续调优直接改这个 YAML 文件,不用动代码。我把这套习惯保留到了所有项目里:参数外置到配置文件,代码只负责逻辑。好处是实验不同阈值时不用反复编辑 Python 文件,也不会改坏逻辑。

3.3 最小推理脚本:单张图跑通全流程

主入口的逻辑我用最简版本剥一遍:

import cv2 import torch from ultralytics import YOLO from segment_anything import sam_model_registry, SamPredictor from utils.prompt import boxes_to_prompt from utils.visualize import draw_masks # 加载模型,SAM 需要先指定 device detector = YOLO("weights/yolov8s.pt") sam = sam_model_registry["vit_b"](checkpoint="weights/sam_vit_b_01ec64.pth") sam.to("cuda") predictor = SamPredictor(sam) # 读取图像,注意通道顺序统一 image = cv2.imread("data/images/demo.jpg") image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 第一步:YOLOv8 检测 results = detector(image_rgb, conf=0.25, iou=0.45, max_det=100) # 第二步:提取检测框,形状为 [N, 4] boxes = results[0].boxes.xyxy.cpu().numpy() # 第三步:SAM 图像编码 + 逐框分割 predictor.set_image(image_rgb) masks = [] for box in boxes: mask, score, _ = predictor.predict( box=box, multimask_output=False ) masks.append(mask[0]) # 第四步:可视化并保存 result_img = draw_masks(image, masks) cv2.imwrite("data/outputs/result.jpg", result_img)

代码逻辑说明:第一段加载两个模型;第二段把 BGR 图像转 RGB,YOLO 和 SAM 的预训练输入都是 RGB 格式,这是两个模型都能正确推理的前提;第三段是核心串联逻辑,detector 输出 N 个 xyxy 框,predictor.set_image() 做一次图像编码,循环 predict() 只跑轻量解码;第四段把掩码叠加回原图并保存。

参数说明:conf=0.25 是检测置信度阈值,业务要求高精度往上调到 0.5,要求召回往下压到 0.1;max_det=100 限制最多输出 100 个框,显存紧张时调小能保住内存;multimask_output=False 让 SAM 每个 prompt 只返回一个掩码,后续处理不用做候选选择。跑通之后,图像输出目录里就能看到带彩色掩码的结果图了。

4. 核心代码走读与参数调优:四个关键点掰开讲

4.1 检测分支:conf、iou、max_det 三个参数的取舍

conf 是最直观的开关。记住一个原则:conf 调高,误检少但漏检变多;conf 调低,召回高但可能把背景当目标。在这个项目里建议先跑一遍 conf=0.1,把输出可视化逐张看,摸清模型在当前场景下的置信度分布,再定最终阈值。一般工业场景落在 0.25~0.4 之间,如果是监控场景我习惯先看误检代价再决定方向。

iou 是 NMS 的去重阈值。iou=0.45 表示两个框重叠面积超过 45% 时认为它们是同一个目标,保留高置信度的那个。目标密集的场景(人群、果实、货架)建议降到 0.35 以下,否则相邻目标的框会被误杀;目标稀疏的场景保持 0.45~0.5 即可。这个参数调起来很微妙,我见过项目组把 iou 从 0.45 调到 0.3 之后,检测数量直接涨了 20%,但误检也涨了。

max_det 限制了单张图最多保留多少个框。它不仅是数量截断,更是显存保护开关。SAM 的分割循环是逐框跑的,框越多耗时越长,显存占用也会因为解码器缓存持续累积。经验值是普通街景 100 够用,密集小目标场景放到 300,再往上有意义但成本陡增。

4.2 SAM 参数:pred_iou_thresh、stability_score_thresh 与 multimask_output

这三个参数控制 SAM 输出掩码的质量和数量。pred_iou_thresh 是 SAM 对掩码与真实目标之间 IoU 的预测值阈值,低于这个值的掩码会被当作低质量结果过滤,默认 0.88。调高这个值,保留的掩码更干净,但可能丢掉边缘模糊的目标;调低则保留更多候选,噪声也会进来。stability_score_thresh 是稳定性分数阈值,衡量掩码在 logits 扰动下的稳定性,默认 0.95。对于低对比度边缘,比如白色物体在白色背景上,这个阈值建议降到 0.90。

multimask_output 决定一次 prompt 返回几个掩码。False 时返回一个最佳掩码;True 时返回三个不同粒度的候选。项目源码默认 False,掩码体感最干净。如果发现细长物体(电线、树枝、杆状物)的边缘被过度平滑,可以改成 True 然后取 IoU 分数最高的候选,通常能救回一些细节。

实际调参时不要同时动多个参数,一次只动一个,用同一批图像做对比。我就是因为一次改了三个参数,花了半下午才定位到是 stability_score_thresh 导致掩码抖动。

4.3 类别过滤与开集标签映射

场景:你只想分割"瓶子和杯子",但 YOLOv8 输出 80 个 COCO 类别。在串联 SAM 之前过滤类别 ID:

# 只保留属于 pick_classes 的检测框 pick_classes = [39, 41] # COCO: 39=bottle, 41=cup boxes = [] keep_ids = [] for i, r in enumerate(results[0].boxes): cls = int(r.cls.item()) if cls in pick_classes: boxes.append(r.xyxy.cpu().numpy().tolist()) keep_ids.append(cls)

这段代码的逻辑是:遍历 YOLOv8 输出的每个框,检查类别 ID 是否在目标集合里;过滤后同时保留类别 ID,方便后续给未知类别做标签映射。开集标签映射的做法是:对已知类别沿用 YOLO 类别 ID;对未知类别(比如新发现的缺陷类型),自定义 ID 从 100 开始递增,避免和现有类别冲突。这样生成的标注文件进训练流程时不会打架。

4.4 性能优化:半精度、图像尺寸与批处理

use_half=True 是性价比最高的一项优化。SAM 的 vit_b 在 fp16 下推理,显存占用接近减半,速度提升 30% 左右。做法是模型加载后调用 .half():

sam = sam_model_registry["vit_b"](checkpoint="weights/sam_vit_b_01ec64.pth") sam.to("cuda") sam.half() # 输入图像也要转成 float16 image_tensor = torch.from_numpy(image_rgb.transpose(2, 0, 1)).unsqueeze(0).half().cuda()

模型转半精度后,所有输入 tensor 必须同步转 float16,否则会报 dtype 不匹配。另一个优化点是 set_image 只做一次,所有检测框复用同一个图像嵌入;如果对每一帧都重新编码,性能完全不可接受。视频流场景可以考虑把多帧拼成 batch 做 SAM 编码,吞吐还能再涨,但显存峰值也会上升,建议从 batch=1 逐步加到 4,盯着显存监控调。

5. 避坑指南:复现这个项目最常见的五个坑

5.1 显存爆掉:CUDA out of memory

现象:跑 demo 时程序崩溃,报 CUDA out of memory,卡在 SAM 的 set_image 或者第一次 predict。

原因:SAM 的 Image Encoder 是 ViT 结构,本身就是显存大户,8G 显存跑 vit_h 必爆,vit_b 在 6G 卡上也接近极限。另一种常见情况是把 set_image 写进了循环,每处理一个框都重新编码一次图像,显存和耗时同时爆炸。

解决:换 vit_b 权重,这是显存和精度的平衡点;开启半精度推理,显存直接减半;确保一张图只 set_image 一次;还爆就把 max_det 从 100 降到 50。如果 4G 显存也要跑,只能牺牲精度换 vit_tiny 或者其他轻量版分割模型。

5.2 掩码错位:分割区域和物体对不上

现象:掩码出现在图像上完全不相关的位置,或者轮廓大面积偏移,看起来像分割了另一处纹理。

原因:通道顺序问题占了八成的概率。YOLO 和 SAM 的预训练输入都是 RGB,但 cv2.imread 默认读的是 BGR,如果直接把 BGR 图像传给 SAM 编码器,它看到的颜色分布完全反了,深色浅色语义颠倒。

解决:所有图像入口统一做一次 cv2.cvtColor(image, cv2.COLOR_BGR2RGB),推理结束要保存可视化结果时再转回 BGR。从那以后我每次接新场景都强制走一遍"检测框可视化→SAM分割→掩码叠加"三步检查,确保两个模型喂进去的东西是对的。

5.3 检测框为空:分割没有输入

现象:程序不报错,但结果图干干净净,一个 mask 都没有。打印 results[0].boxes 发现是空数组。

原因:conf 阈值设太高,把置信度不高的目标全过滤了;或者目标本身很小,经过 YOLO 的下采样之后特征丢失。还有一种情况是图像直接 resize 到 640x640,宽高比被拉伸,目标变形导致检测失败。

解决:先用 conf=0.1 跑一遍排查是不是阈值问题;图像缩放用 letterbox 保持宽高比,不要直接扭曲;小目标场景把输入尺寸从 640 提高到 1280,代价是推理时间翻倍。这一套排查顺序我固定了下来,每次先看框再看 mask。

5.4 版本不匹配:ultralytics 接口变动

现象:AttributeError: 'Boxes' object has no attribute 'xyxy',或者 predict 接口报参数错误。

原因:ultralytics 在 8.0 到 8.2 之间改过接口,早期版本用 results[0].boxes.boxes 取坐标,后面版本改成 xyxy,predict 的参名也调整过。直接装最新版和项目源码配套的版本不一致时,这类报错几乎必现。

解决:锁定 ultralytics==8.2.0 按项目依赖安装;不想锁版本就用 results[0].boxes.data 取原始 tensor,这个字段在多个版本里都稳定。注意 SAM 的 checkpoint 路径也要确认存在,权重文件缺失时 SAM 加载会直接抛 FileNotFoundError,容易误判成接口问题。

5.5 CPU 推理慢到怀疑人生

现象:一张 640x640 的图,跑完整个流程要一分多钟,YOLO 部分很快,慢的是 set_image。

原因:SAM 的 ViT Image Encoder 在 CPU 上非常慢,一次图像编码就要几十秒,这不是代码能优化的,是结构性的计算量问题。

解决:显存不够的机器,换 vit_b 加半精度,能压到 4G 显存以内;或者把 SAM 的图像编码部分导出成 ONNX,用 ONNX Runtime 的 CPU 优化跑,实测比 PyTorch CPU 快两到三倍。如果一定要纯 CPU 部署且对速度有要求,这个组合方案可能不适合,建议直接用轻量分割模型换 SAM。

6. 进阶:把掩码转成标注,反哺 YOLOv8 训练

6.1 掩码转多边形标注

项目落地时往往需要把 SAM 生成的掩码转成 YOLOv8-seg 的训练格式。YOLO 分割标签存的是多边形顶点,不能直接用像素掩码,转换逻辑用 OpenCV 的轮廓提取就能完成:

import cv2 import numpy as np def mask_to_polygon(mask: np.ndarray, epsilon: float = 0.001) -> np.ndarray: contours, _ = cv2.findContours( mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if len(contours) == 0: return None contour = max(contours, key=cv2.contourArea) polygon = cv2.approxPolyDP( contour, epsilon * cv2.arcLength(contour, True), True ) return polygon.reshape(-1, 2)

参数说明:epsilon 是多边形简化系数,0.001 表示保留几乎所有细节,适合高精度标注;转训练标注时可以用 0.005 减少顶点数,降低标注文件体积,也更容易被 YOLO 的后处理接受。一个大原则是:SAM 输出的掩码通常比人工标注更精细,直接转 YOLO 格式后顶点数可能上百个,训练时反而容易过拟合到噪点,简化是必要的一步。

6.2 自动化标注流水线

把上面的函数串起来,可以形成一条半自动标注管线:遍历目录下所有图像,YOLO 检测→SAM 分割→每个 mask 转 polygon→写入 COCO JSON。写入前加一道过滤:只保留置信度大于 0.7 的框对应的掩码,把边缘 case 留给人工检查。这样自动标注的准确率能控制在 95% 以上,然后训练一个初版 YOLOv8-seg 模型,再人工修正,迭代两轮之后模型效果会越来越接近 SAM 的上限。

我常用的评估指标是 COCO 格式里的 segmentation 字段,polygon 顶点归一化到图像宽高。YOLOv8-seg 训练时要求每个 polygen 至少三个点,少于三点的 mask 直接丢弃,这个坑在批量转换时很容易踩,建议转换后做一个顶点数量的断言检查。

6.3 验证分割质量:抽 20 张图算 mIoU

拿到一批自动标注数据后,抽 20 张图,手工画一遍真实 mask,和 SAM 输出的 mask 计算逐像素 IoU:

def compute_iou(mask_a, mask_b): intersection = np.logical_and(mask_a, mask_b).sum() union = np.logical_or(mask_a, mask_b).sum() return intersection / union if union > 0 else 0

如果平均 IoU 低于 0.85,说明 SAM 的分割质量不适合直接做训练标注,回到 prompt 质量问题上——大概率是 YOLO 的框不贴合目标。调整 conf 和 iou 阈值,或者换更大的 yolov8l 模型,通常能救回来。从那以后我每次生成一批自动标注,都强制抽 20 张算一遍 mIoU 再决定要不要进训练集,这个习惯帮我省下了好几轮无效训练的时间。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 1:40:53

ESP8285+MQTTX:电机控制器物联网接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 1:39:16

AnyPS5实战:用SQLite构建本地游戏库管理与统计工具

1. 游戏库从第三十款开始失控:我为什么要写AnyPS5说实话,我的PS5游戏库大概从第三十款开始就彻底失控了。当时我对着主机里的游戏列表想找某款回合制RPG,想了半天没想明白它到底是实体盘还是数字版、当时多少钱入的、还差几个奖杯能白金。群里…

作者头像 李华