简介:本资源为肺炎检测数据集,面向医学影像分析、目标检测算法学习与模型训练的研究者及开发者,可用于肺炎阴性/阳性两分类的检测任务,帮助快速搭建训练与验证流程。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及对应的xml、txt标注文件,标注工具为labelImg,采用矩形框标注,共2个类别:pneumonia negative与pneumonia positive,总框数10041。压缩包为7z格式,共2000个文件,其中1999个xml标注文件与1个说明用txt文件,整体约112.65MB,目录结构清晰,便于按格式直接接入常见检测框架。需注意数据集中包含较多增强图片,建议先查看图片预览再斟酌下载。目前已有469人学习下载,适合需要现成标注数据开展肺炎检测实验、课程设计或算法对比的读者参考使用。
1. 肺炎检测数据集 VOC+YOLO 格式 4983 张 2 类别:这批数据到底能训出什么
胸片肺炎检测是医学影像里最容易被低估的入门方向。很多人第一次拿到「肺炎检测数据集 VOC+YOLO 格式 4983 张 2 类别」这类资源,第一反应是直接丢进 YOLOv8 跑一遍看 mAP,结果训练曲线漂亮得离谱,一上真实胸片就集体翻车。问题不在模型,在于这批数据的 2 类别定义、VOC 与 YOLO 两套标注的坐标口径、以及 4983 张里正负样本的实际分布,决定了它能做什么、不能做什么。
这批数据适合三类人:想跑通目标检测全流程的新手、需要做医学影像预处理练手的算法工程师、以及要给肺炎辅助筛查做原型验证的团队。它能解决的是「从零到一跑通一个二分类检测任务」,解决不了的是「直接上线做临床诊断」。下面按数据拆解、格式转换、训练配置、避坑、进阶验证的顺序讲清楚,每一步都能照着复现。
2. 先看清 4983 张 2 类别:VOC 与 YOLO 双格式到底差在哪
拿到压缩包先别急着解压训练,花十分钟把目录结构和标注口径摸清楚,能省掉后面几小时的 debug。VOC 和 YOLO 两种格式同时提供,本质是同一批标注的两种表达,但坐标归一化方式和文件组织完全不同,混用会直接导致框全错位。
2.1 VOC 的 XML 结构与 YOLO 的 txt 结构对照
VOC 格式每张图对应一个同名 XML,核心字段是<size>里的宽高和<object>里的bndbox,坐标是绝对像素值,左上角xmin,ymin、右下角xmax,ymax。YOLO 格式每张图对应一个同名 txt,每行class_id x_center y_center width height,全部是相对图像宽高的归一化值,范围 0 到 1。
| 维度 | VOC (XML) | YOLO (txt) |
|---|---|---|
| 坐标类型 | 绝对像素 | 归一化 0-1 |
| 框表达 | xmin,ymin,xmax,ymax | x_center,y_center,w,h |
| 类别字段 | <name>文本 | class_id 整数 |
| 文件组织 | Annotations + JPEGImages | images + labels |
| 类别映射 | 无独立文件 | classes.txt 或 data.yaml |
两类别在这批数据里通常是pneumonia和normal,或者lung_opacity和normal。具体类别名以 XML 里<name>实际出现的字符串为准,不要凭经验假设。先跑一段统计脚本确认类别分布和框数量,这是后面所有配置的依据。
import os, glob import xml.etree.ElementTree as ET from collections import Counter ann_dir = "Annotations" cls_counter = Counter() box_per_img = [] for xml_path in glob.glob(os.path.join(ann_dir, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() objs = root.findall("object") box_per_img.append(len(objs)) for obj in objs: name = obj.find("name").text.strip() cls_counter[name] += 1 print("类别分布:", dict(cls_counter)) print("总图数:", len(box_per_img)) print("平均每图框数:", sum(box_per_img) / len(box_per_img)) print("无框图数:", box_per_img.count(0))这段脚本做三件事:统计每个类别出现的框数、统计总图数、统计无标注框的图片数量。参数上ann_dir指向 VOC 的 Annotations 目录。重点看两个数:如果某一类框数远少于另一类,说明类别不平衡,训练时要考虑加权或重采样;如果无框图数量不为零,这些图在 YOLO 训练里会被当作纯背景样本,需要确认是有意为之还是标注遗漏。
2.2 4983 张的划分策略与类别不平衡判断
4983 张不是小数目,但医学影像里正负样本比例往往悬殊。常见做法是按 8:1:1 划分训练、验证、测试,但前提是划分前先做分层抽样,保证三个子集里两类比例一致。如果直接随机划分,可能出现验证集里某一类几乎为零的情况,mAP 会失真。
我一般会先算一个比例,再决定要不要过采样。假设统计出来pneumonia框 3200 个、normal框 1800 个,比例接近 1.8:1,属于可接受范围,不需要特殊处理。如果超过 5:1,就要在数据加载阶段用WeightedRandomSampler或者对少数类做增强。划分脚本建议固定随机种子,把划分结果写成三个 txt 文件,后续训练直接读,避免每次跑都重新划分导致结果不可复现。
提示:划分前先剔除损坏图片。用 PIL 打开每张图,捕获异常,损坏的直接从列表移除,否则训练到一半报错很难定位。
3. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑
YOLO 训练只认归一化 txt,所以第一步是把 XML 转成 txt。网上转换脚本一大把,但直接抄来用经常出现框偏移、类别错乱、坐标越界。下面这份是我实际用过的版本,处理了边界裁剪和类别映射。
3.1 转换脚本与类别映射文件生成
import os, glob import xml.etree.ElementTree as ET voc_ann = "Annotations" voc_img = "JPEGImages" out_img = "images" out_lbl = "labels" os.makedirs(out_img, exist_ok=True) os.makedirs(out_lbl, exist_ok=True) # 先收集所有类别名,生成固定映射 classes = sorted({obj.find("name").text.strip() for x in glob.glob(os.path.join(voc_ann, "*.xml")) for obj in ET.parse(x).getroot().findall("object")}) cls2id = {c: i for i, c in enumerate(classes)} with open("classes.txt", "w") as f: f.write("\n".join(classes)) for xml_path in glob.glob(os.path.join(voc_ann, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") W = int(size.find("width").text) H = int(size.find("height").text) stem = os.path.splitext(os.path.basename(xml_path))[0] lines = [] for obj in root.findall("object"): cid = cls2id[obj.find("name").text.strip()] bb = obj.find("bndbox") xmin = max(0, float(bb.find("xmin").text)) ymin = max(0, float(bb.find("ymin").text)) xmax = min(W, float(bb.find("xmax").text)) ymax = min(H, float(bb.find("ymax").text)) if xmax <= xmin or ymax <= ymin: continue # 跳过退化框 xc = (xmin + xmax) / 2 / W yc = (ymin + ymax) / 2 / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f"{cid} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_lbl, stem + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:先扫描全部 XML 收集类别名并排序,保证cls2id稳定,不会因为文件遍历顺序不同导致类别 id 变化。然后逐图解析,把坐标裁剪到图像范围内,跳过宽高为负或零的退化框,最后归一化写入 txt。参数上W、H来自 XML 的<size>,如果某些 XML 缺失 size 字段,需要回退到用 PIL 读图获取尺寸。
3.2 坐标越界、类别错位、空标注三个高频问题
坐标越界是最常见的坑。有些标注的xmax会等于甚至略大于图像宽度,归一化后 w 超过 1,YOLO 训练时虽然不报错,但框会溢出图像边界,影响回归。脚本里用min(W, ...)和max(0, ...)做了裁剪,这是必须的。
类别错位通常发生在多次转换后。如果先转了一版,后来又加了新类别重新转,cls2id顺序变了,旧 txt 的 class_id 就全错了。解决办法是类别映射文件只生成一次,后续所有转换都读同一个classes.txt,不要每次重新排序。
空标注问题指的是某些图在 VOC 里没有<object>,转换后生成空 txt。YOLO 会把空 txt 当作纯背景图参与训练,这本身没问题,但如果这批空图其实是漏标,就会引入噪声。转换后统计一下空 txt 数量,和之前统计的无框图数对一下,确认一致。
注意:转换完成后,随机抽 5 张图用可视化脚本把框画出来,和原图对照。这一步花两分钟,能挡住后面几小时的玄学调参。
4. YOLOv8 训练配置:data.yaml、超参与显存控制
格式转好之后就是训练。YOLOv8 的配置比 v5 更简洁,但医学影像有几个参数需要特别调,默认值直接跑往往效果一般。
4.1 data.yaml 写法与路径陷阱
path: /data/pneumonia train: images/train val: images/val test: images/test nc: 2 names: 0: normal 1: pneumoniapath是数据集根目录,train/val/test是相对路径。这里最大的坑是 YOLO 默认会去images同级的labels目录找同名 txt,所以目录结构必须是images/train对应labels/train。如果转换时把图片和标签放在同一目录,训练会报找不到标签。nc必须和classes.txt行数一致,names的顺序必须和cls2id完全对应,错一个位置整个类别就反了。
4.2 医学影像场景下的 batch、imgsz 与增强参数
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ mosaic=0.5 \ fliplr=0.5 \ degrees=10 \ cache=True参数逐个说:imgsz=640是速度和精度的平衡点,胸片分辨率高,但缩到 640 后小病灶会丢,如果显存够可以上 1024。batch=16在 8G 显存上跑 yolov8s 比较稳,爆显存就降到 8。mosaic=0.5比默认的 1.0 低,因为医学影像拼接后解剖结构会错乱,过度拼接反而有害。degrees=10做小角度旋转增强,胸片本身有轻微摆位差异,这个增强合理。fliplr=0.5水平翻转要谨慎,左右肺对称所以可以翻,但如果数据里有左右标记(如 L/R 铅字),翻转会让标记反向,需要确认后再开。
cache=True把图片缓存到内存,4983 张 640 尺寸大概占几 G 内存,能显著加快训练。如果内存不够就设cache=disk或者关掉。
提示:第一次训练先用 10 个 epoch 跑通流程,确认 loss 正常下降、验证集能出结果,再上 100 epoch。直接长跑发现配置错了,浪费的是几小时。
5. 训练过程避坑:loss 不降、mAP 虚高、显存爆炸怎么排查
这一章是血泪经验集中区。医学影像检测的坑和自然图像不太一样,很多现象背后是数据问题而不是模型问题。
5.1 现象:loss 从第一个 epoch 就不降
原因通常是标签格式错误。最常见的是 txt 里坐标没归一化,还是绝对像素值,YOLO 读进去后框全在图像外,模型学不到东西。其次是 class_id 从 1 开始而不是 0,导致类别索引越界被忽略。
解决:随便打开一个 txt,确认五个值都在 0 到 1 之间(class_id 除外),class_id 从 0 开始。再用yolo detect train前先跑一次yolo detect val加载数据,如果数据有问题会在加载阶段报错。
5.2 现象:验证集 mAP 很高但实际预测全是同一类
原因是类别不平衡加上验证集划分不合理。如果验证集里某一类占 90%,模型全预测这一类也能拿到高 mAP。这是指标欺骗。
解决:重新做分层划分,保证验证集两类比例和整体一致。训练时加cls=1.0以上的分类损失权重,或者对少数类做复制增强。看混淆矩阵比看 mAP 更靠谱,yolo detect val会输出混淆矩阵,重点看两类是否都被召回。
5.3 现象:训练到一半 CUDA out of memory
原因是cache=True占满内存后,数据加载又申请显存,或者 batch 设太大。医学影像单图分辨率高,预处理后占用比自然图像大。
解决:先把batch减半,再把cache改成disk或关闭。如果还爆,降imgsz到 512。另外workers设太大也会导致内存峰值,一般设 4 到 8 就够。
5.4 现象:框位置整体偏移一个固定量
原因是 VOC 转换时xmin/ymin和xmax/ymax顺序写反,或者宽高算成了xmax-xmin但中心点算错。这种偏移是系统性的,所有框都偏。
解决:抽一张图,手工算一遍归一化坐标,和 txt 里的值对比。重点检查xc = (xmin+xmax)/2/W这个公式有没有写错。转换脚本里最容易手滑的就是这里。
5.5 现象:训练正常但推理时框重叠严重
原因是 NMS 的iou阈值默认 0.7,医学影像里病灶区域可能相邻,阈值太高导致重叠框保留。另外如果标注本身有重复框,模型会学到重复预测。
解决:推理时把iou降到 0.5 左右,conf从 0.25 提到 0.4 过滤低置信框。同时回查训练标签,统计有没有同一目标被标了多个框,有的话在转换阶段做去重。
6. 训完之后怎么验证:从 mAP 到单张推理的落地检查
训练出权重只是开始,真正决定这批数据值不值得投入的,是推理结果能不能对上临床直觉。我一般会做三层验证,从指标到可视化再到边界样本。
6.1 用混淆矩阵和 PR 曲线判断类别是否真的分开
yolo detect val跑完会在runs/detect/val下生成混淆矩阵和 PR 曲线。混淆矩阵看对角线,如果两类都在 0.8 以上,说明模型确实学到了区分特征。如果某一类召回很低,回去看那一类的标注是不是框得太松或太紧。PR 曲线看曲线下面积,比单看 mAP 更能反映阈值变化时的稳定性。
6.2 单张推理脚本与置信度阈值调优
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_images", conf=0.4, iou=0.5, imgsz=640, save=True ) for r in results: print(r.path, r.boxes.cls.tolist(), r.boxes.conf.tolist())conf=0.4比默认 0.25 高,是为了减少假阳性。医学场景里漏检和误检的代价不对称,具体偏向哪边取决于用途:筛查可以容忍误检,辅助诊断要控制误检。iou=0.5控制重叠框合并。跑完看save=True输出的可视化图,重点看有没有框到非病灶区域,比如把肋骨或医疗器械框成肺炎。
6.3 边界样本检查:小病灶、低对比度、设备差异
从测试集里挑三类图单独看:病灶面积小于图像 1% 的、对比度低看起来灰蒙蒙的、以及不同设备拍的。这三类是模型最容易翻车的地方。如果小病灶漏检严重,说明imgsz=640不够,要上 1024 重训。如果低对比度漏检,考虑在预处理阶段加 CLAHE 增强。设备差异导致的性能下降,说明数据多样性不足,需要补充不同来源的样本。
我自己的习惯是每次训完模型,先不看 mAP,先随机抽 20 张测试图肉眼过一遍,把漏检和误检的图单独存一个文件夹,攒够一批再回头分析是数据问题还是模型问题。这个习惯帮我省掉了很多次盲目调参。希望帮到你。
本文还有配套的精品资源,点击获取