☰
从零构建YOLO直肠息肉检测数据集:抽帧、标注与训练避坑实战
2026/10/8 4:49:05 网站建设 项目流程

简介:YOLO直肠息肉检测数据集面向医学影像与目标检测开发者,适用于直肠息肉识别模型的训练和评估,尤其适合需要快速搭建YOLO训练流程的算法工程师和医工交叉学习者。资源共19795个文件,其中7804张jpg为图像样本,4830个txt标签可供YOLO系列直接训练读取,7161个xml标签便于可视化查看或迁移到其他检测框架,双格式设计减少了格式转换成本;压缩包整体177.21MB,体积适中,方便下载与迭代实验。目前已有1146人学习浏览,具备较好的热度参考价值。数据预处理完整,图像与标注一一对应,有效降低人工筛选负担;除标注文件外,还附有作者整理的检测结果参考博文,可帮助使用者了解模型在当前数据集上的表现,便于对比不同训练设置下的检测效果、分析误差来源并针对性地调整参数,从而更高效地完成直肠息肉自动检测模型的复现与优化。

1. 为什么“YOLO直肠息肉检测数据集”值得你亲手做一遍:公开集永远差一口气

做肠镜AI辅助诊断的从业者,大多绕不开同一个尴尬局面:公开的息肉分割数据集虽然不少,但基本是结肠镜静态图像,分辨率统一、光照均衡、病灶清晰居中。到了实际内镜视频流里,情况完全是另一回事——镜头贴壁时对焦失败、冲洗水反光、黏膜褶皱遮挡、息肉只露出半个边缘、帧与帧之间病灶快速滑动。你拿公开数据集训出来的YOLO模型,验证集上 mAP 能到 0.9,一接到真实内镜设备就露馅。解决路径不是换更重的模型,而是自己构建一份能覆盖这些真实退化场景的数据集。

本文要讲的就是这个:如何从零做一份 YOLO 直肠息肉检测数据集,包括视频抽帧、标注口径、数据清洗、类别平衡和训练前检查,并给出可直接复制的脚本和三个最常被问到的边界坑。目标人群是准备用 YOLOv8 或 YOLO11 做内镜项目的算法工程师、医工交叉方向的研究生,以及想验证“自己攒的数据能不能跑通训练”的硬件工程师。文中不会推荐某个现成数据集打包下载,因为真正的落地价值不在那几万张图里,而在你能否复现一套符合临床场景的数据生产管线。

2. 先搞清楚一份 YOLO 检测数据集的组成:不是“图 + 一个 txt”那么简单

2.1 最小的可用数据集应该包含哪四类文件,缺一不可

很多从分类任务转过来的同学,第一次打开 YOLO 数据集的目录会以为只要 images 和 labels 两个文件夹就够了。实际上,一份能被 YOLOv8 正常训练的直肠息肉数据集,最少需要以下四层内容:

  • images/train、images/val:JPG 或 PNG 格式的原始帧,分辨率建议不低于 640×640 有效内容区;
  • labels/train、labels/val:与图像同名的 .txt 文件,每行记录一个目标的 class_id、归一化后的中心点 x、中心点 y、宽 w、高 h;
  • dataset.yaml:声明路径、类别数量和类别名称;
  • 一个划分记录(可以是固定随机种子生成的 train/val 名单),保证多次训练不改变验证集。

前两者是模型真正读取的内容,yaml 是训练入口配置,第四点是实验可复现的地基。缺少划分记录,你会遇到后面第 5 章要讲的“验证集泄漏”问题——同一个息肉的相邻帧同时出现在训练和验证集中,指标虚高,部署后立刻现原形。

2.2 直肠息肉的类别口径怎么定:一分类起步,还是细分到病理类型

息肉检测数据集的类别设计,直接决定标注成本和模型可用范围。最稳妥的起步方案是单类,也就是 class 0 统一为 polyp,框里包含息肉可见主体。这样做的好处是标注一致性容易保证:两个标注员对“这个框到底该覆盖息肉的哪个边界”的争议会大幅减少,YOLO 训练也更干净。

只有在你确实需要做“腺瘤性息肉 vs 非腺瘤性息肉”的预后提示时,才考虑分成两到三类。此时要注意,类别标签必须从病理活检结果回填,不能靠内镜图像的视觉特征猜——锯齿状腺瘤和增生性息肉在镜头下长得非常接近,猜出来的标签就是脏标签,模型会学到一堆噪声特征。常见做法是:先做一分类检测,跑通整个流程,再结合病理信息做第二版细分类模型,两版互不阻塞。

