☰
糖尿病肾病病理切片目标检测:VOC+YOLO双格式数据集与训练避坑指南
2026/10/5 3:03:13 网站建设 项目流程

简介:一套面向糖尿病肾病检测的数据集,适用于医学影像目标检测模型训练与算法验证,尤其适合研究YOLO、Faster R-CNN等常见检测框架的开发者使用。数据已按Pascal VOC与YOLO两种格式组织,标注类别包括mild-DR、moderate-DR、normal、proliferation-DR、severe-DR共5类,覆盖从正常到严重增殖期的病变等级,便于开展分类与检测实验。资源包共2000个文件,以1999个xml标注文件为主,另含1个txt使用说明,整体压缩包大小约44.31MB,下载解压后即可查看目录结构与标注内容。已有139人学习使用,适合医学图像处理方向的学生、算法工程师在模型训练、数据增强、类别分布分析等环节直接取用。整理者按照统一命名规则组织文件,可显著减少数据清洗和格式转换成本,帮助使用者将更多精力聚焦于模型调优与评估。

1. 糖尿病肾病检测数据集:4122 张双格式标注,先验证数据再决定怎么训

做糖尿病肾病病理切片检测的人都知道,标注比模型更稀缺。拿到一个VOC+YOLO双格式的4122张五分类数据集,第一反应不该是直接开训,而是先确认压缩包里的图片和标注没有在整理过程中被折腾坏。VOC格式适合做通用目标检测与二次标注,YOLO格式是yolo系列训练和推理工具链最顺手的一环,这个数据集同时给两套,等于省掉了耗时的格式转换环节。适合有病理切片、显微图像检测需求,愿意花一晚上把数据链路跑通再投入训练的人。下一步先把 .7z 解压、核对文件数,再谈训练。

2. 先解开 .7z 再说别的:Linux/PyCharm 两种解压路径与数据完整性校验

2.1 Linux 下解压 7z:p7zip 安装与破坏性参数说明

7z 不是 Linux 标配解压器,很多服务器上没装,直接跑7z x会先报 command not found。Debian/Ubuntu 装 p7zip-full,CentOS/RHEL 装 p7zip 加 p7zip-plugins,后者不含 plugins 连 .7z 都解不了。

# Debian/Ubuntu sudo apt update && sudo apt install -y p7zip-full # CentOS/RHEL 7/8/9 sudo yum install -y p7zip p7zip-plugins

装完用7z i确认版本和格式支持,能列出 7z 说明插件加载正常。

# 先做完整性测试,不实际解压 7z t 糖尿病肾病检测数据集VOC+YOLO格式4122张5类别.7z # 解压到指定目录,/data/dkd 需预先创建 7z x 糖尿病肾病检测数据集VOC+YOLO格式4122张5类别.7z -o/data/dkd -y

参数说明:x是完整解压并保留目录结构,e会把所有文件摊平到单目录,这种标注数据集千万别用e,目录层级一旦丢了,后面找 train/val 划分和标签对应关系会很难受;-o指定输出目录,-o与路径之间不能有空格;-y跳过所有确认提示。解压后先看顶层目录:

ls /data/dkd

常见的包内布局是images/、Annotations/(VOC 的 xml)和labels/(YOLO 的 txt),也可能把整个 VOC 目录套在一层子文件夹里。别急着写训练脚本,先用一条命令统计各类文件数量,判断目录结构与预期是否一致:

find /data/dkd -type f | sed 's/.*\.//' | sort | uniq -c
# 如果包内文件是 GBK 编码的中文名,Linux 下可能乱码 # 先只查 jpg/xml/txt 三类,乱码文件一般不会影响训练 find /data/dkd \( -name '*.jpg' -o -name '*.xml' -o -name '*.txt' \) | wc -l

提示:包内文件名如果是中文且压缩时用了非 UTF-8 编码,Linux 解压后可能显示乱码。处理方法是用convmv转码文件名,或者解压后直接按扩展名和内容匹配,不要依赖文件名里的中文做逻辑判断。

2.2 PyCharm 里添加 7z:Windows/macOS 本地预览与抽查

在 PyCharm 里直接双击 .7z 只会看到二进制内容,没法展开预览。想快速抽查几张图和标签,常见做法是装 7-Zip 并把它加入系统 PATH,然后在 PyCharm 的 Terminal 里直接调用 7z 命令,省得每次都在图形界面里拖来拖去。

