ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

RSNA肺炎检测数据集:VOC与YOLO双格式解析及训练前避坑指南

RSNA肺炎检测数据集:VOC与YOLO双格式解析及训练前避坑指南 简介RSNA肺炎检测数据集面向医学影像目标检测方向的学习者与开发者围绕RSNA胸部影像场景整理数据规模为6012张图片、单一类别meat、9555个目标框适合直接用于YOLO、Faster R-CNN等主流检测框架的训练与评估。数据提供Pascal VOC与YOLO两种格式的标注描述便于在主流检测框架间直接复用所有标注由labelImg按矩形框完成规则统一。资源包采用7z压缩大小约886.78MB文件总数为2000主要包含1999个VOC格式XML标注文件与1个TXT使用说明文件可配套6012张影像完成目标检测实验压缩包内文件以标注为主体便于使用者快速了解数据构成并按需划分训练集和验证集。目前已有594人学习浏览适合有一定深度学习基础、需要标准医学影像目标检测数据集的开发者快速上手。需注意数据集仅保证标注准确、格式合理不对训练所得模型或权重文件精度作任何承诺。1. RSNA肺炎检测数据集VOCYOLO双格式、6012张图、9555个框拿来就能直接用如果你在网上搜“RSNA肺炎检测数据集”大概率会看到一堆涉及DICOM和肺结节检测的原始CT资料但真正拿到就能训练的往往不是原始影像。我这次拆的这份资源是一个已经被预处理并标注好的数据集6012张JPG图片配套6012个Pascal VOC XML和6012个YOLO TXT标注文件总共9555个矩形框类别只有1个——标注工具里写的是meat。说实话第一眼看到类名我也愣了一下但核对了图片和框以后发现它是RSNA肺炎病灶/实变区域只是原作者在labelImg里沿用了一个预设标签名。这份资源适合谁适合想跳过标注、直接验证YOLO各版本效果的人也适合做肺炎检测入门、迁移学习的工程人员。接下来我会从文件结构讲到转换脚本、训练前的坑。2. VOC与YOLO双格式的底细目录结构、XML字段和归一化坐标2.1 目录结构同名文件和三种后缀的对应关系一打开压缩包你会看到这是一个大平铺目录没有常见的images/、labels/、Annotations/三层结构。所有文件都在一个根目录下文件名统一是firc_rsna_带编号比如firc_rsna_1010.jpg、firc_rsna_1010.xml、firc_rsna_1010.txt。文件名的数字其实就是原RSNA数据集的某个编号用途不大关键在于三者后缀一一对应。文件类型数量作用.jpg6012训练图片RGB通道尺寸不一.xml6012Pascal VOC格式标注含绝对坐标.txt6012YOLO格式标注含归一化坐标另外根目录还有一个使用前必读.txt里面通常写明格式说明、标注工具和免责声明。注意YOLO txt文件里只有类别和坐标没有图片路径。也就是说它“不包含分割路径的txt文件”不像有些数据集会在每行开头写images/img_0001.jpg这里就是纯数字。这意味着你要自行决定怎么给YOLO训练组织目录。如果你用ls看数量会发现jpg 6012、xml 6012、txt 6012完全一致说明这张数据集里每一张图都标了框没有纯背景图。总框数9555类别唯一meat折合平均每张图有1.59个框。很多RSNA病灶不止一个实变区这很合理。2.2 VOC XML从filename到bndbox的字段逐项解读随便挑一个xml打开比如firc_rsna_1010.xml内容长这样annotation folderRSNA/folder filenamefirc_rsna_1010.jpg/filename pathD:/datasets/rsna/firc_rsna_1010.jpg/path source databaseUnknown/database /source size width1024/width height1024/height depth3/depth /size segmented0/segmented object namemeat/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin210/xmin ymin95/ymin xmax486/xmax ymax355/ymax /bndbox /object /annotation上面是针对结构做的示例展示实际数值以压缩包解出来的为准。这个格式就是labelImg默认保存的Pascal VOC格式。几个关键字段filename图片文件名必须和实际jpg完全一致训练脚本会拿它拼接图片路径。size宽高和通道数YOLO归一化时要用它。object每个目标一个块。name是类别名这里全为meatdifficult为1时可忽略这里基本都是0。bndbox矩形框的左上角和右下角绝对像素坐标。注意xmax、ymax是包含在框内的计算宽高时一般用xmax - xmin不需要加1OpenCV画框时也一样。这份数据里一个xml可能包含多个object因为同一张图有多个病灶区域时需要画多个框。读取时要把每一个都解析出来不能只看第一个。2.3 YOLO TXT归一化坐标的计算与还原再打开对应的firc_rsna_1010.txt内容类似0 0.3398 0.2197 0.2695 0.2539这是按上面xml的框换算出来的。五个数字分别代表类别ID、归一化后的中心点x、中心点y、框宽度w、框高度h。类别ID从0开始所以0对应meat后续如果有多个类别就是0、1、2……。归一化的公式很简单x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height用上面VOC示例数据算一次图片宽高1024×1024框210,95,486,355那么x_center就是0.3398y_center是0.2197宽高分别是0.2695和0.2539正好对应txt里的内容。当你用YOLO训练时模型读入的就是这些归一化数字。还原成绝对坐标只需要乘回去xmin int((x_center - box_width / 2) * width) ymin int((y_center - box_height / 2) * height) xmax int((x_center box_width / 2) * width) ymax int((y_center box_height / 2) * height)注意这里存在精度损失txt只保留四位小数画框验证时会有1~2像素误差属于正常。如果保留6位小数误差更小。这份数据集的txt应该是保留了一定精度但还是建议验证时直接用xml还原绝对坐标最稳。很多老训练脚本只吃VOC新框架往往只要YOLO。双格式意味着你不需要再转换但由此带来的一个坑就是如果两份标注出现不一致你以哪份为准后文会讲到。2.4 用bash快速核对文件数量和类别分布解压后第一步我建议先用命令行做个快速体检确认文件数没少ls *.jpg | wc -l ls *.xml | wc -l ls *.txt | wc -l三条命令分别输出jpg、xml、txt的数量正常都应该是6012。如果某个数字偏少说明解压不完整后面所有训练流程都要停下来。接着可以统计一下类别标签是否有异常grep -h name *.xml | sort | uniq -c正常会看到只有一行6012 namemeat/name注意因为一个xml可能有多个object总数会是9555但名称只会是meat。如果出现别的类名说明标注时混入了其他类别需要用后文的class_map统一清洗。这种极简检查成本不到十秒能避免后面带着错误标注跑一整天。3. 从VOC到YOLO再划分训练集转换脚本、随机种子与边界排查3.1 既然给了双格式为什么还要自己转换你可能会想“txt都有了不用转换直接开训。” 实际工程里这不够。这三个场景很常见想把类别meat改成pneumonia或中文名需要同时改xml和txt。只改txt很简单但改完两边不一致后面验证会互相打架。想按自己的比例划分train/val/test而且想让每个类别在划分后比例不过度倾斜。想重新生成YOLO txt避免原始txt因xml尺寸记录错误而整体偏移。所以我把转换脚本当作一个“清洗动作”而不是先去转换。常见做法是读取xml解析出标准VOC结构再重新生成txt。这样能保证两份标注的源头是同一个。3.2 Python脚本XML解析、坐标归一化与TXT生成下面是一个单文件转换脚本输入一个xml路径输出对应的yolo txt。我在多个数据集上用过类似结构。import xml.etree.ElementTree as ET import os def voc_xml_to_yolo_txt(xml_path, txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() # 取图片真实尺寸 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue # 跳过未映射的类别 class_id class_map[name] 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 / width y_center (ymin ymax) / 2 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height # 钳制到[0,1]避免标出图外 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) box_w min(box_w, 1 - x_center) box_h min(box_h, 1 - y_center) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: class_map {meat: 0} # 只有一个类别ID为0 voc_xml_to_yolo_txt(firc_rsna_1010.xml, firc_rsna_1010.txt, class_map)逻辑说明用ElementTree快速解析XML不依赖第三方库。先拿到size算归一化基准再遍历每个object把name映射成类别ID。计算中心点和宽高时全部用float参与避免整数除法的精度损失。最后三行是边界保护用于处理标注框超界的情况。这种超界在这一类数据集里偶尔会出现生成后最好再反向验证一遍。参数说明class_map决定了类别顺序如果将来换成多类别把字典按自己的规则填即可生成txt时第一列会自然从0递增。文本保留6位小数比常见的4位更稳虽然实际差异不大。我用min(max(...))作钳制时要注意一个极端如果原框中心点越界太夸张钳制后框会变成另一个位置所以最好在钳制之前先打印警告而不是静默吞掉。更稳妥的做法是先判断xmin 0 and ymin 0 and xmax width and ymax height不满足就跳过兼顾后续统计。很多刚接触YOLO的人收到整数坐标就会怀疑“位置怎么变了”其实没有只是被归一化了。这个脚本跑完以后生成的txt与原始txt在数值上应该高度吻合只存在进位误差。3.3 划分数据train/val/test 比例与稳定随机种子有了清洗后的标注文件下一步要把数据集分到images/train、images/val、images/test以及对应的labels/目录。YOLO官方Ultralytics等框架通常要求图片和标签放在平行目录里。常见做法是先收集所有jpg按文件名stem找对应txt然后随机打乱。import os import random import shutil src_root ./rsna_dataset # 解压后的根目录 dst_root ./yolo_dataset # 输出目录 split_ratio {train: 0.8, val: 0.1, test: 0.1} random.seed(42) # 找出所有jpg文件确保都有对应的xml和txt jpegs [f for f in os.listdir(src_root) if f.endswith(.jpg)] valid [] for jpg in jpegs: stem os.path.splitext(jpg)[0] xml_path os.path.join(src_root, stem .xml) txt_path os.path.join(src_root, stem .txt) if os.path.exists(xml_path) and os.path.exists(txt_path): valid.append(stem) random.shuffle(valid) # 计算边界 n len(valid) n_train int(n * split_ratio[train]) n_val int(n * split_ratio[val]) for split_name, stems in [ (train, valid[:n_train]), (val, valid[n_train:n_train n_val]), (test, valid[n_train n_val:]) ]: img_out os.path.join(dst_root, images, split_name) lbl_out os.path.join(dst_root, labels, split_name) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for stem in stems: shutil.copy(os.path.join(src_root, stem .jpg), os.path.join(img_out, stem .jpg)) shutil.copy(os.path.join(src_root, stem .txt), os.path.join(lbl_out, stem .txt))逻辑说明先做一次“双保险”校验只有jpg、xml、txt三者齐全才进入划分。random.seed(42)固定了伪随机顺序保证每次执行得到相同划分这是训练可复现的基础。注意我用的是复制而不是移动这样原压缩包解出来的原始数据还保留着出了错有后悔药。如果你磁盘紧张用os.replace改成移动也可以但建议先在副本上测试。边界处的n_train n_val是val的结束下标最后一个valid[n_trainn_val:]就是剩下的test由于比例都是0.1几乎不会丢样本。参数说明split_ratio可以按数据集规模调整。6012张图0.8/0.1/0.1会得到4809/601/602左右val/test略少对单类任务已经够用。如果要做K折实验把这里改成循环种子换成不同值即可。注意random.seed要放在shuffle前面才有效放在后面等于没设置。另外由于这份数据只有一个类别按纯随机划分基本能保证类别均衡。如果换成多类别最好再用sklearn.model_selection.train_test_split做stratify。这里就不展开了。3.4 为YOLO训练准备config.yaml划分完目录训练前还需要一个yaml配置。Ultralytics的YOLO训练会读取这个文件定义数据路径、类别数量和类别名。path: ./yolo_dataset train: images/train val: images/val test: images/test nc: 1 names: [meat]逻辑说明path填上面脚本生成的dst_roottrain、val、test填相对path的图片目录。yolo会自动在同一级目录下找同名的labels目录所以images/train旁边的labels/train不需要额外配置。nc必须是1names长度与nc一致。如果你把类别名改成了pneumonia这里就要同步写[pneumonia]否则训练时的类别ID和显示名会错位。这样做的意义是让标注和训练完全解耦即使txt里第一列是0在模型意义下它依然是meat或你定义的第一个类别。4. 避坑手册从解压到训练前的五个常见问题4.1 文件名对不上导致训练直接报错现象用YOLO框架训练时日志里出现大量image not found或label not found检查后发现有些jpg没有同名txt有些txt没有同名jpg。原因原始压缩包内文件名前缀是firc_rsna_下载工具或解压软件有时会把超长文件名截断或Windows资源管理器对特殊字符处理导致改名。也有用户自己用脚本重命名图片但没有同步重命名标签。解决解压后第一时间跑一个校验脚本用stem把三个后缀配对缺少任何一个就单独挑出来。合理做法是把异常的挑出来但如果你只是想快速开训直接过滤掉这些文件即可。import os bad [] for f in os.listdir(.): if f.endswith(.jpg): stem os.path.splitext(f)[0] if not (os.path.exists(stem.xml) and os.path.exists(stem.txt)): bad.append(stem) print(异常文件数:, len(bad)) print(bad[:10])这个脚本会列出缺配对的stem。我遇到过一次解压中途报“文件已存在”的提示原因是我之前解压过一部分到同一目录后缀没覆盖完全最终导致配对混乱。从那以后我都先删干净再解压。4.2 类别名“meat”让人误以为标注错误现象用labelImg打开VOC标注看到类别名是meat再去看RSNA原始定义觉得牛头不对马嘴怀疑数据集作者标错了。原因meat是作者在labelImg预设类别里顺手填的一个标签名不是其真实语义。RSNA肺炎数据集里需要检测的目标通常是肺部实变/毛玻璃影区域所以框都框在肺部。只要框的位置在肺部区域就没有问题。解决不需要改数据在训练配置里把names写成[meat]即可或者用第3章的class_map批量改名。我一般更建议改成pneumonia或lung_opacity方便以后和别人协作但改完注意txt也要同步改不要只改一半。4.3 xml尺寸与图片真实尺寸不一致造成框偏移现象用xml里的绝对坐标画框框的位置整体偏到一侧或大小不对。原因xml里的size是标注工具当时读到的尺寸如果作者在修改图片缩放后没有重新读取xml保留的还是旧宽高。另一种情况是某个大目录下混入了不同尺寸的图片但xml套用了统一模板。解决统一以真实图片为准。用PIL读取img.shape打印出和size不一致的样本。重新计算归一化坐标时不要直接用xml里的宽高而是用真实宽高。注意如果你的图片统一被缩放到固定尺寸那么必须先把图片缩放到相同大小再生成txt或者干脆让训练时保留原始尺寸。import os import cv2 import xml.etree.ElementTree as ET for f in os.listdir(.): if not f.endswith(.xml): continue stem os.path.splitext(f)[0] img cv2.imread(stem .jpg) if img is None: continue real_h, real_w img.shape[:2] root ET.parse(f).getroot() xml_w int(root.find(size/width).text) xml_h int(root.find(size/height).text) if (real_w, real_h) ! (xml_w, xml_h): print(尺寸不一致:, stem, 真实, (real_w, real_h), xml, (xml_w, xml_h))运行后会列出所有可疑文件。如果数量很少可以直接用人工修正如果数量过半就要回到转换脚本按真实尺寸重新算。4.4 7z解压大文件时的数据完整性问题现象解压到一半报Unexpected end of data或者解压完数量统计少了几百个文件。原因压缩包下载不完整或者网盘下载工具没有正确保留7z格式。7z大文件损坏很常见这篇正好是.7z格式。解决下载后先校验大小用7-Zip的7z t命令测试完整性7z t RSNA肺炎检测数据集VOCYOLO格式6012张1类别.7z如果有任何CRC错误基本上只能重新下载不要指望修复。解压时优先用7-Zip而不是Windows自带的“压缩文件夹”因为后者对7z支持有限容易静默解压出空目录。另外下载文件名如果被浏览器改成了(1).7z先改回原名再测试避免工具识别格式错误。4.5 只有一个类别时txt第一列的0带来的歧义现象训练配置里nc1但日志有时输出class 0 has no labels或者验证时mAP一直为0。原因很多人把class_id0当成背景ID而在YOLO里背景不在标注行里0就是第一个类别。如果误把nc设成2或者把names写成了[background, meat]模型会认为有两类但数据里只有0导致1类标签为空。解决把nc保持为1names只要一个元素即可。如果你真要加背景类那么数据里的0要改成1同时把所有框的类别ID重新映射。合理的习惯是看到txt里第一列全是0就说明只有一个类别这是单类数据集正常现象不要乱改。5. 训练前花两分钟遍历核查一个画框与统计的小脚本无论转换脚本写得多严谨训练前最好还是用朴素方法验证一遍。我习惯的做法是随机抽10张图片把xml里的绝对坐标画到jpg上另存到checks/目录同时统计每张图的框数和面积分布。这样能一眼看出标注是否偏移、是否有明显错标。5.1 快速核查脚本10张图看全局import os import cv2 import numpy as np import xml.etree.ElementTree as ET src_root ./rsna_dataset checks_dir ./checks os.makedirs(checks_dir, exist_okTrue) for idx, f in enumerate(sorted(os.listdir(src_root))): if not f.endswith(.xml): continue if idx 10: break xml_path os.path.join(src_root, f) stem os.path.splitext(f)[0] img_path os.path.join(src_root, stem .jpg) root ET.parse(xml_path).getroot() img cv2.imread(img_path) if img is None: print(图片读取失败:, img_path) continue h, w img.shape[:2] for obj in root.iter(object): bb obj.find(bndbox) xmin int(float(bb.find(xmin).text)) ymin int(float(bb.find(ymin).text)) xmax int(float(bb.find(xmax).text)) ymax int(float(bb.find(ymax).text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, meat, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) out_path os.path.join(checks_dir, stem _check.jpg) cv2.imwrite(out_path, img) print(已保存核查图片:, out_path)逻辑说明通过cv2读取真实图片用xml里的绝对坐标画红色矩形框。这里要注意cv2.rectangle的坐标顺序是左上、右下正好对应VOC的bndbox。如果框在图中能贴合病灶区的边缘说明标注基本可信如果框跑到图外或者明显错位就要回到错误目录重新生成标注。这个脚本只抽查前10个xml便于快速人工看。如果你想全量生成把idx 10的循环条件去掉即可但6012张图全部输出会比较占空间。参数说明颜色(0,0,255)是红色文字标签可以改成你最后使用的类别名。如果图片较大画了框以后保存的jpeg体积会增加检查完没发现问题就删掉整个目录不用保留。注意中文路径下cv2.imwrite可能报错输出文件名建议用纯英文stem。这个方法投入的时间不超过三分钟但能扼杀掉绝大部分“标签格式没问题但训练崩了”的玄学问题。最后一次用这个数据集时我抽查到第7张图发现xml里的xmin变成0追溯原因仍然是前一份xml尺寸错误。从那以后我每次下载别人标注数据集都会先强制走一遍这个遍历核查流程确认框形和统计都正常后才交给训练脚本。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表