简介:该数据集围绕RSNA肺炎检测场景构建,提供VOC与YOLO两种流行的目标检测标注格式,适配主流训练框架,方便算法工程师、科研人员以及医学影像智能分析方向的学习者使用,也可迁移至其他单类别目标检测任务。数据规模包含六千零一十二张图像对应的标注信息,每张图像均配有XML和TXT标注文件,类别名称为meat,共标注出九千五百五十五个矩形框;标注工具为LabelImg,绘制规则仅针对目标画框,标注结果准确合理,但作者特别声明不对训练后的模型精度提供任何保证,因此实际使用时应独立验证模型效果。压缩包内文件总数为两千个,其中一千九百九十九个为XML标注文件,另外一个是使用说明文档,整体体积约八百八十六点七八兆字节,且不包含分割路径信息。目前已有五百九十四人学习下载。获取之后可直接用于YOLO、Faster R-CNN等常见检测框架的训练与验证,免除自行转换格式的步骤;还可按照VOC或YOLO目录规范配置路径,快速展开肺炎检测实验,结合数据规模与标注类型进行数据增强、预训练和模型评测等后续研究。
1. RSNA肺炎检测数据集:VOC+YOLO双格式到底解决了什么问题
做过目标检测的人最怕的不是模型难调,而是数据格式难缠。RSNA肺炎检测数据集来自北美放射学会(RSNA)的公开挑战赛,原始素材是DICOM胸片,标注埋在CSV里,坐标、灰度、尺寸都不规整,直接拿来喂给YOLO会当场劝退。这个7z包已经把数据整理成VOC(XML)和YOLO(txt)双份格式,共6012张、1个类别,解压之后可以直接进训练流水线。适合正在做肺结节或肺炎检测的从业者,也适合想用医学影像入门YOLO目标检测的人;你不需要再写一轮格式转换脚本,省下的时间足够你把训练参数多调几轮。
2. 把7z压缩包安全落盘:Linux与Windows两条解压路径
2.1 为什么用7z:压缩率、固实包与磁盘预判
这个包把VOC和YOLO两份数据放进了同一个7z文件,而不是分开两个zip,是有现实考虑的。JPG、PNG这类图片已经做过一次有损或无损压缩,zip对它们的再压缩能力很弱,但7z的LZMA使用固实压缩,把整个包当成一个连续数据块处理,能利用图片之间的横向相关性再挤出10%~20%的体积;更重要的是只有一个文件,分发、校验、归档都省事。
拿到包的第一步,不要急着解压,先在命令行下用7z l看包内清单。这样你能摸清几件事:包内是不是VOC和YOLO并列的目录结构、JPEGImages里是jpg还是png、有没有单独放ImageSets。这些信息直接决定你后面的数据加载代码怎么写。另外,先看下磁盘剩余空间够不够——解压后的体积通常是压缩包体积的2~3倍,RSNA这类胸片数据集尤其明显。
2.2 Linux下解压7z:p7zip-full与7z x的精确用法
常见做法是装p7zip-full,Debian/Ubuntu一行搞定。如果你在CentOS/RHEL系,对应包名是p7zip,用yum install p7zip。装完确认7z命令存在再操作。
# 查看包内目录结构,避免盲解压 7z l RSNA肺炎检测数据集VOC+YOLO格式6012张1类别.7z # 完整解压到指定目录,-o后面不要带空格 7z x RSNA肺炎检测数据集VOC+YOLO格式6012张1类别.7z -o/data/rsna/只要7z l能看到VOC/和YOLO/两个顶层目录,解压出来就不会乱。注意7z x会保留包内目录结构,7z e则会把所有文件平铺到同一个目录下——很多人在这步翻车,解压完6000多张图全堆在一层,XML和JPG混在一起没法用。这个包务必用7z x。
如果只需要YOLO格式,完全可以用通配符只解压一部分,省一半磁盘:
7z x RSNA肺炎检测数据集VOC+YOLO格式6012张1类别.7z -o/data/rsna_yolo/ "YOLO/*"通配符参数跟在包名后面,路径要从包内根目录写起。我在生产环境还会加-bsp0把进度信息关掉,配合nohup扔后台,避免终端被刷屏。
2.3 Windows下解压7z:7-Zip与侧边栏文件树的取舍
Windows下最省事的方案是装7-Zip,然后右键解压。但对于只想确认包内结构的场景,直接双击7z文件,7-Zip会在界面上把包内目录展开成文件树,不用解压就能预览VOC和YOLO各目录里的文件。这个习惯能帮你提前发现是不是只有图片没有标注、ImageSets里train和val是否齐全,等于一个免费的后悔药。
真正要解压时,选中需要的目录再点“解压”按钮,7-Zip只会解出选中的部分。需要全部解压就右键整个压缩包,选择“7-Zip -> 解压到 当前文件夹”。解压过程中不要直接拔电源或强杀进程,7z写文件是顺序进行的,被中断后虽然能重新解压,但之前的半截文件会造成空间浪费。
有一点容易忽略:如果文件名里有中文,Windows打包后到Linux下可能出现编码错乱。这个包里顶层目录是VOC和YOLO这类ASCII名称,一般不受影响;但如果发现解压出来的文件名带乱码,用convmv或7z的编码参数重命名/重新解压一次就能解决。
2.4 解压后的三件事:数图片、数标注、看尺寸分布
解压完先校验,不要直接开训练。第一步数文件数量,图片数和标注数必须一致。第二步看图片模式和尺寸,这决定后面数据加载写不写额外处理。第三步是抽样目检,用脚本随机挑几张图,把XML里的框画出来,确认框不是乱框。
find /data/rsna/VOC/JPEGImages -type f | wc -l find /data/rsna/VOC/Annotations -type f | wc -l两条命令的结果应该一致。如果不一致,优先怀疑是解压时跳过同名文件,回压缩包里核对一下。接着用Python看部分图片的尺寸和模式:
from PIL import Image import glob for img in sorted(glob.glob("/data/rsna/VOC/JPEGImages/*.jpg"))[:10]: with Image.open(img) as im: print(img.split("/")[-1], im.size, im.mode, im.format)输出里看到L模式的灰度图,先记下来,第5章会讲它怎么坑你。看到尺寸参差不齐也不用慌,ultralytics在训练时会自动letterbox;但如果你要自己写Dataset类,这一步就躲不掉了。
3. VOC和YOLO两种标注格式:转换脚本与四个边界坑
3.1 Pascal VOC的目录约定与XML里的坐标系
VOC格式有个显著特点:它不靠配置文件,而是靠目录结构说话。JPEGImages里放图,Annotations里放同名XML,ImageSets/Main里放train.txt、val.txt这些划分文件。拿到别人整理的VOC包,第一件事就是确认这三个目录都在;缺Annotations就白搭,缺ImageSets其实还能补救,自己按比例划分就行。
XML里每个目标对应一个<object>节点,name是类别名,bndbox里是xmin、ymin、xmax、ymax的绝对像素坐标。这个直观,适合人看,但YOLO训练不认这种坐标,所以双格式包的意义就是把麻烦提前解决掉。这里有个细节:XML里的<size>字段记载的是图片宽高,但它只是一份“当时的记录”。图片被resize过、被格式转换过程丢掉过原始信息的情况在RSNA数据里不少,所以转换时永远以JPEGImages里真实图片的宽高为准,不要信XML里的size。
3.2 归一化坐标的计算法与class id的处理
YOLO格式的核心是“归一化到0~1的相对坐标”。每一行txt对应一个目标:class_id x_center y_center width height。x_center和y_center是框中心的相对坐标,width和height是框宽高相对图片的比例。而class id是整数,映射关系由data.yaml里的names定义。
这个包只有1个类别(pneumonia),class_id固定为0。很多人拿到1类数据集会忽略类别字典,一旦哪天往包里加了第二个类别,所有训练代码都要返工。我一般会从一开始就维护一个name->id字典,哪怕只有1个类别,也保持同样的代码路径。
3.3 一份可以直接跑的VOC转YOLO脚本
import glob import os import xml.etree.ElementTree as ET from PIL import Image xml_dir = "/data/rsna/VOC/Annotations" img_dir = "/data/rsna/VOC/JPEGImages" out_dir = "/data/rsna/YOLO/labels" class_map = {"pneumonia": 0} os.makedirs(out_dir, exist_ok=True) for xml_path in sorted(glob.glob(os.path.join(xml_dir, "*.xml"))): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = os.path.join(img_dir, img_name) with Image.open(img_path) as im: img_w, img_h = im.size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in class_map: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h if w <= 0 or h <= 0: continue lines.append(f"{class_map[cls_name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") txt_path = os.path.join(out_dir, os.path.splitext(img_name)[0] + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines))这段脚本里有两个地方特意写成这样:一是用Image.open读真实宽高,而不是用XML里的size;二是float()而不是int()去解析坐标。RSNA里有些XML坐标带小数,用int会截断,标注框会偏移一点点,累积起来就是mAP下跌的元凶。归一化坐标保留6位小数足够,再多不会带来精度提升。脚本跑完后,用下面这段代码做单样本正确性校验:把txt里的归一化坐标换算回绝对值,画在原图上,直接目测框位。
from PIL import Image, ImageDraw img_path = "/data/rsna/YOLO/images/0001.jpg" txt_path = "/data/rsna/YOLO/labels/0001.txt" with Image.open(img_path) as im: w, h = im.size draw = ImageDraw.Draw(im) for line in open(txt_path): 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) draw.rectangle([x1, y1, x2, y2], outline="red", width=4) im.save("/tmp/check_0001.jpg")这步花不了1分钟,能拦下至少一半的“训练效果不好”的玄学问题。
3.4 转换过程中最常踩的四个边界坑
坑一说的是多目标只取第一个。RSNA胸片上肺炎病灶可能不止一块,XML里会有多个<object>节点。如果转换脚本只取了obj.find("object")(返回第一个节点)而不是findall,后面几个框就凭空消失了。现象是训练时正样本数急剧减少、模型recall低;原因是多数入门教程只演示单目标XML;解决是统一用findall遍历。
坑二是坐标小数被int截断。现象是标注框整体往左上偏移;原因是白用了int();解决见3.3的脚本,用float。
坑三是文件夹命名与txt文件不同名。RSNA整理包有时图片叫0001.jpg、标注叫0001.txt,但labels目录里可能混有来自其他数据集的同名文件。现象是训练警告image not found或者样本数对不上;解决是解压后先数文件数量一致性。
坑四是ImageSets的train/val划分没转成YOLO路径清单。YOLO训练需要的是train.txt和val.txt两个文件,每个路径一行。而VOC的ImageSets里写的是不带扩展名的名字。不转的话data.yaml配train会找不到文件,训练直接报错。这条对用ultralytics跑的朋友来说几乎必踩。
生成这两份清单的常见做法是:
# 把VOC的ImageSets/Main/train.txt转成YOLO需要的train.txt(图片绝对路径清单) main_dir = "/data/rsna/VOC/ImageSets/Main" with open(f"{main_dir}/train.txt") as f: names = [line.strip() for line in f if line.strip()] with open("/data/rsna/YOLO/train.txt", "w") as out: for name in names: out.write(f"/data/rsna/YOLO/images/{name}.jpg\n")这段代码逻辑不复杂,但要注意拼接的是images目录,不是labels目录;data.yaml里train指定的就是这个txt。
4. 用YOLO在PyCharm里跑通肺炎检测:yaml、损失函数与训练参数
4.1 在PyCharm里从零装好YOLO环境
用PyCharm跑,因为新手调试友好,断点、监视变量都直观。常见做法是新建项目时选好虚拟环境,然后在底部Terminal里装ultralytics。注意不要直接在全局Python里装,避免把系统环境搞坏。
pip install ultralytics装完先不要急着训练,跑两条验证命令:确认torch能看到GPU,确认yolo能加载权重。
import torch from ultralytics import YOLO print(torch.__version__, torch.cuda.is_available()) model = YOLO("yolov8n.pt") # 首次运行会下载预训练权重torch.cuda.is_available()返回False,说明装的torch是CPU版,需要重装CUDA版:
pip uninstall torch torchvision -y # 根据本机CUDA版本选择cu118/cu121/cu124后缀 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121参数说明:cu121表示匹配CUDA 12.1的预编译包,如果本地是CUDA 11.8,把后缀改成cu118或cu124。这步踩的人最多,报错里带CUDA unavailable十有八九是这里的问题。
4.2 data.yaml的两个字段:绝对路径与单类别的names
path: /data/rsna/YOLO train: train.txt val: val.txt names: 0: pneumonia这份yaml最关键的一点:path写绝对路径。PyCharm里运行训练脚本时,当前工作目录往往是项目根目录而不是数据目录,写相对路径会导致ultralytics找不到train.txt。names里0: pneumonia说明这个检测任务只有1个类别。
还有一个容易忽略的细节:train.txt里的图片路径必须和path字段拼接后恰好构成真实路径。如果train.txt里写的是绝对路径,ultralytics会让train.txt里的路径优先于path,所以两种写法不要混用,要么全是绝对路径,要么全是相对路径(相对data.yaml里的path)。
4.3 三个优先调的参数:epochs、batch、imgsz
from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.train( data="/data/rsna/YOLO/data.yaml", epochs=100, batch=16, imgsz=512, device=0, patience=20, )参数说明:epochs在这个数据规模下100轮足够,再多容易过拟合;batch按显存来定,12GB卡用16比较稳,24GB卡可以试32;imgsz=512是因为胸片分辨率高但目标(肺炎区域)相对大,512和640之间的差别不大,512能省显存、提速。patience=20是早停耐心值,val的mAP连续20轮不涨就停。医生标注的边界框主观性较强,val指标波动本来就大,patience设大一点没坏处。
用yolov8n而不是yolov8s起步,是因为这个数据集只有6000多张且只有1类,n的容量够用。想追更好效果再跑s或m,但要在同等参数下对比,避免换模型的同时换了imgsz导致结论失真。
4.4 训练日志里的损失函数:box_loss、cls_loss与波动现象的观察
训练时终端会同步打印box_loss、cls_loss、dfl_loss三个数值。box_loss对应边界框回归损失(包含CIoU与DFL组合),cls_loss是分类损失,dfl_loss是分布聚焦损失。三者之和是总loss,但ultralytics默认分别打印。
对RSNA这类医学影像,前10个epoch里cls_loss下降最快,因为模型先学会区分有炎症和没有;box_loss的下降会慢很多,因为边界框精度依赖特征图分辨率。常见误区是只看总loss——它在3个分量互相抵消时可能显示平稳,掩盖了某一个分量发散的问题。我一般会把每个epoch的三个分量都记录到CSV里,单独画曲线。如果box_loss在后面出现反弹,先查坐标转换;如果cls_loss持续不降,先查类别配置。
另外,医学影像里病变区域往往只占整图很小比例,anchor在特征图网格上大多数位置是负样本,这种背景主导会让loss曲线呈现波动——看着在降,其实模型在输出“没有目标”。这不是代码bug,属于数据本身特性,处理方法放在第5章的样本不均衡条目里。
如果做的是肺结节定位而不是肺炎检测,建议把cls_loss的权重调高一点(ultralytics的train参数里有cls权重)。肺结节更小、更稀疏,分类错误的代价比定位错误更高。
4.5 数据划分与跑训练时的注意事项
训练前先检查train.txt与val.txt是否按病人ID隔离,尤其RSNA一类的医学数据。“同一病人的两张胸片,一张在train、一张在val”是个隐性数据泄露,会让val指标虚高。这个包在整理时基本会按VOC约定的划分走,但换到自己的数据集时要按病人维度做分组划分。
如果是在AGX Orin这类边缘设备上跑同样的训练,建议后续把权重导出为TensorRT,推理速度比FP32翻倍不止;训练阶段这些参数不用动,直接把device指到CUDA核心即可。
5. RSNA数据集专踩的5个坑:灰度图、坐标转换与样本不均衡
医学影像数据集和自然图像数据集有一批不一样的坑。自然图像翻车大多出在网络结构或超参,RSNA这类胸片数据的坑集中在数据本身的物理性质上——灰度、尺寸、标注质量。下面5条是跑这个数据集跑出来的血泪经验,按现象、原因、解决三段写。
5.1 灰度图被当成单通道,训练时直接报错
现象:训练进行到某个epoch时,日志突然抛出类似“Expected input channels to be 3, but got 1”的报错,或者图片在DataLoader加载阶段直接失败,整个训练中断。
原因:RSNA原始胸片是DICOM灰度图,整理成VOC格式时如果只做了格式转换没有做通道复制,JPG里就是单通道灰度。PIL读取为L模式,而YOLO的backbone接受的是RGB三通道输入。
解决:解压后统一检查并转RGB再开训练。
from PIL import Image import glob for img_path in glob.glob("/data/rsna/VOC/JPEGImages/*.jpg"): with Image.open(img_path) as im: if im.mode != "RGB": im.convert("RGB").save(img_path)这段代码跑一遍后,再随机抽几张确认im.mode都是RGB。如果XML里的<size>depth写3但实际是灰度图,转换脚本也要同步修正——这就是第3章强调“以实际图片为准”的由来。
5.2 图片尺寸悬殊,batch训练OOM
现象:训练时一个batch正常,下一个batch直接OOM,换小batch又慢得离谱。
原因:RSNA整理出来的胸片尺寸不统一,有1024x1024的,也有256x256的。YOLO在训练时会按imgsz做letterbox缩放,大图的内存占用明显更高,导致显存波动。
解决:不要在自己代码里统一resize(会破坏bbox坐标),而把缩放交给模型的letterbox。ultralytics在送入batch前会统一缩放,显存峰值基本稳定。如果单卡实在不够,优先降imgsz而不是降batch,因为batch太小时BN层统计不稳,训练效果会明显变差。
5.3 坐标绝对值写死,换图就错位
现象:训练出来的模型在单张测试图上效果不错,换一批图后框整体偏移。
原因:VOC的XML坐标是绝对像素值,图片resize后如果没有同步缩放坐标,框就错位。常见做法是有人为了统一尺寸直接用PIL的resize保存图片,却忘了改XML。
解决:统一用归一化坐标喂给YOLO;如果要resize,先算缩放比例再更新坐标,不要手动改XML。
5.4 阳性样本占比低,recall虚低
现象:训练完后日志里mAP50看着还行,但实际推理时很多真实病灶没检出,recall只有0.4~0.6。
原因:RSNA肺炎检测数据里,很多胸片根本没有病灶,阴性样本占大头。YOLO默认的置信度阈值是0.25,在负样本主导下模型倾向保守输出,mAP和实际漏检率对不上,这是个典型的黑匣子问题。
解决:推理时把conf降到0.05~0.1,再配合NMS。评估还是用val的mAP为准。如果阈值降下来recall有明显提升,说明模型本身没有大问题。另一种手段是训练时开启更强的增强(mosaic、mixup),或者给cls_loss加权重。
5.5 病灶框太小,被YOLO直接过滤
现象:训练后发现val集上一个小病灶都没检出,而大的实变区都能检出。
原因:YOLO有默认的anchor和最小框限制,框宽或高小于某个像素阈值时,会被当成噪声滤掉。RSNA里早期肺炎的病灶可能是几毫米的小斑片影,转成512x512训练图后可能只占十几个像素。
解决:在转换阶段把小目标按原图比例保留,训练时imgsz不要开太小;同时用ultralytics的scale增强参数扩大目标尺寸多样性。如果要正经做小目标检测,后续应该切patch训练或者换成支持小目标的模型结构。
6. 训练完成后的验证:mAP曲线、漏检分析与ONNX导出
6.1 val跑一遍,不要只看日志里的mAP
训练结束后,ultralytics在runs/detect/train/下已经存了验证集的结果图像和metrics。但自己再显式跑一次val更放心:
from ultralytics import YOLO model = YOLO("/data/rsna/runs/detect/train/weights/best.pt") metrics = model.val(data="/data/rsna/YOLO/data.yaml", conf=0.05) print(metrics.box.map50, metrics.box.map)conf=0.05是故意调低的。医学影像负样本多,按默认0.25评估会把一堆低置信度真阳性框过滤掉,map50会偏低。如果压低conf后map50从0.55跳到0.65,说明模型其实学到了特征,只是置信度标定偏保守。
6.2 漏检分析:哪些图翻车
val输出里有每张图的预测结果,把漏检的图挑出来逐张看。我会把漏检图分成三类处理:病灶太小、病灶被肋骨遮挡、病灶对比度极低。前两类靠增强或切patch缓解,第三类多是标注本身的问题——原始标注框画得极松,模型学出来的框反而比标注准。
这个习惯帮我避开了一个坑:第一次跑通时mAP50有0.6,我觉得可以收工了,结果漏检的全是小病灶。后来训练时开了mosaic加小目标增强,才把recall拉上去。
6.3 导出ONNX,脱离PyTorch也能部署
导出这一步很常见,也简单:
model.export(format="onnx", imgsz=512, half=True)参数说明:imgsz要和训练时的输入尺寸一致;half=True导出FP16模型,体积减半,但部署端需要支持FP16推理。导出的onnx可以用onnxruntime在CPU上直接跑,遇到显存紧张的情况,转TensorRT部署的收益会更明显,AGX Orin这类边缘设备上差距尤其大。
我现在拿到任何新的医学影像数据集,第一件事一定是先看图模式和坐标分布,而不是急着写训练代码。这个习惯帮我省下一个又一个深夜排查的时间,也让我少走了很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取