基于 Ultralytics YOLO 的分割对象隔离指南:从推理结果中提取纯净目标图像
【免费下载链接】yolov10YOLOv10: Real-Time End-to-End Object Detection [NeurIPS 2024]项目地址: https://gitcode.com/GitHub_Trending/yo/yolov10
导读
本指南讲解如何在使用 Ultralytics YOLO 完成分割任务推理之后,将检测到的各个目标对象从原始图像中"抠"出来——生成仅含目标的二值掩码,并输出黑底、透明背景(PNG)或裁剪后的纯净目标图像。这是一份通用的实战配方:读完本文,你将掌握Results结果对象中masks、boxes的底层数据结构,能够用 OpenCV + NumPy 自主实现目标提取管线,并了解内置save_crop参数等更省事的替代方案。该能力在电商素材抠图、目标数据集裁剪、图像预处理等场景中非常实用。
1. 前置知识:分割推理的产出物
YOLOv8 分割模型(yolov8n-seg.pt等-seg后缀模型)的输出不仅是目标框,还包括每个目标的像素级掩码(mask)。在 Ultralytics 框架中,predict()返回的是Results对象列表,其核心属性定义于 ultralytics/engine/results.py:
| 属性 | 类型 | 含义 |
|---|---|---|
orig_img | numpy.ndarray | 原始图像(BGR,未被缩放) |
boxes | Boxes | 检测框,支持xyxy、xywh、conf、cls等属性 |
masks | Masks | 分割掩码,提供xy、xyn等轮廓坐标属性 |
names | dict | 类别名称映射(如{0: 'person', ...}) |
path | str | 输入图像的文件路径 |
从源码看,分割推理的后处理由 ultralytics/models/yolo/segment/predict.py 中的SegmentationPredictor.postprocess()完成:先经 NMS 得到预测框,再通过ops.process_mask()将掩码上采样回原始尺寸,最终以Results(orig_img, path, names, boxes=pred[:, :6], masks=masks)的形式打包返回。也就是说,results[i].masks中的掩码数据已经对齐到原始图像尺寸,这正是我们能直接用它做像素级隔离的基础。
2. 逐步配方:目标隔离的完整流程
2.1 导入依赖
from pathlib import Path import cv2 import numpy as np from ultralytics import YOLOcv2(OpenCV):用于绘制轮廓、位运算与保存图像;numpy:用于数组类型转换、形状变换与通道堆叠;YOLO:Ultralytics 推理入口。
2.2 加载模型并执行推理
from ultralytics import YOLO # 加载一个预训练分割模型 model = YOLO('yolov8n-seg.pt') # 运行推理 results = model.predict()- 模型选择:仓库支持
yolov8n-seg.pt、yolov8s-seg.pt、yolov8m-seg.pt、yolov8l-seg.pt、yolov8x-seg.pt等分割模型,详见分割任务文档中的模型性能表。模型权重在首次使用时自动下载。 - 不传 source 参数时:
predict()会使用库自带的示例图片ultralytics/assets/bus.jpg与ultralytics/assets/zidane.jpg(见 assets 目录),非常适合快速验证流程。 - 如需指定数据源,可传图片路径、目录、URL 或视频流,完整参数见 Predict 模式文档。
2.3 迭代结果与轮廓
# 迭代检测结果(对多张图片同样适用) for r in results: img = np.copy(r.orig_img) img_name = Path(r.path).stem # 源图片的 base-name,后续保存文件时用 # 迭代每个目标轮廓(同一张图可能有多个检测目标) for ci, c in enumerate(r): # 获取检测类别名称 label = c.names[c.boxes.cls.tolist().pop()]说明:
- 外层
for r in results:每张输入图片对应一个Results; - 内层
for ci, c in enumerate(r):遍历该图上的每个检测目标,ci是该目标的索引(同一类别出现多个实例时用来区分); c.boxes.cls返回类别索引张量(Boxes.cls在 results.py 中定义为self.data[:, -1]),.tolist().pop()取出其中的整型类别号,再用c.names[...]映射为类别名称;- 单张图片时外层循环只执行一次;单张图且只有一个检测目标时,两层循环都只执行一次。
2.4 生成二值掩码并绘制填充轮廓
隔离目标的关键一步:从原图尺寸生成一张全零的二值图,把目标轮廓以白色(255)填充进去,其余区域保持 0。
# 创建与原图同尺寸的二值掩码(单通道) b_mask = np.zeros(img.shape[:2], np.uint8) # 提取轮廓坐标点 contour = c.masks.xy.pop() # (x, y) 像素坐标点集 contour = contour.astype(np.int32) # float32 -> int32,兼容 drawContours contour = contour.reshape(-1, 1, 2) # 整理为 [N, 1, 2] 形状 # 将轮廓填充绘制到掩码上 _ = cv2.drawContours(b_mask, [contour], -1, (255, 255, 255), cv2.FILLED)contour三步处理的内部原理:
c.masks.xy:返回掩码轮廓点坐标列表,格式为(x, y)像素坐标。底层实现见 results.py 的Masks.xy属性——它调用ops.masks2segments()将每个掩码张量转换为轮廓点序列,再通过ops.scale_coords()缩放到原图坐标。.pop():masks.xy是只含一个元素的列表(当前目标的轮廓),用pop()取出该元素得到轮廓点数组。注意masks2segments()默认采用"largest"策略(见 ops.py),即单个掩码只保留面积最大的外部轮廓(由cv2.findContours以RETR_EXTERNAL模式提取),因此单目标场景下取单个轮廓是合理的。.astype(np.int32):masks.xy返回的坐标是float32类型,而 OpenCV 的drawContours()不接受浮点坐标,必须先转为int32。.reshape(-1, 1, 2):drawContours()要求轮廓形状为[N, 1, 2]——N为轮廓点数,每个点用1个条目承载2个坐标值(x, y);-1表示该维度长度自动推断。
drawContours()配置解读:
[contour]:把轮廓包一层列表(多个轮廓可同时传入),经测试能正确生成目标轮廓掩码;-1:表示绘制传入的所有轮廓;(255, 255, 255):绘制颜色为白色;cv2.FILLED:将轮廓包围的所有像素统一填充为白色,其余像素保持 0——这正是"二值掩码"的含义,也是后续隔离操作的依据。
3. 两种隔离方案与各自的裁剪子选项
拿到二值掩码b_mask后,根据期望的背景表现有两种主流方案,每种方案又分"保持整图尺寸"与"裁剪到目标区域"两个子选项。
3.1 方案一:黑色背景像素
# 将单通道二值掩码转为三通道 mask3ch = cv2.cvtColor(b_mask, cv2.COLOR_GRAY2BGR) # 用按位与运算隔离目标 isolated = cv2.bitwise_and(mask3ch, img)工作原理:
- 二值掩码原本是单通道(灰度)图像,而原图是三通道 BGR。
cv2.cvtColor(..., cv2.COLOR_GRAY2BGR)先把掩码广播为三通道,使两张图通道数一致,满足后续混合运算的兼容性要求; cv2.bitwise_and()逐像素按位与:只有两个输入在同一位置像素值都大于 0 时结果才大于 0。掩码只在目标轮廓内部为 255(>0),因此保留下来的恰好是原图中与轮廓区域重叠的像素;轮廓之外全部变为 0,呈现黑色。
子选项 A:保持整图尺寸
无需额外步骤,isolated即为原图尺寸、目标以外区域全黑的图像。
子选项 B:裁剪到目标区域
利用检测框坐标做数组切片,仅保留目标所在矩形区域:
# 获取边框坐标:xmin, ymin, xmax, ymax x1, y1, x2, y2 = c.boxes.xyxy.cpu().numpy().squeeze().astype(np.int32) # 裁剪到目标区域 iso_crop = isolated[y1:y2, x1:x2]这行代码的三段式处理:
c.boxes.xyxy:返回[xmin, ymin, xmax, ymax]格式的边框张量(Boxes.xyxy在 results.py 中定义为self.data[:, :4]),单位为原始图像像素;.cpu().numpy().squeeze():从 GPU 移至 CPU、转为 NumPy 数组、去掉多余维度(单目标时为形状(1, 4)→(4,));.astype(np.int32):浮点坐标转整数,才能用于数组切片;isolated[y1:y2, x1:x2]:按[ymin:ymax, xmin:xmax]切出目标区域。
3.2 方案二:透明背景像素
# 将 BGR 三通道图与单通道二值掩码沿深度方向堆叠为四通道 isolated = np.dstack([img, b_mask])工作原理:np.dstack()沿第三维(通道/深度方向)堆叠数组。原图是 H×W×3,二值掩码是 H×W×1,堆叠后得到 H×W×4 的图像——即 BGRA 四通道。目标区域对应的 alpha 通道值为 255(不透明),轮廓之外的 alpha 为 0(全透明),保存为 PNG 格式时背景即呈现为透明。注意:JPEG 不支持透明通道,务必使用 PNG 保存。
子选项 A:保持整图尺寸
无需额外步骤,保存为 PNG 后整图尺寸、背景透明。
子选项 B:裁剪到目标区域
与方案一完全相同:
# 获取边框坐标 x1, y1, x2, y2 = c.boxes.xyxy.cpu().numpy().squeeze().astype(np.int32) # 裁剪到目标区域 iso_crop = isolated[y1:y2, x1:x2]3.3 内置替代:直接使用save_crop
如果你只是想得到"包含背景的裁剪图"(目标周围保留原背景,不做二值隔离),无需手写上述流程——这是 Ultralytics 的内置能力:在predict()中设置save_crop=True即可自动把每个检测目标按框裁剪保存,该参数与完整推理参数列表见 Predict 模式文档。在结果对象层面,Results.save_crop()方法也已内置(见 results.py 的方法列表)。
4. 可选收尾:将结果保存到文件
隔离或裁剪完成后的下一步完全由你的业务决定。一个最基本的收尾是把结果写盘:
# 将隔离出的目标保存为 PNG 文件 _ = cv2.imwrite(f'{img_name}_{label}-{ci}.png', iso_crop)命名规则:img_name是源图片 base-name(来自Path(r.path).stem),label是检测类别名,ci是同一类下目标的序号,三者组合可保证同一张图、同一类别、多个实例时文件名不冲突。此步骤可选,不需要保存时可跳过。
5. 完整示例代码
以下将全部步骤合并为一个可直接运行的脚本。需要说明:对要保存三通道(BGR)图像时用cv2.imwrite即可;若选择了透明背景方案(四通道),OpenCV 写 PNG 也能保留 alpha 通道。实际使用中把两个for循环体封装为函数会更利于复用,此处留作练习。
from pathlib import Path import cv2 import numpy as np from ultralytics import YOLO model = YOLO('yolov8n-seg.pt') # 加载分割模型 results = model.predict() # 执行推理(默认使用示例图片) # 迭代检测结果(支持多张图片) for r in results: img = np.copy(r.orig_img) # 原始图像副本 img_name = Path(r.path).stem # 源图片 base-name # 迭代每个目标轮廓(多个检测目标) for ci, c in enumerate(r): label = c.names[c.boxes.cls.tolist().pop()] # 类别名称 # 创建二值掩码 b_mask = np.zeros(img.shape[:2], np.uint8) # 提取轮廓并整理为 drawContours 所需格式(合并为一行) contour = c.masks.xy.pop().astype(np.int32).reshape(-1, 1, 2) _ = cv2.drawContours(b_mask, [contour], -1, (255, 255, 255), cv2.FILLED) # 以下两种方案任选其一: # 方案一:黑色背景 mask3ch = cv2.cvtColor(b_mask, cv2.COLOR_GRAY2BGR) isolated = cv2.bitwise_and(mask3ch, img) # 方案二:透明背景(需保存为 PNG 才能看到透明效果) isolated = np.dstack([img, b_mask]) # 可选:按检测框裁剪到目标区域(对以上任一方案都适用) x1, y1, x2, y2 = c.boxes.xyxy.cpu().numpy().squeeze().astype(np.int32) iso_crop = isolated[y1:y2, x1:x2] # TODO: 在这里编写你的后续处理逻辑 # 例如:cv2.imwrite(f'{img_name}_{label}-{ci}.png', iso_crop)提示:运行前请确认已安装
ultralytics、opencv-python、numpy。安装引导参见快速开始文档。
6. 源码级原理速查
为便于深入理解与排查问题,下面汇总本指南涉及的关键实现位置:
| 关注点 | 源码位置 | 关键说明 |
|---|---|---|
| 分割推理后处理 | ultralytics/models/yolo/segment/predict.py | NMS →process_mask上采样 → 组装Results(boxes, masks) |
| 结果对象结构 | ultralytics/engine/results.py | orig_img、boxes、masks、names、path等属性 |
| 掩码轮廓坐标 | ultralytics/engine/results.py | Masks.xy(像素坐标)与Masks.xyn(归一化坐标) |
| 掩码转轮廓算法 | ultralytics/utils/ops.py | masks2segments(),cv2.findContours+largest策略 |
| 检测框属性 | ultralytics/engine/results.py | Boxes.xyxy、Boxes.conf、Boxes.cls |
| 内置裁剪保存 | ultralytics/engine/results.py | Results.save_crop()方法 |
7. 常见问题与注意事项
- 为什么
masks.xy取到的轮廓只有一个?因为masks2segments()默认strategy="largest",每个掩码只保留最大外部轮廓(ops.py)。对绝大多数目标(单个连通区域)够用;若目标本身有孔洞或由多个分离区域组成,可关注"concat"策略或自行扩展。 - 透明背景必须存 PNG:
np.dstack([img, b_mask])产生的四通道图仅在 PNG 等支持 alpha 通道的格式下才保留透明效果。 - 坐标系必须一致:
masks.xy与boxes.xyxy都基于原始图像坐标(后处理时已用scale_boxes/scale_coords回缩),因此可以直接与r.orig_img一起使用,无需再次缩放。 - 修改推理默认行为:若想过滤低置信度目标或限定类别,可在
predict(conf=0.25, classes=[0])中传入参数,完整参数说明见 Predict 模式文档。
【免费下载链接】yolov10YOLOv10: Real-Time End-to-End Object Detection [NeurIPS 2024]项目地址: https://gitcode.com/GitHub_Trending/yo/yolov10
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考