☰
YOLOv8+SAM开集实例分割实战:原理、数据流与部署避坑
2026/10/11 12:37:16 网站建设 项目流程

简介:这是一份结合SAM与YOLOv8实现开集实例分割与目标检测的实战项目,面向计算机视觉研究人员、算法工程师及希望深入理解分割检测原理的学习者。项目覆盖数据准备、模型微调、推理与评估完整流程,核心在于借助SAM的提示分割能力和YOLOv8的高效实时检测优势,解决未见过类别目标的像素级识别问题,对提升模型泛化能力具有参考价值。压缩包内共6个文件,包括3个Python脚本、2个Jupyter Notebook示例和1个流程说明文档,脚本涵盖多目标检测与掩膜可视化等实现,Notebook则提供从基础到单/多目标的逐步演示,配合教程可快速上手复现。包体约1018KB,轻量易部署。该资源已有948人学习下载,内容组织清晰,既适合系统学习算法组合思路,也可作为实际项目的可扩展基础。

1. 开集实例分割的实际痛点:SAM 加 YOLOv8 到底解决什么

做实例分割项目的同学,大概率都遇到过同一个卡点:你辛辛苦苦标了几百张图、训出一个分割模型,客户现场突然指着一种训练集里从没出现过的新目标说“这个我也要框出来”。重新标注、重新训练,少则一周多则半个月,项目节奏直接被打乱。把 SAM 和 YOLOv8 拼到一起,本质上就是为了应对这种“类别表永远跟不上现场需求”的局面:目标检测阶段仍然用 YOLOv8 那种又快又稳的框定位,分割阶段交给 SAM 去做泛化能力极强的前景轮廓提取。两者一前一后组合,就形成了一条实用的开集实例分割 + 目标检测管线。

这条路线的价值不在于打榜指标,而在于交付速度。今天这篇文章,我会把这条组合链路的原理、数据流、可复现代码和部署时最容易翻车的几个细节一次说清,让新手能照着搭,让老手能直接补上自己踩过的那几个坑。

2. 为什么说这是个“检测 + 分割”联合框架:开集能力的核心原理

2.1 开集与闭集的差距:不是检测头里多写几个类名

传统 YOLOv8 做目标检测,最后一个检测头输出的是一张固定长度的类别概率表,比如 COCO 的 80 类。训练时这些类别被锁死,推理时模型只能在“这 80 类里挑一个最可能的”给你。这种做法的本质是闭集假设:所有会出现在场景里的类别,训练集里都已经见过。但实际交付中,这种假设十次有八次不成立。我见过最典型的场景是做鸟类目标检测的数据集,客户今天说只需要麻雀、喜鹊,下周又拿了一批新鸟种的照片过来说“这个也要识别”,如果走重新训练流程,光是标注和调参就能把项目拖垮。

所谓开集,并不是让模型凭空认识一个它从未见过的类别,而是让“分割能力”不会因为类别表的变化而失效。当我们把类别的判定和实例的像素分离后,开集的问题就变成了两个独立的小问题:检测器负责回答“这里有没有一个前景物体、它的框在哪”,分割器负责回答“这个框里哪些像素属于这个物体”。SAM 恰好是一个不挑类别的分割器,给它一个框提示,它能把框内主体的大致轮廓抠出来,它根本不关心这个物体属于哪个物种。

所以在复现标题里这套方案时,我一般会先把工程上的预期管理做好:严格意义上的开集,指的是把检测器也换成具备开放词汇能力的目标检测器,比如 YOLO-World 或 Grounding DINO;如果只沿用原生 YOLOv8,我们可以用“前景区分”的思路,把所有类别统一当作前景,再把框交给 SAM。这个细节很多人容易理解偏,后文第 5 章我会专门讲它的坑。

2.2 YOLOv8 与 SAM 的分工:一个定框,一个抠图

SAM 的提示方式有四种:点提示、框提示、掩码提示和文本提示。但标准开源的 SAM 权重,文本提示实际没法直接用,真正稳定、落地时大家最爱用的是框提示。这正好和 YOLOv8 的输出对上:YOLOv8 的检测头输出 xyxy 的边界框,我们直接把这些框扔给 SAM 作为 prompt,SAM 就会只在这个框范围内做前景/背景分割,输出一个二值掩码。

