ARTICLE DETAIL

资讯详情

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

糖尿病肾病病理图像数据集VOC转YOLO格式与训练实战

糖尿病肾病病理图像数据集VOC转YOLO格式与训练实战 简介面向糖尿病肾病DR眼底影像检测任务这份数据集包含4122张JPG眼底图像并为每张图像配套Pascal VOC格式XML与YOLO格式TXT标注覆盖normal、mild-DR、moderate-DR、proliferation-DR、severe-DR 5个类别分别对应正常、轻度、中度、增殖性、重度糖尿病视网膜病变可支撑智能筛查与分级检测模型训练。压缩包共2000个文件以XML标注文件为主另有使用说明TXT整体约44.31MB且不含分割路径的TXT图像、XML、TXT集中打包格式规整便于按检测任务重新划分训练集、验证集与测试集。已有139人浏览学习。解压后数据可直接按YOLO或VOC目录接入主流检测框架免去自行收集医学影像、标注和格式转换流程适合医学影像AI入门与进阶开发者训练五分类检测模型、验证算法改进效果或开展对比实验也可用于数据增强、迁移学习与模型鲁棒性测试。1. 糖尿病肾病检测数据集一份能直接喂给YOLO的病理图像资产拿到糖尿病肾病检测数据集第一反应是“终于有货了”然后解压、写训练脚本、等mAP。实际做过几轮之后你会发现VOC和YOLO两种格式之间差的不是文件后缀而是坐标归一化、类别顺序和一系列看不见的脏数据问题。这个标题对应的数据集共4122张图像、5个类别覆盖肾小球硬化、肾小管间质病变这类病理切片的常见检测目标理论上解压后既能用YOLO直接训也能倒回VOC做传统检测器。适合做病理AI课题验证、毕业设计选型以及第一次接触医学图像目标检测的工程人员。它能用但直接训会翻车需要先完成格式转换、数据体检和类别统计这几步。2. 解压与盘点.7z在Linux和PyCharm下的处理与VOC目录结构拿到 .7z 压缩包我习惯先测试完整性和目录结构再真正解压而不是直接双击或者一把梭7z x。医学数据集的压缩包体积通常不小下载过程很容易丢块解压到一半报“CRC Failed”是常见事故。7-Zip 命令行里有两个参数值得记住t用于测试完整性x用于解压并保留目录树配合-o指定输出目录。2.1 先验证完整性再解压7z 命令的两个必用参数在 Linux 环境下如果还没有 p7zip先安装再操作# Debian/Ubuntu 系安装 p7zip sudo apt update sudo apt install -y p7zip-full # 测试压缩包完整性 7z t 糖尿病肾病检测数据集VOCYOLO格式4122张5类别.7z # 解压到 datasets 目录-o 后面不要带空格 7z x 糖尿病肾病检测数据集VOCYOLO格式4122张5类别.7z -odatasets/7z t会逐块校验并输出状态如果中途出现红色 ERROR说明压缩包不完整不值得继续浪费时间。7z x解压出来的是完整目录结构这个参数要看准如果误用了e所有文件会被摊在一个平目录里VOC 的 JPEGImages 和 Annotations 就会混在一起后面脚本全得重写。Windows 下的处理更简单装好 7-Zip 之后把 7-Zip 安装目录比如C:\Program Files\7-Zip加入系统 PATHPyCharm 的 Terminal 面板里就能直接敲7z命令。这样做的收益是后续所有数据预处理都在项目终端里完成不用来回切窗口。PyCharm 里“添加 7z”的本质就是把命令行工具暴露给虚拟环境而不是装什么插件。如果不想改 PATH也可以在 PyCharm 外部用 cmd 进入压缩包所在目录执行同样的命令。2.2 摸清数据家底统计类别、图像尺寸与标注框数量解压完成后目录里如果是标准 VOC 风格通常能看到JPEGImages、Annotations、ImageSets/Main这三个核心目录。标题里写了“VOCYOLO 格式”实际拿到手时可能是双份标注齐全也可能只有一边这些都不影响操作——重点是把类别、框数量、图像尺寸统计一遍这个步骤能直接决定后续转换脚本怎么写。用 Python 遍历Annotations里的 XML最快能摸清类别分布# inspect_classes.py import os import xml.etree.ElementTree as ET ann_dir Annotations classes {} for xml_name in os.listdir(ann_dir): tree ET.parse(os.path.join(ann_dir, xml_name)) for obj in tree.findall(object): name obj.findtext(name) if name is None: continue name name.strip() # 清理前后空格防止同一个类别被拆成两个 classes[name] classes.get(name, 0) 1 for name, count in sorted(classes.items(), keylambda x: x[1], reverseTrue): print(f{name}: {count})这段脚本的逻辑很直白逐条 XML 解析出所有name标签并计数。strip()是必须的病理数据集的标注常出现“Sclerosed Glomerulus ”这种带尾巴空格的情况不清理会出现两个看似相同实际不同的类别 id。跑完脚本后要重点看类别数是不是 5以及每个类别的样本量差距。如果某个病变类别只有几十张后面训练时就要针对类别不平衡专门处理。图像尺寸不能只看文件名要看 XML 里的size节点因为病理切片原图常常是几千乘几千的 TIF/SVS压缩成 JPEG 后尺寸差异依旧很大。用一个循环把每张图的宽高汇总确认是否有1024x1024这类的合理尺寸还是出现了5000x3000的超大图。后者意味着 YOLO 直接 resize 到 640 会把小病灶彻底压没这个坑放在后面训练章节再展开。3. 把VOC转成YOLO格式归一化坐标脚本与8:1:1划分VOC 的坐标体系用像素绝对坐标xmin ymin xmax ymax写在 XML 里YOLO 每张图对应一个同名 txt每行记录class_id x_center y_center width height后四个值全部归一化到 0~1。YOLOv8 训练脚本读取的就是这种 txt配合images和labels的同名目录组织。标题既然要求“VOCYOLO 格式”数据转换这一步就绕不开。3.1 为什么YOLO只认txt而不认xml标注格式与读取管线YOLO 系列训练管线在数据加载阶段用的是torch.load加自定义 Dataset每个 epoch 需要反复读取标注并做随机增强。VOC 的 XML 解析开销更大而且坐标是像素值每次增强都要重新换算。YOLO 的 txt 设计把归一化做到数据准备阶段训练时只需要读一行浮点数即就是新手也能直观看出每一行代表一个框改起来也不会破坏原始 XML。另外一个原因是 YOLO 的类别 id 从 0 开始而 VOC 是字符串类别名。转换脚本里如果直接按字母序或解析顺序给类别排 id后续改类别顺序会造成灾难性后果。所以脚本首先要全量扫描 XML收集去重后的类别列表再按固定顺序生成class_to_id映射整个过程可复现。3.2 转换脚本从Annotations到labels/train与验证下面这个脚本可以直接放进项目根目录运行它支持两种情形存在ImageSets/Main/*.txt就按预设划分读取没有就自动按 8:1:1 随机划分。核心思路是把 XML 的像素坐标转为归一化坐标同时建立标准的images/train、images/val、labels/train、labels/val目录结构。# voc2yolo.py import os import random import shutil import xml.etree.ElementTree as ET ROOT . # VOC 目录所在位置 IMG_DIR os.path.join(ROOT, JPEGImages) ANN_DIR os.path.join(ROOT, Annotations) SPLIT_DIR os.path.join(ROOT, ImageSets, Main) # 可能不存在 OUT_IMG os.path.join(ROOT, images) OUT_LABEL os.path.join(ROOT, labels) SEED 42 TRAIN_RATIO, VAL_RATIO, TEST_RATIO 0.8, 0.1, 0.1 # 1. 全量扫描构建类别表 class_names set() for xml_file in os.listdir(ANN_DIR): tree ET.parse(os.path.join(ANN_DIR, xml_file)) for obj in tree.findall(object): name obj.findtext(name) if name is not None: class_names.add(name.strip()) class_names sorted(class_names) class_to_id {name: i for i, name in enumerate(class_names)} print(类别表:, class_to_id) # 2. 确定图片名列表优先读 ImageSets否则自动划分 train_ids, val_ids, test_ids [], [], [] if os.path.exists(SPLIT_DIR): for split_name, out_list in [(train, train_ids), (val, val_ids), (test, test_ids)]: with open(os.path.join(SPLIT_DIR, split_name .txt)) as f: out_list.extend([ln.strip() for ln in f if ln.strip()]) else: all_ids [xml_file[:-4] for xml_file in os.listdir(ANN_DIR) if xml_file.endswith(.xml)] random.seed(SEED) random.shuffle(all_ids) n_train int(len(all_ids) * TRAIN_RATIO) n_val int(len(all_ids) * VAL_RATIO) train_ids all_ids[:n_train] val_ids all_ids[n_train:n_train n_val] test_ids all_ids[n_train n_val:] print(未发现 ImageSets已按 8:1:1 自动划分) # 3. 转换并写入 labels for split_name, id_list in [(train, train_ids), (val, val_ids), (test, test_ids)]: os.makedirs(os.path.join(OUT_LABEL, split_name), exist_okTrue) os.makedirs(os.path.join(OUT_IMG, split_name), exist_okTrue) for img_id in id_list: xml_path os.path.join(ANN_DIR, img_id .xml) tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.findtext(width)) img_h int(size.findtext(height)) label_lines [] for obj in root.findall(object): name obj.findtext(name).strip() cls_id class_to_id[name] box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 过滤非法框负坐标或宽高为 0 直接跳过 if xmax xmin or ymax ymin: print(f警告: {img_id} 中 {name} 的 bndbox 非法) continue x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h label_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(OUT_LABEL, split_name, img_id .txt), w) as f: f.write(\n.join(label_lines)) # 复制图片到 images 分目录 src_img os.path.join(IMG_DIR, img_id .jpg) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(OUT_IMG, split_name, img_id .jpg)) # 4. 写出 classes.txt 供 data.yaml 使用 with open(os.path.join(ROOT, classes.txt), w) as f: f.write(\n.join(class_names)) print(转换完成类别总数:, len(class_names))脚本的几个关键点说明如下。第一先扫描所有 XML 收集类别再转换。这样保证class_names的顺序不依赖单个 XML 的解析顺序每次运行结果一致。第二点要注意归一化除以的是size里的宽高而不是图片实际尺寸如果 XML 的size与 JPEG 真实尺寸不符会出现框偏移这一步最好在转换前用cv2.imread抽查几张做校验。第三过滤条件是xmax xmin或ymax ymin负坐标也包含其中因为它们换算后必然越界。SEED 42保证自动划分可复现。医学数据集经常存在同一病人的多张切片严格的划分应当按病人分组但这需要额外的 meta 表和业务知识数据包里如果包含 patient_id 之类的字段划分前先按病人去重再 split 更合理。转换完成后classes.txt里的顺序就是后续data.yaml中names的顺序最好不要手改编号避免错位。3.3 转换后的自检文件数量对齐与坐标范围检查转换完成不能直接开训先做两个快速自检一是核对images/train和labels/train目录下的文件数量是否一一对应二是检查所有 txt 里的坐标是否都在 0~1 区间内。# 统计每个 split 下图片与标签数量是否符合预期 for split in train val test; do echo $split images: $(ls images/$split | wc -l) echo $split labels: $(ls labels/$split | wc -l) done # 找出所有坐标越界的 label 文件 find labels -name *.txt -exec awk {if ($20 || $21 || $30 || $31 || $40 || $50) print FILENAME, $0} {} \;第一段命令看数量是否一致第二段用 awk 扫描每行字段。坐标越界最常见的原因是 XML 里出现xmin xmax或归一化时除以了img_w写成了img_h在自检阶段抓到成本最低。出现越界文件要回到对应 XML 逐条看不要图省事直接剔除尤其是医学数据一个错误框可能对应着一个真实的病变区域。4. 训练前先验数据可视化、边界框检查与4个常见问题排查格式转换后的数据需要先可视化验证否则模型训练到一半才发现标签错位返工成本很高。我见过最典型的事故是类别 id 映射错误导致模型把硬化肾小球当正常组织学loss 下降但混淆矩阵完全错乱整轮训练白费。数据集可视化正是防这种翻车的环节。4.1 画框可视化用OpenCV验证每一条归一化坐标用 OpenCV 把 txt 里的归一化坐标还原成像素框逐张检查。下面脚本对一张图执行强校验框四角是否落在图内标签是否能对上目标位置。# check_labels.py import os import cv2 def draw_labeled_image(img_path, label_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: lines f.read().strip().splitlines() for line in lines: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) # オーバーフロー防止 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w - 1, x2), min(h - 1, y2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) return img if __name__ __main__: class_names open(classes.txt).read().strip().splitlines() for split in [train, val]: label_dir flabels/{split} for fn in os.listdir(label_dir): img_id fn[:-4] img_path fimages/{split}/{img_id}.jpg label_path os.path.join(label_dir, fn) if not os.path.exists(img_path): continue vis draw_labeled_image(img_path, label_path, class_names) cv2.imwrite(fvis_{split}_{img_id}.jpg, vis)draw_labeled_image里的关键步骤是还原坐标(xc - bw/2) * w算出左上角 x右下角是(xc bw/2) * w。这个还原如果在转换脚本里写错可视化一眼就能看出来。逐张看太耗时我一般抽 val 里每个类别各 10 张以及 label 文件行数异常少的图。看到框全漂在组织外就回查 XML 里的size与cv2.imread的实际尺寸是否一致。4.2 病理大图与小目标的冲突切图策略先想清楚病理切片扫描图通常大到单张几十 MB压缩后的 JPEG 也经常是 2000×2000 起步而病变目标如结节性病变可能只有几十乘几十像素。YOLO 训练时走 resize 流程把 4000×4000 直接缩到 640相当于小目标缩小六倍多特征彻底被池化层抹掉。针对这种场景常见做法是切 patch 训练把整图切成 512×512 或 1024×1024 的 patch标注坐标跟着平移、裁剪只保留落在 patch 内的框。切 patch 时要注意重叠区两张相邻 patch 都应保留边界处的目标否则目标恰好骑在切缝上就会丢失。重叠率通常取 10%~25%视目标大小而定。标题虽然没有明确图像原始分辨率但 4122 张数据若要直接训 640大概率部分小目标类别 mAP 会很难看。所以拿到数据后先统计尺寸再决定是否切 patch这一步在训练前完成后面才不会返工。4.3 高频坑排查4条现象、原因与解决办法检查过程中最容易撞见下面这几类问题按现象、原因、解决三步走能快速定位。现象一训练时日志里持续提示No labels found每个 batch 都显示 0 张可用图。原因多半是data.yaml里的train、val路径没指向images/train、images/val或指向的是labels目录。解决打印os.listdir确认目录层级并确保 images 和 labels 下都有同名且非空的 txt 文件。现象二某张图可视化后框全部飞在图像外。原因通常是 XML 里的size和实际 JPEG 尺寸不一致。下载下来的数据里如果标注工具中途改过尺寸会出现这种情况。解决用cv2.imread(path).shape读实际尺寸对比 XML 的 width/height保留较大的坐标系为基准重算一遍归一化。现象三类别统计出现“多了一个类别”比如 5 类别变 6 类别。原因是标注文本里存在大小写或空格不一致像Normal_Glomerulus与normal_glomerulus被当成两种。解决做类别清理时统一小写加 strip再做一次类别打印人眼扫一遍确认没有重复项。现象四训练 loss 逐渐下降但 val mAP 长期为零。原因很可能是验证集中某种类别完全没有预测样本或者切分时重叠了训练数据。医学数据常按病人分组如果切分时不去重同病人的多张切片会同时出现在 train 和 val 里导致指标虚高但实际泛化差。解决统计唯一病人 id 再做分层切分或者至少按文件名前缀把同 root 的图分到同一个 split。5. 用这份数据训出可用的模型data.yaml、损失函数与类别失衡数据验证通过后进入模型训练阶段。病理数据集和自然图像数据集最大的区别在于颜色分布统一、目标尺度集中但也存在类别严重不平衡的问题。这一章给出一套能直接复现的最小配置再说明为什么特定超参数对病理小目标格外关键。5.1 最小训练配置data.yaml和一条yolo命令先把data.yaml写对。path最好用绝对路径train和val指向 images 下的分目录names必须与转换时生成的classes.txt严格对应path: /home/workspace/datasets/dkd_yolo train: images/train val: images/val test: images/test nc: 5 names: 0: normal_glomerulus 1: sclerosed_glomerulus 2: tubular_atrophy 3: interstitial_fibrosis 4: nodular_lesionnc是类别数names顺序错了模型会学到错位映射这条错误最难排查因为训练过程一切正常推理结果却完全对不上。转换脚本里已经生成了classes.txt直接把内容粘贴进去即可不需要手工脑补名称。训练命令用 ultralytics YOLOv8 的话一条就能跑起来yolo detect train datadata.yaml modelyolov8n.pt epochs150 imgsz640 batch16 device0初次跑通用yolov8n.pt做最小验证即可。如果显存允许换yolov8s.pt在病理小目标上的表现通常比 nano 好一截参数量增大带来的收益在细粒度病灶上是可感知的。imgsz640只是保底如果切过 patch 或者原始图分辨率不高提到imgsz1280通常 mAP50-95 会提升代价是训练时间翻倍。这个参数需要根据实际目标尺寸决定不是越大越好过大的 imgsz 会让小目标框的归一化数值变得极度集中框回归更难收敛。5.2 病理小目标的增强与超参数配置YOLO 的 mosaic 增强把四张图拼成一张本意是提升泛化但在病理图上副作用明显拼图会把一个肾小球切成两半或把正常组织与病变组织拼在一起形成虚假的形态学边界。训练日志里常见的“warping”警告就是这么来的。对于病理检测我一般把 mosaic 关掉或者降到 0.5 以下同时把 scale 和 translate 调小yolo detect train datadata.yaml modelyolov8s.pt epochs150 imgsz1280 batch8 device0 \ mosaic0.3 scale0.2 translate0.1 hsv_h0.02 hsv_s0.3 hsv_v0.2mosaic0.3保留一部分增强但不让拼图成为主要样本来源。scale0.2限制缩放范围防止目标被极致缩小成几个像素。HSV 增强在病理染色图像上要克制PAS 染色下颜色本身就是鉴别诊断依据过度调色会把染色差异变成伪造信号。这些参数是经验值正式训练前建议在 val 上做多组对比不要只盯 train loss。5.3 类别严重失衡时的三个处理方向糖尿病肾病 5 个类别里正常肾小球和间质纤维化数量往往远超结节性病变如果直接训练模型会把多数类学到满分、少数类学成零。处理思路有三个方向按实现成本从低到高排列。方向一是过采样把含稀有类别的图片在训练集里复制多份复制的图片可以做不同程度的翻转、光照扰动避免纯重复。方向二是复制粘贴增强把稀有类别的小目标框从原图裁出来透明叠加到另一张同染色风格的背景图上。要注意不要覆盖到已有的真实病变区域叠加后做一次高斯模糊边缘否则模型会学到“玻璃贴片”式特征。方向三是损失函数调整把 YOLOv8 分类分支的 BCE 换成带类别权重的 Focal Loss 变体让模型在稀有类样本上获得更大的梯度。一个轻量的 Focal Loss 实现可以直接嵌入训练脚本# focal_loss.py import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): ce F.binary_cross_entropy_with_logits(pred, target, reductionnone) p torch.sigmoid(pred) focal (1 - p) ** self.gamma * ce loss focal * (self.alpha * target (1 - self.alpha) * (1 - target)) return loss.mean()alpha0.25偏向正样本gamma2.0抑制易分类样本的梯度。病理数据中负样本多得离谱alpha 可以调到 0.5~0.7 之间gamma 从 1.5 起步试太大了模型会过拟合到难样本上在 val 上损失反而快速抖动。这三个方向可以叠加使用但每次只改一个变量不然无法判断哪个手段起了作用。6. 验证不止看mAP用混淆矩阵和阈值选择守住漏检率训练结束后很多人的习惯是看 best.pt 的 mAP50 大于 0.7 就收工但病理检测场景里漏检一个硬化结节可能比误检十个正常组织更危险。所以跑完yolo detect val之后我通常强行要求自己再看三个东西混淆矩阵、F1-confidence 曲线、以及每个类别的 recall。验证命令本身很简单yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml跑完输出目录里会有confusion_matrix.png和results.csv。混淆矩阵横轴是预测类别纵轴是真实类别重点看对角线两侧。如果nodular_lesion这一行几乎全部落在background列说明该类别被模型完全漏掉如果它大量落在normal_glomerulus列说明模型学到的是亮度或纹理特征而不是真正的形态学差异。这两种情况都值得回头处理数据而不是靠调阈值解决。阈值选择靠 F1-confidence 曲线。默认置信度 0.25 在自然图像上是个折中值但病理检测目标密集且尺度小0.25 会把大量低分真实框过滤掉。我自己的习惯是用 val 上 F1 最高的点作为推理阈值如果业务上更关心查全率就再往下压 0.05~0.1。曾经有一个项目我把置信度设 0.5mAP50 看着很漂亮后来病理医生复核时指出漏了几个硬化小球一查才发现这些框的 conf 集中在 0.3~0.45。从那以后我几乎不看 mAP 单独做决策而是先确认混淆矩阵里最关心的类别 recall再决定阈值落在哪里。验证阶段还不止指标。医学检测模型真正上线前至少要抽 200 张 val 图做一次人工复核把预测框叠加到原图上按类别分目录存放请标注方或研究者快速浏览。这一步的作用是发现指标看不见的问题比如框与病灶边界错位半个目标大小、某类目标被模型反复切成两个半框。跑完这一轮再决定模型是否值得进临床预试验。希望这套从解压到验证的流程能让你少走几趟弯路也愿这份数据在你的任务里跑出一个对病理判断真正有用的结果。本文还有配套的精品资源点击获取
返回列表