# Windows 下 7-Zip 安装目录加入 PATH 后 7z t DKD.7z 7z x DKD.7z -oC:\datasets\dkd

如果不允许装图形软件,也可以直接在 PyCharm 里跑 Python 的 py7zr 库,这个库 pip 安装即可,适合纯终端环境:

import py7zr with py7zr.SevenZipFile('糖尿病肾病检测数据集VOC+YOLO格式4122张5类别.7z', mode='r') as z: names = z.getnames() print(len(names), names[:10]) # 先看包内路径,确认目录结构 z.extractall(path='/tmp/dkd_preview') # 全量解压到临时目录

参数说明:mode='r'是只读打开,不会碰原包;getnames()返回包内完整路径列表,能在真正解压前先判断有没有images/、Annotations/这些顶层目录;extractall(path=...)全量解压,输出目录不存在会自动创建。macOS 同样用 p7zip 或 py7zr,不必额外装图形界面。

2.3 解压后抽五分钟做三件事:文件数、图片可读性、哈希一致

数据从别人手里拿过来,最怕的不是压缩包打不开,而是解压一半断电、下载丢包导致某个 jpg 损坏,训练时 dataloader 在某个 epoch 突然崩掉,再回头查是哪张图。解压完先按下面顺序排查。

# 1) 统计各类文件数量,4122 张图应一一对应 find . -name '*.jpg' | wc -l find . -name '*.xml' | wc -l find . -name '*.txt' | wc -l # 2) 对整包做一次 sha256 校验,和发布方给的哈希对比 sha256sum 糖尿病肾病检测数据集VOC+YOLO格式4122张5类别.7z

文件数对不上时,先对照 xml 和 txt 的文件名集合,优先怀疑两版标注不是同一批人补的,有人单独改过图。图片可读性用 Python 批量检查,4122 张规模用verify()足够,它只校验文件头和完整度,不会真的把像素解码一遍。

from PIL import Image import glob bad = [] for p in glob.glob('/data/dkd/**/*.jpg', recursive=True): try: im = Image.open(p) im.verify() except Exception as e: bad.append((p, str(e))) print('损坏图片数:', len(bad), bad[:5])

注意verify()之后如果要再读像素,必须重新Image.open,因为 verify 会关闭文件句柄。这一步做完,数据链路才算真正立住,后面训练崩了就不会再把锅甩给数据文件。

3. 把 VOC 和 YOLO 两套标注对齐:格式差异、转换脚本与五类别标签映射

3.1 VOC 的 xml 到底存了什么:逐字段看一遍 bndbox

拿到数据后先打开一个标注文件,确认 object 字段里的 name、bndbox 与 difficult/truncated。下面用示意性名称展示结构,真实五类以解压后 grep 的结果为准。

<annotation> <folder>DKD</folder> <filename>case_00123.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>glomerulus</name> <bndbox> <xmin>420</xmin> <ymin>280</ymin> <xmax>610</xmax> <ymax>470</ymax> </bndbox> <difficult>0</difficult> </object> <object> <name>interstitial_fibrosis</name> <bndbox> <xmin>1200</xmin> <ymin>150</ymin> <xmax>1390</xmax> <ymax>330</ymax> </bndbox> <difficult>0</difficult> </object> </annotation>

这段结构里有几个关键判断点:size决定后续 YOLO 归一化的分母;filename一般不带扩展名前缀;difficult=1的样本在转换时通常跳过,因为它代表边界难以确认的样本。还要注意同一张图可以出现多个 object,也就是多个目标框。拿到压缩包后第一步应该先对比所有 xml 里 name 的去重结果,确认五类的真实拼写,别凭印象写 CLASS_MAP。