2.3 数据量级估算:多少帧、多少个框才能喂饱 YOLOv8

工业界对 YOLO 目标检测的数据量级有一个比较通用的经验区间:单类别检测任务,实例数(也就是标注框总数)在 5000 个以上,基本能训出一个可用的模型;如果目标占图像面积很小,建议超过 10000 个实例。对于直肠息肉这种目标,尺寸跨度极大——扁平息肉可能只有几十像素宽,带蒂息肉能占到画面四分之一,所以实例总数建议取区间上限,而不是下限。

从内镜视频抽帧来说,一条 30 分钟的高清肠镜检查视频,去掉进出镜、冲洗、模糊和重复帧后,大约能筛出 300 到 600 张有效帧,每帧平均 1 到 3 个可见息肉或可疑病灶,也就是一条视频能产出 800 到 1800 个实例。攒到 10000 个实例,大约需要 8 到 12 条完整检查视频。这个量级对单人标注来说,每天标 300 到 400 帧,一个月内能完成,属于可承受的工程投入。

3. 用真实内镜视频制作 YOLO 数据集:从抽帧到标注再到划分的完整脚本

3.1 第一步:用 ffmpeg 做主抽帧,再用感知哈希去重

原始内镜视频一般体积大、帧率高,直接逐帧抽取会产生大量近乎重复的画面,后续标注员会在相似帧上浪费大量时间。常见做法是先抽一个低帧率的候选帧,再剔除感知重复帧。

# 每 10 秒抽一帧,输出到 frames_raw 目录 # -vf fps 是抽帧率,按视频速度可调;-q:v 控制 JPG 质量,2 表示画质较好 ffmpeg -i colonoscopy_001.mp4 -vf fps=0.1 -q:v 2 frames_raw/colonoscopy_001_%04d.jpg

这个命令的逻辑是让 ffmpeg 以每 10 秒一张的节奏抽帧,适合镜头推进速度中等、息肉停留时间较长的肠镜检查。如果视频里息肉在某个位置只停留两三秒,可以改成fps=0.3,但会引入更多重复帧,模糊帧也会变多。抽帧结束后,用一个简单的感知哈希脚本去重,保留同一片段中中间位置的那一帧。

# 感知哈希去重:只去除画面几乎完全相同的帧,保留时间靠后的那一帧 import os, hashlib from PIL import Image import imagehash def dhash(img_path, hash_size=8): with Image.open(img_path) as img: img = img.convert("L").resize((hash_size + 1, hash_size)) diff = [] for row in range(hash_size): row_pixels = list(img.getdata())[row * (hash_size + 1):(row + 1) * (hash_size + 1)] for i in range(hash_size): diff.append(row_pixels[i] > row_pixels[i + 1]) return ''.join('1' if d else '0' for d in diff) seen = {} for f in sorted(os.listdir("frames_raw")): path = os.path.join("frames_raw", f) h = dhash(path) if h in seen: os.remove(path) # 删掉重复帧 else: seen[h] = f

这里解释一下:感知哈希把图像缩小成 8×9 的灰度图并逐行比较相邻像素亮度,得到一个 64 位指纹;指纹相同即视为几乎同样的画面。真正的息肉检测场景里,相邻抽帧的息肉位置会有轻微位移,哈希不会把它们误判为重复,只有镜头完全停顿或画面几乎静止的帧会被去掉。这个脚本删掉的是“镜头没动”的帧,没有被删除的帧之间往往仍然存在连续性,后面还需要做帧间隔采样,防止连续帧进入同一个数据集子集。

3.2 第二步:标注工具与导出 YOLO txt 的格式细节

推荐用 LabelImg(矩形检测框)或 Labelme(多边形,可导出矩形)。如果目标是检测,LabelImg 足够;如果后续想转向分割任务,直接上 Labelme。这里的关键不是工具本身,而是导出格式的细节。

YOLO 格式的 txt 每一行是:class_id x_center y_center width height,四个坐标值全部是相对图像宽高的归一化小数。常见换算错误是使用边界框左上角坐标 (xmin, ymin) 和右下角坐标 (xmax, ymax) 时忘记转成中心点格式:

# 从 Labelme JSON 转 YOLO txt 的核心逻辑 import json, os from PIL import Image def convert_labelme_to_yolo(json_path, out_txt_path, img_width, img_height, class_map): with open(json_path, encoding='utf-8') as f: data = json.load(f) lines = [] for shape in data['shapes']: cls_name = shape['label'] # 只接受矩形标注;多边形时先取外接矩形,再做形状校验 pts = shape['points'] xs = [p[0] for p in pts] ys = [p[1] for p in pts] xmin, xmax = min(xs), max(xs) ymin, ymax = min(ys), max(ys) # 归一化:所有值必须除以原图宽高,不是除以缩放后的宽高 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height lines.append(f"{class_map[cls_name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines))

参数说明:class_map 是一个字典,把标注时写的中文标签如“息肉”映射到整数 0;如果一开始标注就统一用 polyps 作为唯一标签,class_map 可以简化为{'polyp': 0}。注意代码里显式强调除以原图宽高,因为内镜视频抽帧后如果做过去黑边处理,原图尺寸变了,标注 json 里记录的坐标不会被 yolo 脚本自动适应,这里容易出现一个全场翻车的点。

3.3 第三步:目录组织与固定随机种子的训练验证划分

目录结构建议按 YOLO 官方约定展开:

polyp_data/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── dataset.yaml └── split_seed.txt

划分脚本要特别处理一件事:来自同一段视频连续多帧的图像不能既进 train 又进 val。这就是“按病例或视频划分,而不是按单帧随机划分”。

# 按视频来源划分训练/验证集,防止数据泄漏 import os, random, shutil random.seed(42) # 固定种子,保证每次运行结果一致 video_ids = ['001', '002', '003', '004', '005', '006', '007', '008'] val_videos = set(random.sample(video_ids, 2)) # 抽出 2 个视频作为验证集 for fname in os.listdir('images'): video_id = fname.split('_')[0] # 文件名约定:视频ID_帧号.jpg dest = 'val' if video_id in val_videos else 'train' os.makedirs(f'images/{dest}', exist_ok=True) os.makedirs(f'labels/{dest}', exist_ok=True) shutil.move(f'images/{fname}', f'images/{dest}/{fname}') label = fname.replace('.jpg', '.txt') if os.path.exists(f'labels/{label}'): shutil.move(f'labels/{label}', f'labels/{dest}/{label}')

这段脚本的关键参数是split_seed和val_videos的数目。经验是保留 15% 到 20% 的视频作为验证集,不要贪多。固定种子是为了让后续每次训练、每次调整超参数时对比的对象一致,否则你改了一个增强参数,却因为数据划分不同而无法判断效果差异是来自参数还是来自数据。

dataset.yaml 的内容很简短,但文件名不能写错,YOLOv8 对路径非常敏感:

path: /home/user/polyp_data # 改成你自己的绝对路径 train: images/train val: images/val names: 0: polyp

到这里,一份可以启动训练的数据集已经成型。下一步是训练前必须做的三个质量把控动作——这三个动作直接决定你第一版模型的可用性。

4. 训练前必须做的三件事:类别平衡、小目标增强和脏数据清洗

4.1 类别与尺寸失衡:息肉有大有小,不能一视同仁

很多人直接拿整理好的数据集跑 YOLOv8 默认配置,结果小息肉 mAP 只有大息肉的一半。原因在于默认的训练采样是均匀的,每个 batch 里大尺寸目标占主导,小目标的梯度贡献被稀释。

一个在实践里验证有效的做法是先统计标注框的相对面积分布,然后按面积区间重采样:

# 统计训练集中所有标注框的相对面积,用于判断是否需要小目标过采样 import os areas_small, areas_medium, areas_large = 0, 0, 0 for f in os.listdir('labels/train'): with open(os.path.join('labels/train', f)) as fp: for line in fp: _, x, y, w, h = line.strip().split() area = float(w) * float(h) if area < 0.015: # 相对面积小于 1.5% areas_small += 1 elif area < 0.1: # 1.5% - 10% areas_medium += 1 else: areas_large += 1 total = areas_small + areas_medium + areas_large print(f"小目标占比: {areas_small/total:.2%}, 中目标: {areas_medium/total:.2%}, 大目标: {areas_large/total:.2%}")