这个“检测 + 分割”联合框架示意图在论文和开源项目里见过很多回,画出来很清爽:输入图像先进 YOLOv8 检测头,得到若干个检测框;再把原图连同检测框一起送进 SAM 的 Image Encoder 和 Mask Decoder;最后把 SAM 输出的掩码叠回原图。实际工程中,SAM 的 Image Encoder 只需要对整张图跑一次,得到图像嵌入后,每个框只是一个轻量的 Mask Decoder 查询,所以整体推理开销并不是简单的“单张图跑 N 次 SAM”,这一点也是它能上生产的前提。

为什么不直接用 YOLOv8 自带的分割分支去做全套?因为 YOLOv8-seg 的分割头是基于 COCO 训练数据收敛的,它的掩码质量对未见过的类别偏差很明显;而 SAM 在 SA-1B 这类大规模分割语料上见过极大量的物体形态,即使是一个没有语义标签的新类别,它也能根据边界纹理把轮廓补出来。所以这里更合理的分工是:检测器保证速度、负责定位,SAM 负责质量、负责抠图,谁也别越界。

2.3 选型权衡:yolov8n-sam-vit-b 是复现性价比最高的组合

选型这件事直接决定后续能不能顺利部署。先看 YOLOv8 这一侧,它的网络结构图我已经背得很熟:主干 C2f 模块负责特征提取,PAN-FPN 负责多尺度融合,检测头负责回归框和类别。这里面最关键的实践是不要贪大,不是越大的模型就越好。做开集项目时,检测器只是上游的“框来源”,真正拖慢速度的往往是 SAM,所以检测器选 yolov8n 或 yolov8s 通常就够用了。GTX 1660 Ti 跑 YOLOv8n 可以做到实时,而给 SAM 的 ViT-B 做 Image Encoder 编码一张 1024 分辨率图像大约要一两秒,整个管线的瓶颈其实在 SAM 那侧。

SAM 这一侧,官方开源了 vit_b、vit_l、vit_h 三档。标题这类工程复现,我会优先选 vit_b:它体积小、内存占用低,掩码质量比起 vit_h 虽然有差距,但在大多数业务场景里已经足够。如果你后续要部署到 rk3588 这类边缘设备,ViT-B 的算子转换和量化也要比 ViT-L 容易得多。真到了追求极致边界精度的阶段,再考虑用 MobileSAM 或 TinySAM 之类的轻量替代,但这是后话。

综合下来,除非客户有明确的高精度硬指标,否则我的默认配置就是:YOLOv8s 负责检测框,SAM-ViT-B 负责掩码,整套管线在一张 GTX 1660 Ti 上大概能做到单张图 2~4 秒,基本满足准实时的业务交付。

3. 任务流程设计:从目标检测框到 SAM 提示的完整数据链路

3.1 常见流程的成败点:letterbox 坐标还原和原图分辨率

整套流程看起来简单:YOLOv8 出框,SAM 出掩码。但真正把代码跑通后你会发现,框和掩码经常是错位的。这个问题的根源几乎都出在坐标空间不统一上。YOLOv8 推理时默认会对输入图像做 letterbox 预处理,把长边缩放到固定尺寸并加灰边;而 SAM 的set_image内部也会把输入图像按长边 1024 重新缩放。两边各自做了一次缩放,如果你把 YOLOv8 输出的框直接塞给 SAM,掩码偏出目标物体会是必然结果。

正确的数据链路应该这样设计:先用原图画检测框、再做 SAM 提示、最后在原始图像分辨率空间里输出掩码。实际操作时,我一般会让 YOLOv8 先对原图做推理,拿到的是经过它内部 padding 后坐标空间的框;这一步必须先把 letterbox 的 pad 和 scale 还原,把框映射回原始图像坐标;SAM 那边则直接把原始图像交给set_image,让 SAM 用自己内部的自动缩放来处理,我们只管把已经还原好的原始坐标框传进去。这样两个模型各处理各的缩放,交界处只传递“原图坐标”,就不会出坐标偏差。

