简介: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肺炎检测数据集:VOC+YOLO双格式、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数据集的某个编号,用途不大,关键在于三者后缀一一对应。
| 文件类型 | 数量 | 作用 |
|---|---|---|
.jpg | 6012 | 训练图片,RGB通道,尺寸不一 |
.xml | 6012 | Pascal VOC格式标注,含绝对坐标 |
.txt | 6012 | YOLO格式标注,含归一化坐标 |
另外根目录还有一个使用前必读.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> <folder>RSNA</folder> <filename>firc_rsna_1010.jpg</filename> <path>D:/datasets/rsna/firc_rsna_1010.jpg</path> <source> <database>Unknown</database> </source> <size> <width>1024</width> <height>1024</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>meat</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>210</xmin> <ymin>95</ymin> <xmax>486</xmax> <ymax>355</ymax> </bndbox> </object> </annotation>上面是针对结构做的示例展示,实际数值以压缩包解出来的为准。这个格式就是labelImg默认保存的Pascal VOC格式。几个关键字段:
<filename>:图片文件名,必须和实际jpg完全一致,训练脚本会拿它拼接图片路径。<size>:宽高和通道数,YOLO归一化时要用它。<object>:每个目标一个块。<name>是类别名,这里全为meat;<difficult>为1时可忽略,这里基本都是0。<bndbox>:矩形框的左上角和右下角绝对像素坐标。注意xmax、ymax是包含在框内的,计算宽高时一般用xmax - xmin,不需要加1,OpenCV画框时也一样。
这份数据里一个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.3398,y_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 <name>meat</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', encoding='utf-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_ok=True) os.makedirs(lbl_out, exist_ok=True) 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_train+n_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_root;train、val、test填相对path的图片目录。yolo会自动在同一级目录下找同名的labels目录,所以images/train旁边的labels/train不需要额外配置。nc必须是1,names长度与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肺炎检测数据集VOC+YOLO格式6012张1类别.7z如果有任何CRC错误,基本上只能重新下载,不要指望修复。解压时优先用7-Zip而不是Windows自带的“压缩文件夹”,因为后者对7z支持有限,容易静默解压出空目录。另外下载文件名如果被浏览器改成了(1).7z,先改回原名再测试,避免工具识别格式错误。
4.5 只有一个类别时,txt第一列的0带来的歧义
现象:训练配置里nc=1,但日志有时输出class 0 has no labels,或者验证时mAP一直为0。
原因:很多人把class_id=0当成背景ID,而在YOLO里背景不在标注行里,0就是第一个类别。如果误把nc设成2,或者把names写成了['background', 'meat'],模型会认为有两类,但数据里只有0,导致1类标签为空。
解决:把nc保持为1,names只要一个元素即可。如果你真要加背景类,那么数据里的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_ok=True) 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尺寸错误。从那以后我每次下载别人标注数据集,都会先强制走一遍这个遍历核查流程,确认框形和统计都正常后才交给训练脚本。希望帮到你。
本文还有配套的精品资源,点击获取