ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO肺结节目标检测:VOC/COCO标签转YOLO格式与训练验证实战

YOLO肺结节目标检测:VOC/COCO标签转YOLO格式与训练验证实战 简介面向医学影像与目标检测研究者的YOLO肺结节检测数据集包含1000张真实场景图片使用LabelImg精细标注提供VOC、COCO、YOLO三种主流格式标签可直接接入YOLO系列模型训练与验证。压缩包共2000个文件以XML与TXT标签文件为主辅以HTML图文教程、Python划分脚本和YAML配置文件整体约18.74MB目录按标签格式与用途分放便于按需提取。除数据外还附赠环境搭建、GPU驱动配置及Windows/Linux训练教程并提供将图片与标签同步划分为训练集、验证集、测试集的Python脚本可引导初学者从数据准备到模型训练完整跑通流程。目前已有812人学习/下载适合需要高质量医学影像数据并快速上手YOLO训练的开发者使用。1. 拿到YOLO肺结节目标检测数据集后先拆解RAR包里的三套标签一个 RAR 包同时给了 1000 张图、VOC/COCO/YOLO 三种标签、划分脚本和训练教程看起来像是“解压就能训练”但实际踩坑点几乎都藏在标签里。肺结节这类医学目标检测目标框经常只有十几个像素宽一张 512×512 的 CT 切片上结节占整图面积可能不到 0.1%。这种场景下VOC 和 YOLO 格式之间的坐标换算一旦写错模型不是训不出来而是训练过程正常、mAP 也不低可视化时却发现所有预测框都偏向图像一侧。另一个容易被忽略的问题是同一患者的连续切片高度相似划分脚本如果按图片随机切验证集结果会虚高换到真实 CT 序列上直接失效。这篇文章顺着这三个核心问题展开三套标签怎么读、怎么转、怎么划分最后落到训练后的验证技巧上。2. 肺结节YOLO目标检测数据集的目录结构VOC、COCO、YOLO标签怎么读2.1 为什么一份肺结节图片要配VOC、COCO、YOLO三份标签医学影像目标检测的开发链路里标注格式不只是训练脚本的输入也是整个团队协作的协议。标注工具默认输出 VOC XMLMMDetection 训练接口经常吃 COCO JSONUltralytics YOLO 系列只认纯文本的 YOLO 格式。如果只保留一种格式后面换框架、做模型对比、接第三方预处理库都要重写一遍标注解析。打包者把三套都放进 RAR 里省去的是你从 VOC 转向 YOLO 时最容易出错的那一步坐标换算这也正好覆盖了“yolov8训练自己的数据集”之前的数据准备环节。但省事的另一面是你并不知道这三套标注是否完全一致。我检查过不少公开数据集包常见问题包括VOC 和 YOLO 内容一致但 COCO 的 category_id 整体偏移 1某个框在 VOC 里有多余的name空格或者某张图只有图片没有 XML导致训练时那张图被静默跳过。所以拿到 RAR 后第一件事不是解压看 JPG而是把 labels 目录完整过一遍。2.2 常见RAR包解压后的目录树和文件布局以 Linux 环境为例解压带中文文件名或嵌套目录的.rar包用unar比unrar更省心它能自动处理编码问题unar lung_nodule_dataset.rar tree -L 2 du -sh images labels splittree用来确认目录层级du用来检查是不是有空目录或者某个 labels 子目录明显缺文件。假设按最常见的打包习惯整理你会看到类似这样的结构lung_nodule_dataset/ ├── images/ │ ├── patient001_slice01.jpg │ ├── patient001_slice02.jpg │ └── ... ├── labels/ │ ├── voc/ │ │ ├── patient001_slice01.xml │ │ └── ... │ ├── coco/ │ │ └── annotations.json │ └── yolo/ │ ├── patient001_slice01.txt │ └── ... ├── split/ │ ├── train.txt │ └── val.txt └── yolo_data.yamlimages与labels各子目录之间靠文件名关联。这种设计在你补图或重命名时最容易被破坏图片名和 XML 名一旦不一致VOC 转 YOLO 脚本会静默丢掉该样本。因此解压后先跑一条命令核对数量echo images: $(ls images | wc -l) echo voc: $(ls labels/voc | wc -l) echo yolo: $(ls labels/yolo | wc -l)三个数字不一致时不要往下走训练流程先去定位缺失文件。2.3 三种标签里的肺结节框到底差在哪同一个肺结节框在三种格式里长这样。VOC XML 记录的是左上角和右下角的绝对像素坐标annotation filenamepatient001_slice01.jpg/filename size width512/width height512/height /size object namenodule/name bndbox xmin214/xmin ymin201/ymin xmax232/xmax ymax219/ymax /bndbox /object /annotationCOCO 的annotations.json里bbox 是“左上角 x 左上角 y 框宽 框高”依然是像素绝对值{ images: [ {id: 0, file_name: patient001_slice01.jpg, width: 512, height: 512} ], annotations: [ {id: 0, image_id: 0, category_id: 1, bbox: [214, 201, 18, 18], area: 324} ], categories: [ {id: 1, name: nodule} ] }YOLO txt 只有一行坐标全部归一化到 0 到 1 之间记录的是框中心点和宽高0 0.4355 0.4102 0.0352 0.0352第一个数字是类别 ID后面四个分别是x_center, y_center, width, height都要除以图片宽高。一张 512×512 的图里18 像素宽的结节换算成 YOLO 坐标后只有 0.035这就是“小目标检测”在数据层面的直接体现框的数值非常小脚本里少除一个 512模型就会去学一个错误的尺寸分布。下面这张表总结了最关键的差异转换脚本的核心逻辑也都来自这里格式坐标参考点单位/范围类别编号VOC左上 (xmin, ymin)右下 (xmax, ymax)绝对像素name 字段通常是 nodule 字符串COCO左上 (x, y)加上宽高 (width, height)绝对像素category_id常见从 1 开始YOLO中心点 (x_center, y_center)加上宽高相对图片尺寸0~1class_id从 0 开始COCO 的 category_id 和 YOLO 的 class_id 是新手最容易踩的坑。COCO 虽然也支持从 0 开始编号但很多公开数据集沿用默认约定从 1 开始转换成 YOLO 时需要做category_id - min_category_id的偏移否则两套格式的类别含义会错位。3. 肺结节VOC/COCO标签转YOLO格式转换脚本与三个一致性校验3.1 坐标换算的三个方向和边界条件VOC 转 YOLO 的核心公式是x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_heightCOCO 转 YOLO 时因为 COCO 的 bbox 已经是“左上角 宽高”公式变为x_center (x width / 2) / image_width box_width width / image_width需要注意三点第一VOC 老版本里有xmax可能等于xmin的空框这种样本必须跳过或人工复核第二XML 里size写的不一定是真实图片尺寸最保险的做法是直接用 PIL 读取真实尺寸参与归一化第三clamp只能把越界坐标拉回合法范围不能替代标注修正。把越界的框简单截断到图像边界实际上是给模型喂了一个改过的目标这会让后续假阳性分析失真。3.2 直接能跑的VOC转YOLO最小脚本下面的脚本按单张图片逐个处理xml_dir指向 VOC 的 XML 目录image_dir指向原图目录out_dir写 YOLO txt#!/usr/bin/env python3 # voc2yolo.py import sys from pathlib import Path import xml.etree.ElementTree as ET from PIL import Image def xml_to_yolo(xml_path: Path, img_path: Path, out_path: Path): with Image.open(img_path) as im: img_w, img_h im.size root ET.parse(xml_path).getroot() lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name.lower() not in (nodule, pulmonary_nodule): continue b obj.find(bndbox) xmin float(b.findtext(xmin)) ymin float(b.findtext(ymin)) xmax float(b.findtext(xmax)) ymax float(b.findtext(ymax)) if xmax xmin or ymax ymin: print(f[warn] 空框或负宽高: {xml_path}) continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 限位但只用于兜底不能替代人工检查 x_center max(0.0, min(x_center, 1.0)) y_center max(0.0, min(y_center, 1.0)) box_w max(0.0, min(box_w, 1.0)) box_h max(0.0, min(box_h, 1.0)) lines.append(f0 {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_path.parent.mkdir(parentsTrue, exist_okTrue) if lines: out_path.write_text(\n.join(lines) \n, encodingutf-8) else: out_path.touch() # 保留 0 字节文件便于后面核对哪些图没标签 if __name__ __main__: xml_dir Path(sys.argv[1]) image_dir Path(sys.argv[2]) out_dir Path(sys.argv[3]) for xml_file in sorted(xml_dir.glob(*.xml)): image_file image_dir / (xml_file.stem .jpg) if not image_file.exists(): print(f[warn] 找不到图片: {image_file}) continue out_file out_dir / (xml_file.stem .txt) xml_to_yolo(xml_file, image_file, out_file)脚本里的touch()值得解释一下很多转换脚本遇到空 XML 会直接跳过结果 labels 目录里少一个 txt。YOLO 训练逻辑会认为“没有 txt 的图片没有目标”从而照常训练但验证阶段又找不到对应标注文件问题被隐藏得很深。保留空文件后续检查能直接发现这张图疑似标注缺失。COCO 转 YOLO 时重点改成遍历 JSON 里的images和annotations用image_id关联文件名再用category_id - min_category_id计算 class_id。代码结构类似唯一要多做的一步是先遍历一次annotations拿到所有出现的 category_id再决定偏移量不要硬编码成 1。3.3 转换后必须做的三个一致性校验转换完成不等于转换正确。我一般会按表格里的顺序做三轮检查校验项方法通过标准文件数量对齐对images和labels/yolo跑wc -l同名文件一一对应无多余 txt坐标范围合法写一个小遍历检查每行数值每个值都在 0 到 1 之间无负宽高可视化抽检把 YOLO 框画回原图逐张看框贴合结节边缘无整体偏移可视化脚本是这三步里最直观的也最容易发现问题# check_yolo.py from pathlib import Path import cv2 yolo_dir Path(labels/yolo) image_dir Path(images) out_dir Path(visual) out_dir.mkdir(exist_okTrue) for txt_path in sorted(yolo_dir.glob(*.txt)): img_path image_dir / (txt_path.stem .jpg) if not img_path.exists(): print([warn] 缺图:, img_path) continue img cv2.imread(str(img_path)) height, width img.shape[:2] for line in txt_path.read_text().splitlines(): parts line.split() if len(parts) ! 5: print([warn] 字段数不对:, txt_path, line) continue _, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * width) x2 int((xc bw / 2) * width) y1 int((yc - bh / 2) * height) y2 int((yc bh / 2) * height) if x1 0 or y1 0 or x2 width or y2 height: print([warn] 越界框:, txt_path, line) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(str(out_dir / img_path.name), img)画完关掉自动缩放窗口直接看图片原始像素尺寸。肺结节框如果比预期大一圈或者整体偏左上多半是 XML 的size和真实图片尺寸不一致脚本里用了假尺寸做归一化。这一步不做后面训练出来的模型框位置会整体偏移而且 mAP 可能不低因为模型学到的偏移模式在训练集和验证集中同时存在指标被系统性误差骗过了。4. 肺结节数据集划分脚本与YOLOv8训练流程从数据YAML到训练命令4.1 划分脚本的防泄漏设计按患者分组而不是按图片随机打散很多目标检测教程里的划分脚本长这样把所有图片shuffle前 80% 写进 train.txt后 20% 写进 val.txt。这个逻辑放到肺结节数据集上是有风险的。肺结节数据集通常来自 CT 序列同一个患者的相邻切片在纹理和形态上高度相似。如果按图片随机划分同一个患者的切片会同时出现在训练集和验证集模型相当于在“开卷考试”里评估mAP 会比真实场景高不少。等你把模型部署到新患者的 CT 序列上性能会明显回落。正确的做法是提取患者 ID按组划分。文件名如果是patient001_slice01.jpg这种格式可以用下划线分隔的第一个字段作为分组依据# split_by_patient.py from pathlib import Path from sklearn.model_selection import GroupShuffleSplit image_files sorted(Path(images).glob(*.jpg)) groups [p.name.split(_)[0] for p in image_files] gss GroupShuffleSplit(n_splits1, train_size0.85, random_state42) train_idx, val_idx next(gss.split(image_files, groupsgroups)) with open(split/train.txt, w, encodingutf-8) as f: for i in train_idx: f.write(str(image_files[i].resolve()) \n) with open(split/val.txt, w, encodingutf-8) as f: for i in val_idx: f.write(str(image_files[i].resolve()) \n)GroupShuffleSplit和普通train_test_split的区别在于它保证同一组的样本不会同时被分到训练集和验证集。如果文件名里没有患者 ID需要先从csv或子目录名里建一个patient_mapping字典。这一步宁可多花时间也不要靠经验猜测。4.2 data.yamlYOLO格式数据集的配置文件和目录约束划分脚本生成的 train.txt 和 val.txt 是“图片路径列表”YOLO 会从每个图片路径推导出对应的标签路径。假设图片路径是/data/images/patient001_slice01.jpgYOLO 默认到/data/labels/patient001_slice01.txt找标签而不是到annotations目录。data.yaml 写起来很简单path: /data/lung_nodule_dataset train: split/train.txt val: split/val.txt test: split/test.txt nc: 1 names: - nodulepath字段是根目录train和val可以填目录路径也可以填 txt 文件路径。针对这种 1000 张图的小规模医学数据集我更推荐填 txt 文件路径因为这样split目录本身就成了数据版本的一部分每轮实验用哪组划分一清二楚。这里要提前处理一个医学影像特有的问题很多 CT 切片导出时是单通道灰度图而 YOLO 预训练权重是在三通道自然图像上训练的。让灰度图强行变成三通道 RGB 的常见做法是三个通道复制同样内容from PIL import Image from pathlib import Path for img_path in Path(images).glob(*.jpg): im Image.open(img_path).convert(RGB) im.save(img_path)如果 RAR 包里的图片本身就是三个通道的 JPG只是看起来灰就不用动。如何判断读一下modeRGB 就跳过L 才转换。4.3 最小可复现的训练命令和三个必调参数以当前主流的 Ultralytics YOLOv8 为例训练命令是yolo detect train \ modelyolov8n.pt \ datalung_nodule_dataset.yaml \ epochs200 \ imgsz640 \ batch16 \ device0 \ patience30 \ cacheTrue \ projectruns/lung_nodule \ nameyolov8n_baseline参数里优先级最高的是cacheTrue。1000 张图不多全部放进内存后每个 epoch 的 IO 等待会大幅下降训练速度通常能快一倍。如果机器内存只有 16G可以改成cacheram或者干脆不加。patience30是第二个关键参数。数据量小模型很容易在 60 到 100 个 epoch 后开始在验证集上过拟合patience能自动停掉后续无效训练。不要在 1000 张图上强行跑满 200 个 epoch你会得到一个 mAP 很高但泛化很差的权重。第三个值得调整的是imgsz。如果原图是 512×512用 640 做训练已经足够如果原图是 1024×1024且结节目标比 5mm 还小可以尝试imgsz1024但显存占用和训练时间都会明显上升。建议先跑一个yolov8n的 baseline确认整条数据链路没问题再换yolov8s或yolov8m验证精度收益。参数建议值设定理由modelyolov8n.pt数据量小先跑通流程再考虑更大模型imgsz640 或 1024匹配原始切片分辨率不要盲目降采样cacheTrue1000 张图完全放得进内存省 IOpatience20~30防止小数据集过拟合避免无效 epoch 堆积另外如果整个 RAR 包里所有图片都带有结节没有一张是纯背景负样本模型在真实场景里会把血管截面、胸膜增厚都当成结节。遇到这种情况建议从公开的肺部 CT 数据里裁一批不含结节的切片放进训练集并让它们的 txt 文件为空标签里的 class 数量还是 1不需要额外加背景类。负样本的比例通常放到正样本的 1 到 2 倍。5. YOLO肺结节训练后的验证K折交叉验证与假阳性排查5.1 用5折交叉验证计算真实mAP单次随机划分的验证集只有一两百张图肺结节数据又高度不均衡一次验证指标很容易被一个难例带偏。我会在 1000 张图这种量级上做 5 折交叉验证每一折仍然按患者 ID 分组避免同一患者出现在训练和验证两侧。跑完后把每折的 mAP 拿出来看分布而不是只看平均值。每折分别训练代价比较大但可以先用一个折中方案只用两折验证每折 500 张训练图50 张验证图如果两折的 mAP 差距小于 2 到 3 个点再花时间跑完剩余三折。验证命令不需要改脚本yolo detect val \ modelruns/lung_nodule/yolov8n_baseline/weights/best.pt \ datafold_0.yaml5.2 从混淆矩阵和假阳性框里找阈值训练完成后runs/detect/train/目录下会生成confusion_matrix.png。肺结节只有一个类别这张图主要看两类错误真实结节被漏检到背景里以及背景被误检成结节。如果背景误检高第一优先不是调阈值而是回头补负样本如果漏检高再去看漏检目标的尺寸分布。定位漏检和误检的具体样本可以用低置信度阈值导出所有预测框再按目标尺寸做统计分析。推理时把conf降到 0.05让模型尽可能把所有可疑区域都吐出来然后写脚本计算每个预测框的像素面积。漏检目标是框特别小、置信度都低于 0.5 的说明模型对小目标召回不足误检目标是长宽比接近 1、纹理偏圆形的说明模型把血管截面当成了结节。导出这些结果之后把置信度阈值从 0.05 按 0.05 步长扫到 0.8记录每一档的漏检率和误检率找出漏检率开始陡增的那个临界点那就是当前模型最适合做辅助筛查的部署阈值。本文还有配套的精品资源点击获取
返回列表