如果项目里特意要提升小目标分割质量,也可以把 YOLOv8 检测框稍微向外扩 10~20 个像素作为 SAM 的输入框,给 SAM 多留一些边缘上下文。这个技巧对鸟类目标检测的数据集这类小目标场景特别有效,后面第 6 章会展开。

3.2 图像预处理代码:把检测框对齐到 SAM 的输入空间

这里给出一段我在本地复现流程时经常用来做坐标对齐的代码。它解决的是 YOLOv8 的 letterbox 输出与原始图像之间的映射关系:

import cv2 import numpy as np def letterbox_to_original(boxes, scale, pad, original_shape): """ 把 YOLOv8 内部 letterbox 坐标还原回原始图像坐标。 参数: boxes: YOLO 输出的 xyxy 数组, shape 为 (N, 4) scale: letterbox 缩放比例, 从 YOLO 预处理结果中拿到 pad: (left, top) 灰边偏移量 original_shape: 原始图像 (H, W) 返回: 还原到原始图像坐标系的 xyxy 数组 """ boxes = boxes.copy() # 先减去 pad, 再除以 scale boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad[0]) / scale boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad[1]) / scale # 防止浮点误差把框推出去 boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, original_shape[1]) boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, original_shape[0]) return boxes.astype(np.float32)

这段代码的逻辑很直白:YOLO 在做 letterbox 时,先按比例缩放图像到目标尺寸,再把剩余区域用灰边填充。因此它的框坐标是“缩放后图像”的坐标,要还原回去,就必须先减掉灰边偏移量、再除回缩放比例。后面两个np.clip是保险操作,因为坐标落在边界外时 SAM 的框提示会产生异常 mask。

拿到 scale 和 pad 的方式,常见做法是在调用YOLO.predict时打开verbose=True,从日志里读;更稳妥的是直接对输入图调用letterbox函数手动预处理,然后传给YOLO并关闭自动预处理。我一般倾向后者,因为能明确定义坐标空间,少猜一次。

3.3 预处理统一是“玄学”?其实是 RGB/BGR 和归一化没对齐

很多新手在跑通这个流程后,发现 SAM 分割出来的掩码虽然位置对了,但边缘像被狗啃过,或者某些目标整个丢掉。这种问题十有八九不是模型能力不行,而是图像颜色空间不统一。YOLOv8 的predict方法默认读入 BGR 图像,内部也按 BGR 处理;而 SAM 的 Image Encoder 期望的是 RGB 输入。如果你用 OpenCV 读图后直接传给 SAM,通道顺序反了,分割质量会肉眼可见地下降。

所以我在流程里会固定一个标准:所有图像统一用 OpenCV 读取,转成 RGB 后再给 SAM;YOLO 那边为了保持稳定也主动传 RGB 数组,并且把device、half这些参数固定下来。此外,EXIF 旋转也是容易被忽略的坑。手机拍出来的照片可能带方向信息,OpenCV 读图时不会自动旋转,而 YOLO 内部会做某些处理,两边不一致时,框和掩码的整体位置就会偏。保险的做法是在进入管线前,先用cv2.getRotationMatrix2D根据 EXIF 把图像摆正,再做后续一切操作。

我的习惯是封装一个统一入口函数:输入一个图像路径,输出被摆正的、RGB 顺序的、且带有原图尺寸信息的字典。这样后续无论调 YOLO 还是 SAM,拿到的都是同一个“标准视图”,两边的坐标空间也只用维护一份映射关系。把预处理做到这一步,后面部署到 rk3588 上做推理,需要排查的点才不会被坐标问题干扰。

4. 核心代码:用 Python 把 YOLOv8 的检测框转成 SAM 提示并输出掩码

4.1 OpenSetSegmenter 类的最小可运行实现

把 YOLOv8 和 SAM 串起来,最朴素的方式是写一个类,内部持有两个模型实例。下面这段代码是我在本地复现流程时常用到的最小骨架,去掉了所有项目专属逻辑,只保留核心链路:

import cv2 import numpy as np from ultralytics import YOLO from segment_anything import sam_model_registry, SamPredictor class OpenSetSegmenter: def __init__(self, yolo_weights, sam_checkpoint, device="cuda"): self.device = device self.yolo = YOLO(yolo_weights) sam = sam_model_registry["vit_b"](checkpoint=sam_checkpoint) sam.to(device) sam.eval() self.predictor = SamPredictor(sam) def detect_boxes(self, image_rgb, conf=0.3, iou=0.7, max_det=100): # image_rgb 已确保是 RGB 顺序 results = self.yolo.predict( source=image_rgb, conf=conf, iou=iou, max_det=max_det, classes=None, # 不过滤类别, 把所有前景都当作候选 agnostic_nms=True, # 不同类别之间统一做 NMS verbose=False ) boxes = results[0].boxes.xyxy.cpu().numpy() return boxes def segment_boxes(self, image_rgb, boxes): # 注意: set_image只需要调用一次, 整张图的embedding会被缓存 self.predictor.set_image(image_rgb) all_results = [] for box in boxes: masks, scores, _ = self.predictor.predict( box=box, multimask_output=True, # 返回 3 个候选掩码 ) best = int(np.argmax(scores)) all_results.append({ "box": box.astype(int), "mask": masks[best], "score": float(scores[best]), }) return all_results

代码的逻辑说明如下:detect_boxes负责拿到原始坐标的边界框,segment_boxes负责把每个框送给 SAM 并取回当前框的最佳掩码。这里有一个值得重视的参数agnostic_nms=True。开集场景下,我们不关心物体属于哪一类,类别编号已经没有意义,如果关闭类别无关 NMS,同一个物体会因为类别概率抖动被留下多个重复框,SAM 会莫名其妙地分割出同一个物体的多个变体。设置成 True 后,所有框统一按空间重叠度去重,整个输出的稳定性明显提升。

multimask_output=True是 SAM 的一个很聪明的机制。它会对同一个 box 生成三个不同细化程度的候选掩码,scores代表 SAM 对每个候选的置信度。取分数最高的那个通常最稳,但如果你想抠出更精细的边缘,也可以手动从三个里挑一个 IoU 更大的,这个后处理项会在 4.2 节展开。

4.2 参数怎么调:conf、iou、multimask_output、max_det 的取值参考

新手最容易犯的错是把 YOLO 当开集检测器用时,直接把conf调到很低,结果一张图出来几百个框。下表是我通常采用的参考值,能在框召回和效率之间取一个平衡:

参数建议值说明
conf0.25 ~ 0.35开集场景不要低于 0.2,低于 0.2 垃圾框会大幅增加
iou0.7用于 NMS 去重;目标密集时可降到 0.65
agnostic_nmsTrue开集下强制开启,避免同物多框
max_det100防止高密度场景把 SAM 的推理次数拉爆
classesNone保留所有类别输出,由后续 mask 质量二次筛选

上面这些参数落实下去后,你得到的results里会带上原始图像尺寸的掩码。SAM 预测返回的mask是 float32 类型,数值为 0 或 1,并且尺寸和输入给set_image的图像完全一致,不需要额外 resize。很多人在这一部会画蛇添足地对掩码做缩放,结果边界直接漂移掉,这点一定要忍住。

4.3 给 0 基础读者留的命令行入口:不碰网络结构也能跑通

完整包装成命令行脚本后,入口长这样:

python run_open_set.py \ --source ./images/ \ --yolo-weights yolov8s.pt \ --sam-checkpoint sam_vit_b_01ec64.pth \ --conf 0.30 \ --max-det 100 \ --output ./output/

对应解析参数并执行主流程的逻辑也很直白,先遍历目录,读图,转 RGB,然后依次调用detect_boxes和segment_boxes,最后用cv2.imwrite把掩码叠在原图上。对 0 基础读者来说,真正值得关注的是两件事:一是权重文件的路径必须对应上实际文件;二是--source接受的是目录而不是单张图时,要有意识地处理视频帧的抽取。这个命令行的价值在于,即使你不理解 SAM 的 prompt 原理,也可以先把端到端流程跑通,再回头改参数。

跑通之后,建议你立刻做一个可视化小工具:把每张图上叠加掩码的结果单独存盘。因为 SAM 在未见类别上会有肉眼可见的“探索性分割”,你只有多看结果才能就知道conf和multimask_output应该往哪个方向调。光看控制台日志是定位不了这些问题的。

