简介:一份可直接用于YOLO目标检测训练的息肉(Kvasir-SEG)数据集,包含划分好的训练集与验证集,以及配套的类别文件和数据可视化脚本。数据集共2000个文件,压缩后大小57MB,其中1000个txt标签文件、999张jpg原图和一个py可视化脚本,所有标注均采用YOLO相对坐标格式(x_centre、y_centre、w、h),边界框完整清晰。训练集含800张图片及对应标签,验证集含200张图片及对应标签,图像分辨率覆盖332x487至1920x1072,适合目标检测模型训练与评估。资源还附带无需修改即可运行的可视化脚本,可随机传入图片快速绘制边界框,方便直观检查标注质量。目前已有315人学习下载,适合计算机视觉方向学生、研究者及YOLO模型开发者直接使用。
1. 一份连划分都做好的 YOLO 数据集,能帮你省掉哪些事
做内镜图像里的息肉检测,第一道坎往往不是模型,而是数据长什么样。Kvasir-SEG 这类公开数据集,原始标注是像素级分割掩膜,不是 YOLO 要的矩形框;拿到手你还得自己划分训练集、验证集、测试集,写 class 文件、写可视化脚本。这个标题里的 YOLO 数据集,把这三件事打包解决了:息肉检测只有 1 个类别,数据已经按比例划分好,附带的类别 class 文件一次对齐,数据可视化脚本让你能在训练前把每张图的标注“看”一遍。适合谁?想用 YOLO 系列模型快速跑通医疗小目标检测、又不想在数据预处理上消耗两周的人。这篇文章就按这套东西的实际落地路径讲清楚:掩膜怎么转成框、划分怎么不漏帧、可视化脚本怎么写、坑又在哪里。
2. Kvasir-SEG 转 YOLO:分割掩膜怎么变成检测框
2.1 原始数据里到底有什么:一张图配一张黑白掩膜
Kvasir-SEG 全量约 1000 张肠镜图像,每张图像对应一张像素级掩膜。掩膜里白色区域就是息肉,黑色是背景,没有类别意义上的多分类——整个数据集就一个对象类,所以转成 YOLO 格式时类别编号固定写 0 即可。
这里有个容易忽略的点:原始标注是分割掩膜,不是检测框。YOLO 训练需要的是class x_center y_center width height这样的归一化坐标文本,掩膜不能直接喂给 YOLO 检测头。所以第一步一定是做格式转换。如果你的同事说“把 Kvasir-SEG 直接丢进 YOLO 训练”,这句话在技术上不成立,必须先转。
转换时有个视觉细节值得留意:肠镜图像里的息肉并不总是干净的类圆形,边缘可能有细长的凸起、反光造成的孔洞、甚至多个不相连的息肉区域同时出现在一张图里。这些都会影响你转换脚本的稳定性,也是后面避坑部分的重点。
2.2 掩膜转矩形框:连通域分析是比直接取 min/max 更稳的做法
最常见的转换思路是“把掩膜中白色像素的 x/y 坐标取最大最小值拼一个框”。这个方案在掩膜只有一整块且没有噪声时能跑,但遇到多块息肉、掩膜边缘有细小噪声时,会直接得到一个横跨全图的错误大框。
我一般用连通域分析来做,这样每个独立的息肉区域都会被单独拎出来生成一个框。核心逻辑如下:
import cv2 import numpy as np def mask_to_yolo_boxes(mask_path, img_shape, min_area=20): # 读成灰度图,Kvasir-SEG 的掩膜是黑白图 mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: return [] # 二值化:白色目标为 255,黑色背景为 0 _, binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) # connectivity=8 保证斜向相邻的像素也算同一连通域 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( binary, connectivity=8 ) h, w = img_shape[:2] boxes = [] for i in range(1, num_labels): # 0 是背景 x, y, bw, bh, area = stats[i] if area < min_area: continue # YOLO 格式:归一化的中心点坐标和宽高 x_center = (x + bw / 2.0) / w y_center = (y + bh / 2.0) / h box_w = bw / w box_h = bh / h boxes.append((x_center, y_center, box_w, box_h, area)) return boxes这段代码里的min_area按像素面积过滤。Kvasir-SEG 的息肉区域一般远大于几十像素,设 20 能滤掉掩膜边缘的噪点;如果你的图分辨率较高,可以提到 50 再观察。connectivity=8会比4更符合息肉边缘的连通性,减少把同一块息肉切成多块的情况。
还要说明一点:这里用的是轴对齐矩形框,不是cv2.minAreaRect()的最小外接旋转框。YOLO 检测系列默认输出就是轴对齐框,如果用了旋转框,还得再转一次轴对齐,徒增误差。息肉形态即便偏长条,轴对齐框也足够表达位置,这是当前 YOLO 检测的主流做法。
2.3 转换完别急着训练:先做三类合法性检查
转换脚本跑完后,我强烈建议先做三项检查,否则后面出问题回溯成本很高:
- 坐标范围检查:所有 x_center、y_center 应该在 0~1 之间,宽高也应大于 0 且不超过 1。如果出现负数或大于 1 的值,多半是读取掩膜时尺寸和原图不一致。
- 空标签检查:如果某张掩膜全黑,说明这张图没有息肉,转换后会生成空 txt。YOLO 训练时空 txt 不会报错,但验证集如果空 txt 太多,mAP 计算会变得很怪。
- 框数量对照:读几张图,人工确认“图里有几块息肉区域,txt 里就有几行框”。
这块我当时吃过亏:某张掩膜分辨率是 512×512,但原图是 768×576,没检查直接开训,结果那一张图的所有框全错位,模型 AP 掉了将近 3 个点。转换脚本里把img_shape从原图读,而不是从掩膜文件推断,能绕开这个坑。
3. 划分数据集与 class 文件:按 70/20/10 落盘,别让同一帧出现在两个集合
3.1 划分比例怎么定,以及你需要的目录结构
对于医疗小数据集,我一般用 70/20/10 而不是 80/10/10。原因是这类数据集往往场景单一、样本总量只有千张级别,验证集太小的话,指标波动会很大,甚至一两个框的误检就能让 mAP 掉 0.1。测试集保留 10%,最后做一次盲测即可。
目录结构建议如下:
kvasir-yolo/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── polyp.namesimages和labels下三个子目录一一对应,图片是.jpg,标注是同名.txt。这样组织的好处是:YOLO 的train/val路径直接指过去,不需要额外写映射关系。
3.2 划分脚本:按图不按框,并注意掩膜序列的帧泄漏
划分脚本的核心不是随机种子,而是必须确保同一内镜视频相邻帧不会一张进 train、一张进 val。Kvasir-SEG 本身没有提供病人 ID 或视频片段 ID,但它的图像很多是从内镜视频抽帧出来的,相邻帧之间背景、息肉形态高度相似。如果随机打乱,可能让验证集里出现和训练集几乎同一画面的帧,导致验证 mAP 虚高,部署时掉点严重。
import os import random import shutil random.seed(42) # 固定种子,保证可复现 image_dir = "images_all" label_dir = "labels_all" # 转换好的 txt train_ratio, val_ratio = 0.7, 0.2 names = [f[:-4] for f in os.listdir(image_dir) if f.endswith(".jpg")] random.shuffle(names) train_n = int(len(names) * train_ratio) val_n = int(len(names) * val_ratio) split = { "train": names[:train_n], "val": names[train_n:train_n + val_n], "test": names[train_n + val_n:], } for split_name, file_names in split.items(): os.makedirs(f"images/{split_name}", exist_ok=True) os.makedirs(f"labels/{split_name}", exist_ok=True) for name in file_names: shutil.copy(os.path.join(image_dir, name + ".jpg"), f"images/{split_name}/") shutil.copy(os.path.join(label_dir, name + ".txt"), f"labels/{split_name}/")这段脚本的逻辑是按文件名随机划分,再把图片和标签同步复制。关键是图片和标签共用同一份文件名列表,避免只复制了 jpg 漏了 txt,或者反过来。
随机划分在 Kvasir-SEG 上是可接受的做法,因为数据集中没有可用的序列 ID。但如果未来拿到带病例编号的内镜数据,强烈建议先按病人 ID 分组,再在组层面划分,而不是在图片层面随机划分。
3.3 data.yaml 与 class 文件:一次对齐,避免 COCO80 的命名错觉
class 文件和 data.yaml 是 YOLO 训练时读标签的依据。类别编号 0 对应polyp,单类别也要认真写。常见的错误是:忘了改nc,模型默认按 COCO80 的类名去映射,训练出来的名称和输出全乱套,这就是 yolo 入门时最常见的一类翻车。
# data.yaml path: ./kvasir-yolo train: images/train val: images/val test: images/test nc: 1 names: ['polyp']polyp.names文件内容只有一行:
polyp这一步没有复杂逻辑,但它是 yolov8 训练自己的数据集时最容易出错的地方。names列表的下标必须和 txt 标签里的类别编号严格对应:txt 里写的是 0,那么names[0]就必须是polyp,不能是其他类名,更不能把 COCO80 的 names 直接带进来。很多新人把data.yaml里的names写成['polyp', 'background'],这就导致类别数从 1 变成 2,训练损失曲线直接异常,属于典型踩坑。
4. 数据可视化脚本:训练前让标注被看见,训练后让曲线有参照
4.1 可视化标注:把框画回原图上,一眼看出错位
标题里说的数据可视化脚本,我一般拆成两部分:一是把 YOLO 格式的框画回原图,二是统计框的分布规律。画框脚本是转换之后第一个必须跑的脚本,它能帮你快速发现坐标错位、框过大过小等问题。
import cv2 import os def draw_yolo_boxes(image_path, label_path, output_dir): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path) as f: lines = f.readlines() thickness = max(1, int(min(h, w) / 300)) for line in lines: parts = line.strip().split() cls_id = int(parts[0]) x_center, y_center, box_w, box_h = map(float, parts[1:]) x1 = int((x_center - box_w / 2) * w) y1 = int((y_center - box_h / 2) * h) x2 = int((x_center + box_w / 2) * w) y2 = int((y_center + box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), thickness) cv2.putText(img, f"polyp", (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) cv2.imwrite(os.path.join(output_dir, os.path.basename(image_path)), img)这里的thickness根据图像短边动态计算,避免 576px 的图和 1280px 的图用同一个线宽导致小图上线条太粗。cv2.putText的坐标用了max(0, y1 - 5),防止框贴顶时文字画出画面外。跑完这个脚本,我一般会人工抽查全部val图片,而不是只看前几张,因为错位往往集中在特定分辨率或特定掩膜形态上。
4.2 框分布统计:小目标占比直接决定你的输入分辨率
第二个可视化脚本是做尺寸和位置统计。它不是为了发论文配图,而是回答一个问题:这套数据里的息肉目标,在 YOLO 输入尺寸下到底占多少个像素?
import matplotlib.pyplot as plt def analyze_boxes(label_stats): areas = label_stats["area"] plt.figure(figsize=(8, 4)) plt.hist(areas, bins=50) plt.xlabel("Normalized box area") plt.ylabel("Count") plt.title("Polyp box area distribution") plt.savefig("box_area_dist.png") plt.close()把每个 txt 里的框面积做归一化后画直方图,如果大部分目标的边长在 0.1~0.3 之间,意味着在 640×640 输入下对应 64~192 像素,属于中小目标。此时若直接训练,mAP@0.5:0.95会比mAP@0.5低不少,这是正常现象;但如果大量目标边长小于 0.05,你就要考虑用更高输入分辨率或者专门的检测头设计。这个统计脚本数据量不大,写起来也快,但能直接影响你后续训练参数的设置。
4.3 训练曲线可视化:单类别下多关注 yolo 损失函数里的 box_loss
标题里的可视化脚本通常只覆盖数据本身,但训练过程的曲线可视化同样重要。YOLO 训练过程中会实时打印损失,也会写results.csv。你要看的不是总损失一个数,而是拆开来看:box_loss收敛是否平稳、cls_loss是否还在跳、mAP@0.5是否随 epoch 上升。
对于息肉这种单类别数据,cls_loss的参考意义比较弱,因为模型很容易学会“只要看到内镜画面里有一坨东西就分类为息肉”,决定性的是box_loss的收敛质量。我自己一般画三条曲线:train/box_loss、val/box_loss、metrics/mAP@0.5。如果val/box_loss在第 60 轮附近开始横盘甚至抬升,而train/box_loss还在降,就是过拟合信号,此时减增强、加patience或者换更小的模型都比硬调学习率更管用。
5. 避坑清单:YOLO 医疗小数据集最容易翻车的五个问题
5.1 空标签:某张掩膜全黑之后,训练没报错但验证指标不对劲
现象:训练日志正常打印,但 val 的 mAP 出现异常波动,比如某轮突然掉到 0.2,下一轮又回到 0.9。
原因:掩膜里存在全黑图,转换后生成空 txt。YOLO 训练时空 txt 会让该图不参与正样本计算,但验证时类似行为会导致 recall 计算不稳定。
解决:转换脚本里对每个 mask 做一次cv2.countNonZero(binary),为 0 的图单独放一个background/目录,不进训练集也不进验证集。如果数量少(比如不超过 10 张),直接丢弃是最省事的做法。
5.2 掩膜边缘的点状噪声产生大量超小框
现象:训练时 loss 降得很快,但画框可视化时出现大量几像素宽的小框,集中在息肉边缘。
原因:掩膜的边缘高光或分割噪声形成了只有 3~5 像素的连通域,转换脚本全部转成了正式框。这些小框既不符合医生标注意图,也会在训练时给正样本引入大量噪声。
解决:在mask_to_yolo_boxes里把min_area从 0 提高到max(20, 0.001 * h * w),并加一个框宽高下限:if box_w < 0.01 or box_h < 0.01: continue。需要注意,min_area不要设太大,否则小息肉会被一起滤掉,一般先统计一下框面积分布再确定阈值。
5.3 同帧泄漏:验证集 mAP 虚高,部署才发现泛化不够
现象:训练集 mAP 在 0.95 左右,验证集也接近 0.9,部署到新内镜设备上效果骤降,mAP 只有 0.5 上下。
原因:Kvasir-SEG 是从视频抽帧得到的,相邻帧高度相似。随机划分后,同一画面同时出现在 train 和 val,模型等于提前看过答案。这是我在医疗影像数据集上经历过的最大翻车现场。
解决:划分前先检查文件名规律。如果发现文件名前缀相同的图片应该按序列分组,则在划分脚本中用random.sample在组级别划分,而不是在图片级别。Kvasir-SEG 本身没有病人 ID,能做的补救是固定随机种子并检查 train 与 val 之间的图片相似度;后续采集新数据时一定要保留内镜视频片段 ID。
5.4 类别编号与 class 文件错位
现象:训练正常,但可视化时息肉框被标成其他类名,或者加载模型推理时names输出乱码。
原因:txt 标签里类别编号是 0,但data.yaml的names是['background', 'polyp'],导致模型把背景当成第 0 类。另一个常见误用是把 COCO80 的 names 文件直接拿过来,标签映射完全对不上。
解决:单类别数据集一律让nc: 1、names: ['polyp'],txt 中所有目标类别编号统一为 0。改完 yaml 后先跑一次可视化脚本确认标注名称,再进入训练。
5.5 直接把非正方形原图 resize 成 640×640,息肉变形导致精度下降
现象:训练集上 mAP 很高,但可视化发现框偏大,尤其在图像的窄边上。
原因:内镜图像长宽比不一定是 1:1,强制 resize 会让息肉形状拉伸,模型学到的是变形后的形态特征,推理时又得把坐标映射回原图,来回误差叠加。
解决:在 YOLOv8 的训练配置里启用rect=True,它会按 batch 内图像的长宽比做 letterbox 填充而不是粗暴 resize。推理阶段也保持同样的 letterbox 逻辑,yolo predict默认会处理,但如果你自己写推理脚本,要记得在postprocess前还原坐标。
6. 从能训到训得好:验收判据与两个进阶技巧
6.1 判断模型能不能用的三个信号
拿到训练好的模型,别只看 mAP@0.5 一个数。我自己固定看三个信号:mAP@0.5是否稳定在 0.9 以上、val/box_loss是否在后期收敛到和训练 loss 差距小于 20%、PR 曲线的召回端是否在 0.85~0.95 区间。如果mAP@0.5高但mAP@0.5:0.95很低,说明框位准但边缘贴合差——这个问题在息肉检测里经常出现,因为掩膜转框时矩形框本身就会比分割形状偏大。作为检测任务,判断标准应当是:框能不能稳定框住病变区域供医生参考,而不是要求它贴合掩膜边缘。
6.2 两个高频有效的进阶技巧
第一个是多尺度输入。训练时不要固定imgsz=640,可以设imgsz=640但配合 YOLOv8 的scale增强让模型每轮随机看到 448~832 之间的尺寸。这样模型对不同大小的息肉更鲁棒,但也需要更强的patience设置,避免小尺寸训练阶段导致验证分数抖动。
第二个是 Copy-Paste 增强,直接复用掩膜区域把息肉粘贴到内镜背景上。YOLOv8 官方实现里还没有内置这个功能,但你可以基于分割掩膜写一个增强:把某张图的息肉区域切出来,随机旋转缩放后贴到另一张图上,原标签同步增加一个框。这个技巧对小数据集提升明显,但注意粘贴数量不要超过每张图 2 个,否则会让模型对“画面里全是息肉”产生误判。
最后说一条教训:医疗小数据集,第一版先别上复杂模型结构和花哨增强,把数据转换、划分、class 文件、可视化这四个环节钉死,比调任何超参数都重要。数据侧没翻车,后面模型的每一次改动才有参考价值。希望帮到你。
本文还有配套的精品资源,点击获取