如果小目标占比低于 20%,训练时要考虑开 YOLOv8 的augment=true之外再叠加一个随机裁剪增强——在每轮迭代时把图像按 0.5 到 0.8 倍缩放后随机裁剪一块送入网络,等效于把小息肉“放大”后让模型多看到几次。具体实现时,可以直接调用 YOLO 训练参数里的crop_fraction(在 Ultralytics 的 8.x 版本中可用),也可以在前处理里手动叠。本方案更推荐前者,因为参数由官方统一调度,不会干扰 mosaic 增强的触发概率。

4.2 增强参数别贪多:mosaic 和 copy-paste 对内镜数据的特殊副作用

YOLOv8 默认开了 mosaic 增强,把四张图拼成一张新图训练。对自然图像是好事,对内镜图像却有一个隐藏副作用:息肉本身是类圆形、边缘光滑的病灶,四张图的拼接边界会产生锐利的伪边缘,模型可能会学到“边界线也是息肉特征”。这就是实践中常说的“玄学训练指标好看、真实视频上泛化差”的重要来源之一。

一个建议配置是:

  • 保留 mosaic,但将mosaic=0.5附近调低,而不是默认的 1.0;
  • 开启hsv_h=0.0,因为真实内镜图像通常不改变色调,训练结束后模型的色彩鲁棒性不应被虚假的颜色抖动引入;
  • degrees=5留一点点旋转,超过 5 度与真实肠镜的镜头姿态关系不大;
  • fliplr=0.5保留水平翻转,但注意如果标注时对“息肉位于镜头左侧还是右侧”有临床语义(比如辅助进镜方向),就不能做翻转。

这些参数在训练脚本里通过model.train(augment=True, mosaic=0.5, degrees=5, hsv_h=0.0)这种形式传入。没有哪一种增强是绝对正确的,关键是你改一个参数前,想清楚它模拟的是哪一种真实退化。

4.3 脏数据清洗:用一段快速推理把明显错误框找出来

人工标注再仔细,几千帧标注下来也难免出现漏标和错框。比人眼复核更高效的方式是先用当前版本的 YOLO 模型对训练集自身做一次预测,把预测结果和人工标注不一致的帧挑出来重点复查。这个过程在工程上叫“困难样本挖掘”,也是数据迭代的核心闭环。

# 用训练中的模型对训练集做回灌验证,输出置信度高于阈值的未匹配框 from ultralytics import YOLO import os model = YOLO('runs/detect/train/weights/best.pt') results = model.predict(source='images/train', conf=0.25, save_txt=False, save_conf=True) for r in results: img_path = r.path # 人工标注的框数量 label_path = img_path.replace('images', 'labels').replace('.jpg', '.txt') if not os.path.exists(label_path): continue with open(label_path) as f: num_gt = len(f.readlines()) num_pred = len(r.boxes) # 预测框远多于标注框时,大概率标注漏标 if num_pred > num_gt + 2: print(f"疑似漏标: {img_path}, 人工标注框数 {num_gt}, 预测框数 {num_pred}")

这段脚本的实用价值在于,它能把“标注员当时没看到的小息肉”重新找出来。参数 conf=0.25 是保守阈值,太低会输出大量噪声框,太高会把小目标的弱响应过滤掉。一般先按 0.25 跑一遍,人工确认是否漏标,把确认的帧重新标注,再把 conf 上调到 0.4 跑一遍,专查高置信度误检——比如把黏膜高光当成息肉的框。这个循环每做一轮,数据质量都能上一个台阶,比盲目增加标注量更值得投入。

5. 避坑指南:YOLO 息肉检测数据集最常见的 5 个踩坑点

5.1 训练正常收敛,验证集 mAP 很高,但真实视频上几乎检不出小息肉

现象:用 YOLOv8 默认分辨率 640 训练,loss 曲线平滑下降,val mAP@50 超过 0.85,但拿到另一台内镜设备录的视频上测试,5 毫米以下的扁平息肉全部漏检。原因:原始内镜视频抽帧后直接缩放成 640×640,扁平息肉原本只有 30×20 像素左右,缩放后变成 15×10 甚至更小,小于检测器在小尺度特征图上的最小可感知目标。解决:训练分辨率改为 1280 并同时调大输入尺寸,或者使用 YOLO 的小目标专门配置(在 8.x 版本里表现为增加一层 P2 输出头)。注意调大分辨率会等比增加训练显存占用,需要同步降低 batch size。实测相同数据,从 640 升到 1280,小息肉召回率能提升 8 到 15 个百分点,代价是单卡训练时间增加约 2.5 倍。

