简介:面向医学影像目标检测任务的小规模肠道息肉数据集,共包含六百一十二张真实内镜图像,同时提供Pascal VOC与YOLO两种主流标注格式,标注类别统一为息肉,合计标注框七百一十二个,适合高校学生、算法工程师快速掌握检测模型的数据组织与训练流程。压缩包内文件总数为一千八百三十八个,主要包括JPG原图、VOC格式XML标注及YOLO格式TXT标注三类,整个资源包大小约十四点六三兆字节,下载与加载十分轻量,便于本地或云端反复迭代。标注过程中使用labelImg工具对每张图片的息肉区域绘制矩形框,边界框定位准确且经过人工核对,满足通用目标检测网络的训练要求。目前已有约一百五十人学习浏览,借助该数据集可完成VOC与YOLO格式转换、训练集划分、脚本调试和检测效果评估等完整实验流程;需要提醒的是,数据集仅对标注质量负责,不对训练所得模型在临床或任何场景中的精度表现作出保证。
1. 612张肠道息肉VOC+YOLO双格式数据集,到底解决什么问题
先给一个反直觉的结论:612张肠道息肉图像,在目标检测任务里比同等数量的自然场景图像更难伺候。内镜图像背景单一但纹理复杂,息肉和黏膜褶皱在灰度上高度接近,加上样本量不大,模型很容易把注意力放到错误的位置上。这个数据集把标注同时给你VOC和YOLO两份,不是制造冗余,而是让不同训练管线都能直接接入:VOC格式方便你阅读、校验、做可视化;YOLO格式方便你丢进训练脚本当天跑通。接下来我把它当作一份到手的数据资产来拆解,从解压开始,一路讲到训练前的校验、格式转换和实际踩坑点。适合准备训练医疗图像检测模型、或者第一次接触双格式数据集的人。
2. 先用7z命令行把数据集落地:解压、校验与目录清点
2.1 为什么选择7z压缩与p7zip的安装
拿到手是一个.7z后缀的压缩包。7z 的 LZMA 压缩算法在图片这类数据上通常比 zip 有更好的压缩率,尤其面对几百张 JPEG 图片组成的目录时,体积差距能到 10% 到 30%。这也是常见数据集分发会选用 7z 的原因。解压它不需要图形界面,命令行工具是最可靠的方式,因为你可以拿到返回码、能看到每个文件的解压状态、还能测试压缩包完整性。
Linux 环境下先确认有没有装 p7zip:
which 7z || sudo apt install p7zip-full # Debian/Ubuntu # CentOS/RHEL 系用 sudo yum install p7zip p7zip-plugins如果你在 macOS 上,用 Homebrew 安装即可:brew install p7zip。安装后直接执行7z看到版本号就算成功。注意:有些发行版默认只装了p7zip而不是p7zip-full,缺少部分编解码器时,遇到某些压缩参数会解压失败,所以统一装 full 版本。
2.2 解压命令与参数:x、-o、-aoa、-t
解压动作本身不复杂,但参数要写对。常见错误是把解压目标目录写错,或者遇到已存在文件时停下来要交互确认。
7z x 肠道息肉检测数据集612张VOC+YOLO格式.7z -o/home/user/datasets/polyp -aoax是全路径解压,保留压缩包内的目录结构;-o后直接跟目标目录,等号或空格都不要加,这一点和很多 GNU 工具不一样;-aoa表示覆盖已存在的文件,适合重新解压的场景。如果只想先看看压缩包里有什么,用7z l列目录而不是直接解压,能快速确认内部的顶层目录名,避免解压后不知道文件散落在哪。
数据量不大时,解压后务必顺手做一个完整性校验:
7z t 肠道息肉检测数据集612张VOC+YOLO格式.7z这个命令会把压缩包内所有文件重新解压到内存并计算校验和,输出Everything is Ok才说明文件没有损坏。网络传输中偶尔会把字节搞丢,尤其是从网盘或第三方中转站下载的文件,测试这一步能省下训练到一半才报错的痛苦。
2.3 解压后的目录结构长什么样,以及清点时看哪些指标
解压完成后,先不要急着打开图片,先用一个命令把宏观结构拉出来:
find . -maxdepth 2 -type d | sort我一般会要求目录至少包含两类信息。一种是 VOC 风格的结构:JPEGImages/放原始图片,Annotations/放同名 XML 标注,ImageSets/Main/放训练和验证的 txt 划分文件。另一种是 YOLO 风格的结构:images/和labels/两个平级目录,内部按 train/val 分子目录或直接在同一个平级目录里放.jpg和.txt。两份标注描述的是同一批图片,文件名应该一一对应。
目录确认之后,做三件基础清点:第一,数图片数量是否为 612;第二,统计带标注的文件数,VOC 的 XML 数量与 YOLO 的 txt 数量都应等于有目标的图片数;第三,抽取一个样本对比同名的.xml与.txt,确认描述的是同一个目标。
ls JPEFImages | wc -l # 图片总数 ls Annotations | wc -l # XML 标注数 ls labels | wc -l # txt 标注数如果三者数量有出入,优先检查是不是有空标注的图片被遗漏,或者某一张图有多个目标但对齐失败。图像文件名中如果带中文或特殊符号,建议在训练前统一重命名为纯 ASCII,否则后续脚本在跨平台时容易出问题。
3. 从VOC到YOLO:标注格式差异与可复用的转换脚本
3.1 VOC的XML里有什么,YOLO的txt里有什么
VOC 格式本质是一堆 XML 文件,每个文件对应一张图片。打开一个典型的标注文件,结构大致是:
<annotation> <folder>JPEGImages</folder> <filename>polyp_0012.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>polyp</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>300</xmax> <ymax>240</ymax> </bndbox> </object> </annotation>xmin/ymin/xmax/ymax是像素坐标系下的整数坐标,左上角为原点,右下角为终点。这里藏着一个常见坑:xmax/ymax是包含边界还是排除边界。Pascal VOC 的约定是像素索引从 0 开始,xmax指向目标最右侧像素的索引,但很多标注工具导出时会加 1。差值只有 1 个像素,对小目标来说能带来 1 到 2 个百分点的 AP 波动,建议转换前先和图片上可视化框交叉验证一组数据。
YOLO 格式则是一个 txt 文件对应一张图片,每一行一个目标,格式为:
class_id x_center y_center width height五个值全部是归一化浮点数。x_center是目标中心点 x 坐标除以图片宽度,范围在 0 到 1 之间;width是目标宽度除以图片宽度,同样归一化。类别 ID 从 0 开始,这个加 1 或者从 1 开始的错误在真实项目中反复出现,必须用脚本统一检查。
3.2 手写转换脚本:XML坐标转归一化中心坐标
把 VOC 转成 YOLO 是数据准备里最常复用的脚本。直接用现成库当然也行,但手写一遍能让你看清边界问题出在哪。下面这段脚本处理单类别息肉检测,多个类别时改一下class_map即可。
import os import xml.etree.ElementTree as ET voc_annotations = 'Annotations' yolo_labels = 'labels' classes = {'polyp': 0} # 单类别数据集;多类别时按实际标签扩展 os.makedirs(yolo_labels, exist_ok=True) for xml_file in os.listdir(voc_annotations): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(voc_annotations, xml_file)) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) base = os.path.splitext(xml_file)[0] with open(os.path.join(yolo_labels, base + '.txt'), 'w') as out: for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in classes: continue # 跳过未知类别 bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 边界剪裁,防止贴边目标越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) out.write(f"{classes[cls_name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n")逻辑核心是先把左上角右下角换算成中心点加宽高,再做一次像素坐标到归一化坐标的除法。min/max这行不是可有可无的防御:当息肉顶到图像边缘时,xmax偶尔会等于img_w,除数结果正好是 1.0,可以接受;但有些标注工具会导出xmax=img_w+5,这时候直接写进训练会被 YOLO 过滤掉或产生 NaN 损失。类似的剪裁逻辑也要对w/h做,避免贴边目标的框宽超过边界。
3.3 转换后怎么快速验证,以及和常见误用的差别
转换完不要立刻开始训练,先用一个脚本反查标签是否有问题:
import os label_dir = 'labels' for f in sorted(os.listdir(label_dir)): if not f.endswith('.txt'): continue path = os.path.join(label_dir, f) with open(path) as fh: lines = [l.strip() for l in fh if l.strip()] for line in lines: parts = line.split() if len(parts) != 5: print(f'{f}: 字段数不对 -> {line}') continue vals = list(map(float, parts[1:])) if any(v < 0 or v > 1 for v in vals): print(f'{f}: 归一化越界 -> {line}')这里可以反向验证一个重要区别:VOC 坐标是像素绝对值,YOLO 坐标是 0 到 1 的归一化相对值,两者不能混用。常见误用是有人把 YOLO 的x_center当成像素中心直接拿来画框,或者把 VOC 的xmin直接除以img_w当作中心点,画出来的框全部偏到左上角。另一个常见误用是类别 ID 和classes.txt里的顺序没对上,数据集里只有一个 polyp 类时看似安全,一旦图片里有其他异物被标注为第二个类,ID 错位会导致 loss 不收敛。
4. 准备训练数据:划分验证集、检查标签、写data.yaml并启动YOLO
4.1 数据划分:按图随机还是按患者分组
612 张图说多不多,说少不少。划分训练集和验证集的策略直接决定你对模型能力的判断是否可信。自然图像检测里随手train_test_split可能没问题,但医学内镜图像有个隐蔽的关联性问题:同一个患者可能拍了很多张连续帧,这些帧在纹理、光照和息肉形态上高度相似。如果这些相似帧同时出现在训练集和验证集,验证分数会虚高,模型看起来 mAP 不错,实际换一个新患者就掉点。
稳妥的做法是按患者或按视频片段分组,确保同一个来源的图只在训练集或只在验证集出现。但这个数据集只给了图片和标注,没有显式患者 ID,我能接受的折衷方案是先按文件名前缀聚类,或者检查 ImageSets/Main 里是否已有划分文件。常见做法是用 80/20 或者 85/15 的比例:
import os import random random.seed(42) images = [f for f in os.listdir('JPEGImages') if f.endswith('.jpg')] random.shuffle(images) split = int(len(images) * 0.85) train_files = images[:split] val_files = images[split:] with open('train.txt', 'w') as f: f.write('\n'.join(train_files)) with open('val.txt', 'w') as f: f.write('\n'.join(val_files)) print(f'train={len(train_files)} val={len(val_files)}')如果 YOLO 目录结构已经按images/train、images/val组织好了,就不需要额外 txt,YOLOv8 的 data.yaml 会直接读取目录。我建议同时保留一份val.txt给后续可视化验证,不占空间,排查的时候很有用。
4.2 训练前的标签体检:缺失、空文件、越界、类别不一致
训练启动前用一段脚本把标签数据整体体检一遍,比起训练到一半报错要划算得多。我用 Python 写一个最小体检脚本,覆盖四类问题:图片缺失对应标注、标注缺失对应图片、txt 为空、归一化坐标越界。
import os img_dir = 'JPEGImages' label_dir = 'labels' imgs = set(os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith('.jpg')) labels = set(os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith('.txt')) print('有图无标注:', len(imgs - labels)) print('有标注无图:', len(labels - imgs)) for name in sorted(imgs & labels): with open(os.path.join(label_dir, name + '.txt')) as f: lines = [l for l in f.read().strip().splitlines() if l] if not lines: print(f'空标注: {name}') for line in lines: if not line.startswith('0 '): # 单类数据集品类 ID 必须为 0 print(f'类别异常: {name} -> {line}')这段脚本的执行结果如果是空白,才说明标签基本干净。体检时还要注意一个现象:内镜图像里有些息肉特别小,标注框可能只有十几个像素宽。这类目标在归一化后w/h只有 0.01 到 0.03,YOLO 输出特征图上一层 stride 32 之后,这些目标可能连一个格子都覆盖不到,训练时很容易被当成背景忽略。这是小目标检测的通病,不一定是数据本身的问题。
4.3 最小可复现训练命令与参数解释
YOLO 系训练自己的数据集,最省事的是直接用 ultralytics 的 CLI。先写好data.yaml:
path: /home/user/datasets/polyp # 数据集根目录,建议绝对路径 train: images/train # 相对 path 的路径 val: images/val nc: 1 # 类别数 names: ['polyp'] # 类别名,顺序必须和 id 一致如果你的 YOLO 目录没有按 train/val 分子目录,而是所有图都在images/、所有标签都在labels/,可以在train和val处写相同的目录路径,同时通过val.py或者命令行参数实现按比例切分。但更稳的做法还是先手动划分好目录结构。启动一个最小训练:
yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 patience=20 name=polyp_run参数含义分别是:model=yolov8n.pt用 COCO 预训练权重做迁移学习,n是 nano 版本,在 612 张图的小数据集上收敛快;epochs=100不是越多越好,后面接patience=20让模型在验证集 20 个 epoch 没有提升时自动早停;imgsz=640是和预训练权重一致的输入尺寸,对息肉这种小目标来说不建议降到 320,会丢失细节;batch=16在消费级显卡上一般能跑。首次训练建议先跑通 10 个 epoch,观察 loss 是否下降、有没有产生 NaN,再正式跑完整流程。
训练过程的关注点和自然图像检测完全不同。内镜息肉边缘模糊,正负样本边界也模糊,我一般直接看val/box_loss是否平稳下降,以及 PR 曲线在置信度 0.5 附近有没有明显拐点。如果 loss 前几个 epoch 骤降然后立刻平台期,大概率是模型只学到了图像背景色而不是息肉本身的纹理特征,需要在数据增强上做文章。
5. 标注质量校验:可疑样本排查与内镜小目标的增强技巧
5.1 用标注尺寸分布找出可疑样本
612 张图训练前,我最常做的一个操作是画出所有标注框的宽度、高度分布,用来发现异常。比如列出所有标注中宽度小于 15 像素或者面积占比过小的样本:
python - <<'EOF' import os, re label_dir = 'labels' for f in sorted(os.listdir(label_dir)): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fh: for line in fh: parts = line.split() if len(parts) != 5: continue w = float(parts[3]) h = float(parts[4]) if w < 0.02 or h < 0.02: print(f'{f}: 目标过小 w={w:.3f} h={h:.3f}') EOFw=0.02对应 640 像素下的 13 个像素。低于这个阈值的息肉目标,人类标注时都容易把边界画歪,模型学习这些样本的收益极低。对这些样本我的做法是人工抽查图片,确认是不是目标真的极小;如果是,保留但不要期望模型把它检测得很准,mAP50-95 的分数会被拉低,这不代表数据或模型有致命问题。
5.2 增强策略、推理验证与一张图的查错流程
对内镜图像,我的增强策略是:保留 mosaic,关闭过强的旋转增强。息肉没有固定的朝向偏好,旋转 90 度理论上没问题;但实际上内镜图像有光源方向和镜头反光区域的特征,旋转后模型容易学到“反光位置可预测”的错误规律。建议设置degrees=15,flipud 保持默认关闭,hsv_h 也降一点,因为息肉颜色是诊断特征之一,颜色饱和度过强会导致模型依赖颜色而非纹理。
训练结束后用验证集跑一遍推理,加上save=True输出带框图片:
yolo detect predict model=runs/detect/polyp_run/weights/best.pt source=images/val save=True conf=0.25然后挑出几张误检图,看框和息肉边缘的贴合程度。如果所有框都比息肉大一圈,怀疑标注的 bndbox 本身偏宽松;如果框偏向黏膜高光区域,说明增强策略里色调扰动调得过多。单张图的查错流程就是:原图、标注框、模型预测框三张叠在一起对比,这比看任何 loss 曲线都更能定位问题。
本文还有配套的精品资源,点击获取