5. 避坑排查与快速验证:复现这套流程最容易翻车的三个环节

5.1 坑一:分割掩码老是错位一个固定偏移,甚至飘到图外

现象:YOLOv8 的框明明准确框住了物体,SAM 生成的掩码却整体向某个方向偏移,小目标上尤其明显,有时候掩码直接落在目标旁边。

原因:几乎可以肯定是坐标空间没有统一。YOLO 内部 letterbox 后的框,直接被 SAM 当成了原图坐标的提示;或者你给 SAM 用的是 resize 后的图,却拿原始坐标的框去提示。另一个常见变体是把 BGR 图像误传给 SAM,通道顺序导致的特征偏差会让掩码产生系统性漂移。

解决:每次调用 SAM 前,先把检测框映射回原始图像坐标。最稳妥的做法是用第 3.2 节里的letterbox_to_original函数,保证进入segment_boxes的boxes是原始分辨率。如果发现掩码仍偏一点,打印results[0].boxes.orig_shape和image_rgb.shape来核对两个图像的尺寸是否一致。这个打印习惯能帮你定位八成以上坐标问题。

5.2 坑二:SAM 一上来,显卡直接爆显存

现象:GTX 1660 Ti 跑 YOLOv8 非常流畅,帧率感人,但把 SAM 串进来后,显存占用立刻飙升,甚至直接 OOM;把 batch 调到 1 也无济于事。或者部署到 rk3588 上,YOLOv8 转 NPU 后速度尚可,SAM 的 ViT 算子却各种不支持,跑一遍比 CPU 还慢。

原因:SAM 的 Image Encoder 本身就要占用不小的显存和计算量,而且代码里如果无意间在循环中对每个框重复调用了set_image,等于每处理一个框就把整张图的 embedding 重新算一遍,资源被反复浪费。边缘设备上的算子适配问题则是另一个层次:NPU 对 ViT 的算子支持并不完整,部分算子会走到 CPU 回退,速度反而更慢。

解决:核心代码里已经把set_image提到segment_boxes的外层循环之前,这一步必须遵守。如果你发现显存还是吃紧,就把输入图像缩放到 768 或 640 再进 SAM,精度会略降但显存压力骤减;如果是 rk3588 这样的板子,通常的做法是把 SAM 转成 ONNX 后分段部署,只把 Mask Decoder 放到算力有限的设备上,Image Encoder 放到服务端。这条路虽然丑,但在边缘交付场景里管用。

5.3 坑三:开集检测结果里全是“垃圾框”,SAM 把背景也抠出来了

现象:把conf调低后,图像里的边缘纹理、天空、阴影都被 YOLO 当成前景框了出来,SAM 也老老实实地把这些背景区域分割得轮廓分明,结果界面上全是噪点掩码。

原因:这个坑来自对“开集”的误解。YOLOv8 原生检测头仍然是闭集的,它在 COCO 上学到的 decoder 只会对接近训练样本分布的图像区域给出较高置信度,并不能从逻辑上理解“这到底是不是前景区”。当你关掉类别过滤时,它其实只是把输出层所有类别的概率堆在一起,并不表示它具备开放词汇识别能力。

解决:我一般用两层过滤来止损。第一层是 SAM 侧的稳定性分,得分低于 0.8 的掩码直接丢掉;第二层是空间先验,检测框中心周围四分之一的区域必须与掩码中心区域有足够重叠,否则认为 SAM 没“聚焦”到框内主体。如果项目允许引入额外模型,我更建议把 YOLO 换成具备开放词汇能力的检测器,把“开集”真正落实到检测层。老实说,只靠 YOLOv8-SAM 做严格意义的开集是做不到的,它更像是一个“类别不可知的优质分割器”,这个预期要先对齐。

5.4 坑四:掩码有空洞、边缘像果冻

现象:SEM 输出的掩码边缘出现大量锯齿,内部偶尔有空洞;同一个物体,换一帧图像掩码面积忽大忽小。

原因:multimask_output=True返回的三个候选掩码,不一定都是“紧致主体”的。SAM 的最佳候选可能更偏轮廓,也可能更偏子区域,取argmax(scores)虽然是默认做法,但并不是在所有场景都最优。另外,输入图像本身分辨率过低,SAM 的上采样分支拿不到足够的边缘信息,也会出现果冻边缘。