5.2 验证集指标一路绿灯,部署后同一个病灶检测结果上下跳变

现象:模型在视频流中检测同一个息肉,时而稳定框住,时而丢失一两帧,再又重新检出。原因:按视频划分训练/验证集时,验证集选到了镜头推进速度特别慢、画面特别清晰的一段;训练集则充满了冲洗、出血、模糊帧,分布不一致。解决:划分前先对视频片段做内容复杂度打标,保证训练集和验证集都包含“清晰平稳段”和“困难退化段”。常用做法是抽帧时按帧的不清晰度指数分桶,再在每个桶内做视频级划分。没有这一步,模型在真实场景中的实际鲁棒性是靠运气的。

5.3 同一条视频的连续帧被拆进了 train 和 val,指标虚高

现象:mAP@50 有 0.9,但换一台设备的视频就掉到 0.6,差距大得反常。原因:按单帧随机 8:2 划分,同一个息肉病灶的相邻十几帧画面几乎相同,好几帧进了训练集、好几帧进了验证集,验证时模型相当于“见过”目标了。解决:严格按视频 ID 划分,一步到位;如果视频数量不够,至少要保证同一视频内每隔 N 帧抽一张,N 根据镜头移动速度动态调整,最终目的就是避免同源画面被拆开。这是最容易修、也最容易被忽略的坑。

5.4 标注框把所有“可疑的东西”都框进去,类别定义前后不一致

现象:训练结束后,模型总是把肠道内残留粪水、气泡、冲洗水反光也检测成息肉。原因:标注阶段没有规定严格的阳性标准,第 100 帧标了息肉的红色隆起,第 500 帧时标注员对一个小隆起拿不准,也框了,还标成了 polyp。训练集里混入了大量“非息肉但像息肉”的噪声正样本。解决:标注规范前置,在我的流程里要求只能框“确认是息肉”的病灶,拿不准的另建一个 hard_samples 文件夹,不进入训练集。对于已经全局标完的一批数据,用 4.3 的高置信度误检脚本专门筛出这类样本,清洗掉或移出训练集,能显著降低误检率。

5.5 水面反光和暗角导致训练 loss 振荡,模型学不到稳定特征

现象:训练曲线在 epoch 40 到 50 之间突然出现抖动,然后 mAP 下降约 10 个点,之后缓慢回弹但恢复不到之前的高点。原因:增强里开启了随机 HSV 抖动,红光反射区域被随机改成暗红色或亮红色,自动增强滋生了一部分虚假的“颜色不变性”样本,干扰了模型对息肉红色调特征的稳定提取。解决:把hsv_h、hsv_s调到接近 0,仅保留少量hsv_v亮度抖动;同时关闭 mosaic 的拼接补丁,或设置mosaic=0.3。这类“增强副作用”问题最难排查,建议在每次改增强参数后,用同一固定种子重训并记录 mAP 波动,而不是直接叠加使用默认配置。

6. 从检测框走向分割与实时辅助:数据集的下一步进化方向

当基础检测模型稳定后,数据集的下一个迭代方向是分割掩码。YOLOv8-seg 可以直接消费你的检测数据集,只要把 labels 里的矩形 txt 升级为多边形坐标的 segment 格式。这个转换比想象中省事:标注工具里把已标好的矩形框转成多边形,微调边缘让掩码贴合息肉轮廓,然后导出为 YOLO segment 格式的 txt,每行开头仍是 class_id,后面不再是“x y w h”,而是一串成对的多边形归一化坐标点。分割头的收益在于,矩形框在息肉不规则边缘上天然包含背景,导致置信度被背景像素干扰;掩码能提供更精准的边界,便于用户在辅助诊断界面上直接看到病灶轮廓。

验证数据集质量是否真正提升,除了 mAP,我还会用一组固定的困难视频作为“金标准测试集”,每个版本迭代后都跑一遍这组视频,统计召回率、误检数和帧间稳定性。这比反复调参更有判断价值。另一条实用建议是:在导出 ONNX 到实际内镜设备之前,先将视频按原始分辨率切成若干短片段,分别测试不同场景的漏检分布,记录每段视频的失败帧,回灌到数据清洗流程里做补充标注——我习惯把这条路称为数据集迭代的闭环,每走一轮模型就前进一步。希望这些从标注规范到数据清洗的经验能帮到你,让你做出来的数据集和模型真正经得起真实内镜视频的检验。

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

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

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

立即咨询