grep -hoP '(?<=<name>)[^<]+' Annotations/*.xml | sort | uniq -c

这条命令会用正则把每个 object 的 name 字段抽出来,统计每个类别出现次数。输出里的类别名,才是你写 CLASS_MAP 的唯一依据。如果出现第六个类名,先回去翻数据说明,别急着把多余类别合到背景里。

3.2 用脚本把 VOC 转 YOLO:归一化坐标从精度这个坑说起

YOLO 标签是每行一个目标的 txt,五个字段分别是class_id x_center y_center width height,全部相对图片宽高归一化,而且是中心点表示。转换时翻车点很集中:有人直接用 int 相除,把x_center算成 0;也有人忘记跳过 difficult,把噪声框当正样本。下面是一段可以直接用的转换脚本骨架。

#!/usr/bin/env python3 import xml.etree.ElementTree as ET # 这里填上一步 grep 到的真实类别名,顺序即类别 ID,写错全完 CLASS_MAP = { 'glomerulus': 0, 'tubular_atrophy': 1, 'interstitial_fibrosis': 2, 'arteriolosclerosis': 3, 'normal_tubule': 4, } def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() w = int(root.findtext('size/width')) h = int(root.findtext('size/height')) with open(out_path, 'w') as f: for obj in root.findall('object'): name = obj.findtext('name') if name not in CLASS_MAP: continue if obj.findtext('difficult') == '1': continue box = obj.find('bndbox') x1 = float(box.findtext('xmin')) y1 = float(box.findtext('ymin')) x2 = float(box.findtext('xmax')) y2 = float(box.findtext('ymax')) xc = ((x1 + x2) / 2) / w yc = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h f.write(f'{CLASS_MAP[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n')

代码逻辑说明:先解析 xml 拿图片宽高;遍历每个 object,用findtext取子字段,避免缺字段时直接抛 KeyError;difficult 为 1 时跳过;最后写出与图片同名的 txt。这里CLASS_MAP是命门,必须和包内真实类别拼写完全一致,大小写、空格都不能差。

参数说明:xc、yc是中心点归一化坐标,理论范围在 0 到 1 之间;bw、bh是目标宽高占图片的比例。写入时保留 6 位小数,精度足够。YOLO 训练时labels/case_00123.txt和images/case_00123.jpg必须同名且目录结构对应,否则 dataloader 根本找不到标签。如果压缩包已经自带 YOLO 版 txt,这个脚本只用来抽检,别盲转全量覆盖。

3.3 VOC/YOLO 两套标注的一致性检查:找到并排除悬空文件

数据包同时存在两种格式,最容易埋下隐患的是两套标注不同步:某张图的 xml 有 3 个目标,txt 却只有 1 个;或者 txt 里类别 ID 和 CLASS_MAP 对不上。逐张核对用一段十行脚本就能完成。

import glob, os xml_files = {os.path.splitext(os.path.basename(p))[0]: p for p in glob.glob('Annotations/*.xml')} txt_files = {os.path.splitext(os.path.basename(p))[0]: p for p in glob.glob('labels/*.txt')} only_xml = set(xml_files) - set(txt_files) only_txt = set(txt_files) - set(xml_files) print('仅 xml 有标注:', len(only_xml), '仅 txt 有标注:', len(only_txt)) for k in set(xml_files) & set(txt_files): n_xml = open(xml_files[k]).read().count('<object>') with open(txt_files[k]) as f: n_txt = len(f.readlines()) if n_xml != n_txt: print(k, 'xml:', n_xml, 'txt:', n_txt)

统计不一致的目的不是立即去改,而是先标记:xml 有 5 个 object、txt 只有 4 个的文件,训练时 YOLO 看到的是 5 个真值,验证时却按 4 个算,指标漂移会很难排查。对这类文件,最简单的处理是直接从训练集里剔除,而不是手工补标注。

提示:VOC 的 xml 可以包含 truncated、occluded、difficult 等附加属性,转 YOLO 时默认只保留 bndbox 和 name。如果某些框的 difficult=1,建议在统计一致性的脚本里先把它们排除,否则两套标注的目标数永远对不上。

4. 用 YOLO 在糖尿病肾病数据上开训:data.yaml、模型选型与第一轮参数

4.1 data.yaml 怎么配才对得上五类别

直接把 VOC/YOLO 目录丢给 yolo 训练是行不通的,它只认 data.yaml。yaml 里的 train/val 可以是绝对路径也可以是相对目录,nc必须和实际类别数一致,names顺序必须和训练标签里的 class_id 顺序一致。标签里类别 ID 是 0,就对应 names 下标 0;顺序错了,模型会在训练中静默学会错序映射,运行起来看似正常,实际两个类的 bounding box 被完全调换。

path: /data/dkd # 数据集根目录 train: images/train val: images/val nc: 5 names: 0: glomerulus 1: tubular_atrophy 2: interstitial_fibrosis 3: arteriolosclerosis 4: normal_tubule

提示:上面 names 只是示意,必须用第 3.1 节 grep 出来的真实五类替换。如果压缩包里没有划分好的 train/val,先按 8:2 划分目录并保证两边类别分布近似。划分完成后,建议立即对比 train 和 val 每类的目标数量,不要只看图片数量。

4.2 预训练权重、输入分辨率与 epochs 的第一枪

针对肾组织切片这种目标尺度小、轮廓不规则、正样本密度高的场景,建议从 yolov8s.pt 起步,别一上来就用 yolov8n。

yolo detect train \ data=dkd_data.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=200 \ batch=16 \ patience=30 \ seed=42 \ cache=True

参数说明:model=yolov8s.pt会从 COCO 预训练权重继续训练;imgsz=640对切片类图像够用,若原图分辨率很高(几千乘几千)且目标很小,建议先做 tiling 切块,把每块降到 640 或 1280,否则小目标会直接消失在多次下采样后的特征图里;batch=16取决于显存,16G 显卡跑 yolov8s 一般能撑到 16 左右;patience=30表示验证集连续 30 个 epoch 没提升就早停,医学数据集噪声大,早停值设太小容易把模型掐在震荡期。

第一轮训练别急着调 anchor 和各种损失函数权重。先跑通一遍数据链路,把损失曲线和验证集指标看成形,再谈优化。模型选 yolov8s 而不是 yolov8n,是因为糖尿病肾病这类病灶目标在整张切片里占比小,n 模型容量不足以拟合细粒度纹理边界。

4.3 训练中途该盯哪几个数:loss 不降先查数据而不是调参

训练日志会同时给出 box_loss、cls_loss、dfl_loss 和验证集 mAP。这里有一条原则:第一个 epoch 结束因为预训练权重没有收敛到医学域,loss 偏高很正常;但前 20 个 epoch 如果 box_loss 完全没有下降趋势,基本可以断定是数据问题而不是学习率。回去查三类错误:图片有坏图、标签越界、class_id 超出 nc。

用 yolo 训练时,runs/detect/train目录下会产出results.png和混淆矩阵。我一般只盯三条:

  • box_loss 训练集上是否在前 50 个 epoch 内从较高值平滑下降;
  • val/box_loss 不能比 train/box_loss 低太多,低很多说明样本划分有问题;
  • 五类中每一类的 AP 不能只有一类特别突出,那是类别不平衡的信号。

凡是看到 mAP 很高而 PR 曲线面积很小,先怀疑训练脚本是不是用了错误的标签目录。因为把labels指到了和images不一致的地方时,yolo 会静默地用空标签训练,模型学到的只是背景。

5. 糖尿病肾病数据集避坑:5 个会让人半夜改代码的常见问题

5.1 EXIF 旋转把框带偏:坐标转换后目标全部偏离

现象:用 OpenCV 读取 jpg 训练,目标框整体偏移,目标中心普遍落在框外。

原因:医学影像设备拍出来的 jpg 常带 EXIF Orientation 信息,OpenCV 的imread默认不解析 EXIF,读出来的图像被旋转了 90 度或 180 度;而 xml 里的 width/height 是按原始方向记录的,转换出的 YOLO 归一化框基于错误坐标系。

解决:转换前对所有图片做一次ImageOps.exif_transpose,把像素方向固化后再重写 xml 的宽高,再执行 VOC 转 YOLO。不做这步,后面所有指标都是假的,而且很难在指标层面直接看出来。

from PIL import Image, ImageOps import os for p in os.listdir('images'): if not p.endswith('.jpg'): continue im = Image.open(os.path.join('images', p)) im = ImageOps.exif_transpose(im) im.save(os.path.join('images_fixed', p))

逻辑说明:exif_transpose会根据 EXIF 里的 Orientation 字段自动旋转图像并清除该字段;旋转后原图的宽高可能互换,所以 xml 里的<width>和<height>也要同步更新,再重新生成 YOLO 标签。这一步必须在任何转换之前做。

5.2 目标尺度与默认 anchor 不匹配:肾小球只有几十像素

现象:训练过程中 bbox_loss 高,验证集 mAP 偏低,分类概率看着正常,输出的框要么过大要么抖动。

原因:YOLO 默认 anchor 尺寸是在 COCO 常见物体上统计出来的,肾小球、肾小管这类小目标在 640 分辨率下可能只有 30-50 像素,很容易落在 stride 32 那一层的特征图上,空间信息丢失严重。

解决:训练前打开 autoanchor,确认它重新计算出的 anchor 尺寸;如果目标仍然太小,就把原图切成 512 或 640 的 patch 再做检测,每张 patch 单独出框,推理时再合并回原图坐标。tiling 的切分重叠率一般取 10%-20%,避免目标恰好被切在边界上。

5.3 五类样本量悬殊,mAP 参考意义有限

现象:整体 mAP 上了 0.8,但拆开看只有两类高,少数病变类别 AP 不到 0.3。

原因:糖尿病肾病数据集里正常结构多、病变成分少,默认分类损失在多数类上权重过高,少数类的梯度被淹没。

解决:先统计每类 bbox 数量,对不平衡幅度大的类加 class weights;YOLO 的默认分类损失可以换成带 focal 性质的版本,或者对少数类额外做 copy-paste 增强。评估时不要只看整体 mAP,要看少数类的 AP 是否被多数类拉高。

5.4 相似样本在 train/val 里重复出现,验证集虚高

现象:训练时 loss 降得顺利,验证集 mAP 极高,一到新切片上就崩。

原因:整理数据集时可能把同一张大图的不同裁剪放进 train 和 val,也可能一张原图被切块后分散到两个集合里,模型在验证时直接认出了几乎相同的图。

解决:对全量图片算 perceptual hash,筛出哈希重复的样本,保证 train 和 val 之间没有任何近重复图片。这一步虽然耗时,但比日后在部署现场怀疑模型要快得多。

5.5 标签越界与浮点精度:width 为 0、x_center 超过 1

现象:某个 epoch 训练中断,报错指向一个 txt;打开发现x_center是 1.0000001,或width是 0。

原因:xml 里目标贴着图片边缘,bndbox等于图片边界;或标注工具多标了一个 0 宽度的点;转换脚本没做 clip。

解决:转换时对所有值执行max(0, min(1, value)),并跳过x2 <= x1或y2 <= y1的框。

xc = max(0.0, min(1.0, ((x1 + x2) / 2) / w)) yc = max(0.0, min(1.0, ((y1 + y2) / 2) / h)) bw = max(0.0, min(1.0, (x2 - x1) / w)) bh = max(0.0, min(1.0, (y2 - y1) / h)) if bw <= 0 or bh <= 0: continue

逻辑说明:clip 保证归一化数值落在 0-1 区间,YOLO 训练时不会因为越界触发断言;跳过宽高为 0 的框是为了不让 dataloader 在数据加载阶段抛异常。转换完后再加一行检查,加载全部 txt,确认每行的 5 个字段都能转成 float 且在 0-1 区间。这行检查能省下一个通宵的排查时间。

6. 用验证集出混淆矩阵再谈效果:逐类 AP 与推理阶段置信度调整

6.1 固定置信度阈值跑一遍验证脚本,保留一份可复现的结果

训练结束后,先跑一遍验证脚本拿到混淆矩阵和逐类 AP,这时候看到的数字才代表这个糖尿病肾病数据集到底训成了什么样。建议固定 conf 阈值先用 0.25,取 PR 曲线的平衡点后,再单独看五类中每一类的 AP50 和 AP75。医学场景下漏检和误检代价不同:倾向不漏检,conf 降到 0.1 再看 AP 和推理耗时;倾向少误报,conf 提到 0.4 再看。T4 上用 TensorRT 把 640 分辨率 yolo 加速后做这类推理,单路帧率会明显提升,但这是推理侧的事,训练侧先把验证集口径定齐。

6.2 逐类混淆矩阵协助判断形态特征识别能力

常被忽略的一步是在验证集上画出逐类混淆矩阵,重点看两两混淆的类到底错在哪。糖尿病肾病病理图像里纤维化和萎缩形态接近,模型容易互相串,这类错误靠调 anchor 完全没用,要看模型学到的是形状还是纹理。后处理可以做两步:一是用 test-time augmentation,推理时把图像翻转/缩放后合并预测;二是对模型输出的坐标加一个基于目标尺度的 NMS 阈值调节,小目标之间重叠多,NMS 阈值要适当调低。这两步不改训练,直接作用于最终预测精度。

6.3 换 loss 改 head 之前,先在同一验证脚本下做基线对比

我自己的习惯:每个数据集版本只保留一个验证结果文件,里面存混淆矩阵、每类 AP、训练曲线图,版本间对比才有意义。换 backbone、换 efficient head 这类改进,或改损失函数、加 EMA 平滑时,先在同一验证集上跑同一套评估代码,再谈论哪个修改更好。

最后说一句实在话,糖尿病肾病这类医疗影像数据集能凑到 4122 张双格式标注已经很奢侈,把数据校验和验证口径做扎实,比多调十个参数更值。希望帮到你。

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

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

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

立即咨询