解决:可以先把三个候选掩码都拿到,计算它们与检测框的 IoU,选一个与框重叠度高且内部空洞较少的;如果还是不满意,就把检测框向外扩一点再补一次分割,让 SAM 看到更多边界上下文。至于内部空洞,一个很实用的后处理是用scipy.ndimage.binary_fill_holes把掩码内部的孔洞填平。这一步对后续做目标裁剪、求最小外接矩形帮助很大。

排查到这里,还有一条经验想分享:做这套流程时建立“一天只看图,不看指标”的习惯。SAM 的误差模式和 YOLO 完全不同,前者是边界级误差,后者是语义级误差,光用 mAP 判断好坏很容易被平均结果骗过去。每次改参数后,把输出图片在屏幕上过一遍,比盯十轮监督指标都管用。

6. 进阶验证方法:三张卡判断可交付性,以及小目标分割的提速技巧

验证这套开集实例分割方案能不能交付,我一般不看花哨的总体指标,而是准备三张卡:第一张是“高密度小目标图”,看 SAM 能否在每个小框内保持轮廓完整;第二张是“未见类别图”,大概率是从客户现场采集的、训练集里完全没有过的物体,看掩码是否仍然能贴合边界;第三张是“低对比度图”,目标与背景颜色接近,看框提示之后 SAM 能否稳住前景。三张图定生死,比跑一遍完整测试集更快。

配合验证,我习惯把两个附属信号也记录到工程里。一个是 YOLOv8 训练侧的损失函数曲线图,如果检测器本身收敛不稳定,框的抖动会直接传导给 SAM;另一个是给 YOLO 的特征图做可视化热力图,确认它把注意力放在目标主体而不是背景纹理上。这两个信号不算这套流程的必需品,但能帮你在“检测框导致掩码质量下降”和“SAM 本身能力不足”之间做出更干净的归因。

小目标场景里还有一个很好用的提速技巧:不要总把整张图交给 SAM,而是先从原始图像里按检测框位置裁剪出带 padding 的局部图,把局部图作为 SAM 的新输入。这样做有两个效果:一是让 SAM 在更高有效分辨率下看到目标,边缘精度明显提升;二是裁剪后的图更小,Image Encoder 的编码耗时反而下降。裁剪后记得把掩码按坐标偏移搬回原图,代码逻辑如下:

def crop_with_padding(image, box, pad_ratio=0.25): x1, y1, x2, y2 = [int(v) for v in box] w, h = x2 - x1, y2 - y1 px, py = int(w * pad_ratio), int(h * pad_ratio) x1 = max(0, x1 - px) y1 = max(0, y1 - py) x2 = min(image.shape[1], x2 + px) y2 = min(image.shape[0], y2 + py) crop = image[y1:y2, x1:x2] local_box = np.array([x1 - x1, y1 - y1, x2 - x1, y2 - y1]) return crop, local_box, (x1, y1)

用这段逻辑把裁剪图交给 SAM 后,得到的 mask 尺寸和裁剪图一致,最后只需要按(x1, y1)偏移把 mask 贴回原图即可。这个“局部放大”的思路,和我在遥感目标检测场景里用到的策略是相通的,对边缘细节要求越高的业务收益越明显。

最后说一个我自己在这类项目里养成的习惯:永远不要把 SAM 的输出直接当最终交付物,一定要预留一个“人工修正层”,可以是边缘微调,也可以是最小外接矩形的规则约束。SAM 的分割能力再强,它也是基于视觉信号而不是业务语义的,客户真正关心的可能是某个零件的最外侧边界,而不是 SAM 认为的视觉边界。当前这套流程,最值得投入的方向,依然是对不可见类别的像素级泛化能力,它能让你的目标检测系统在类别表失控时有兜底方案。希望这个思路和代码骨架,能帮你少走我当年走过的弯路,也希望你在自己的项目里踩到新坑之后,记得先检查坐标空间,再检查颜色空间,最后再怀疑模型能力,希望帮到你。

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

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

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

立即咨询