简介:面向医学影像与深度学习初学者的肺病X光片分类数据集,整合了细菌性肺炎、新冠病毒、正常肺、结核和病毒性肺炎五类样本,所有图片已基于YOLOv8完成标注,可直接用于目标检测或分类模型的训练与验证。压缩包共1601个文件,包含800张JPG原始影像、800个TXT格式的YOLO标注文件(记录目标框坐标与类别标签)及1个YAML数据集配置文件,整体大小仅25.51MB,结构紧凑,便于快速下载与解压使用。目前已有419人学习/下载该数据集,适合作为课程设计、算法对比或科研验证的基准数据。使用者无需手动标注即可直接划分训练集和验证集,配合YOLOv8系列模型可快速搭建肺病识别流程,也可借助YAML文件灵活调整类别与路径配置,节省数据准备时间,专注模型调优与结果分析。
1. 800张X光片和五类肺病标签:为什么拿到zip先别急着训练
一个zip压缩包,800张原始X光片,用YOLOv8格式标好了细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五类目标——听起来像是一条可以直接开工的检测产线,但真实情况往往没这么顺。这个X光片肺病数据集最大的价值不是“能直接训出一个高分模型”,而是给你一套带标注的医学影像起步素材,让你有机会把数据验收、格式转换、类别不均衡处理、训练调参这一整条链路走通。适合刚啃完YOLOv8文档、手里缺干净医疗数据的从业者拿来练手,也适合做迁移学习基线。下文从解压开始,把这800张图一步步变成能出指标的检测模型。
2. 拆开这个“肺病数据集”zip:目录组织、标签格式与五类目标核对
2.1 五类的医学影像差异决定了YOLOv8学什么
做目标检测和做分类不一样,YOLOv8学的是“框内区域的特征”,所以标签框画在哪、画多大,直接决定了模型能不能区分这五类。肺病X光片里,细菌性肺炎的典型表现是肺实变,病灶区域密度增高,常出现在中下肺野;结核则偏爱锁骨下区和上肺野,可见结节、空洞和播散灶;新冠病毒肺炎的特征是外周带磨玻璃影,往往双侧分布;病毒性肺炎更多表现为间质纹理增粗,呈网格状;正常肺纹理清晰,没有局灶性高密度影。
这里要提一个关键点:细菌性肺炎和结核在很多X光片上看起来非常像,都是局灶性高密度影,如果标注时框的边界画得松,模型就会把两者的纹理特征混在一起。常见做法是单个病灶画一个框,框紧贴病灶边缘,宁可框小一点也不要圈进大半个肺野。YOLOv8的anchor和特征图尺度是固定的,框的宽高比如果严重偏离常见分布,训练时收敛会很慢。
2.2 解压与文件核验:unzip、统计脚本和zip隐藏坑
拿到zip后第一件事不是打开训练脚本,而是先解压并核对文件结构。解压时我习惯用unzip命令而不是图形化工具,因为命令行能把文件数、目录层级这些信息一次性暴露出来。
unzip -q Xray_Lung_Dataset.zip -d lung_data find lung_data -type f | wc -l find lung_data -type f -name "*.jpg" | wc -l find lung_data -type f -name "*.txt" | wc -l find lung_data -type f | head -30第一条命令里-q是静默解压,-d lung_data指定解压目标目录,避免压缩包把文件直接散在当前文件夹。后三条命令分别统计总文件数、图片数和标签数,正常情况下图片数和标签数应该一致,都是800。最后一条head -30是看前30个文件的路径,用来确认zip内部是否嵌套了一层同名目录——这种情况非常常见,解压后实际路径变成lung_data/Xray_Lung_Dataset/images/...,后面写data.yaml时极容易路径错误。
如果发现标签数少于图片数,先别急着补标注,检查是不是有图片真的没有对应目标,还是漏标了。还有一种压缩包特有的坑叫zip伪加密,文件头里加了加密标记,双击解压时提示要密码,但直接用unzip或7-Zip打开却能正常解出来,这是旧式压缩工具的产物,不是真的加密,不用找密码,换个解压工具绕过去就行。
2.3 LabelMe的JSON转YOLO的TXT:坐标换算与格式校验
很多医学影像数据集是用LabelMe标注的,导出的是JSON文件,里面存的是像素坐标系下的多边形或矩形坐标;而YOLOv8的标签格式是归一化中心点加宽高,每一行一个目标。如果拿到手的是JSON,需要自己写转换脚本。
import json import os def labelme_to_yolo(json_path, out_dir, class_map): with open(json_path, encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] x1, y1 = shape['points'][0] x2, y2 = shape['points'][1] cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h bw = abs(x2 - x1) / img_w bh = abs(y2 - y1) / img_h lines.append(f"{class_map[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, os.path.basename(json_path).replace('.json', '.txt')) with open(out_path, 'w') as f: f.write('\n'.join(lines)) class_map = { 'bacterial_pneumonia': 0, 'covid': 1, 'normal': 2, 'tuberculosis': 3, 'viral_pneumonia': 4 }这段脚本的核心是坐标换算:LabelMe的坐标原点是图片左上角,单位是像素;YOLO格式要求把中心点x和y分别除以图片宽高,宽高也做同样的归一化。class_map把标签名映射成整数id,id从0开始连续编号,这个顺序必须和后面data.yaml里的names顺序完全一致,错一位整个训练都是错的。
转换后需要抽样验证三个边界情况:坐标被截断到0或1、宽高为0、类别id越界。一条正常训练标签应该长这样:3 0.4825 0.6183 0.2104 0.2841,里面的数全是0到1之间的小数。如果看到1.024或-0.003这种值,基本可以断定换算时用了错误的图片尺寸。
3. 把X光片肺病数据集喂给YOLOv8:数据YAML、训练命令与必调参数
3.1 数据YAML的写法决定成败:绝对路径、类别顺序不能错
Ultralytics YOLOv8的训练入口是yolo detect train命令,它需要一个数据配置文件来告诉模型“去哪里找图片、图片里有哪些类别”。这个YAML写错是新手最常翻车的点,而且报错信息往往模棱两可。
path: /home/user/lung_data train: images/train val: images/val names: 0: bacterial_pneumonia 1: covid 2: normal 3: tuberculosis 4: viral_pneumoniapath是数据集根目录,train和val是相对path的子目录,指向存放图片的文件夹。这里有个容易绕晕的地方:YOLOv8读取标签时,会自动把images替换成labels,也就是说如果图片在images/train下,标签就必须在labels/train下,目录名必须是这个对应关系,不能自己改成annotations。names里的id顺序要和标签txt里的第一个数字对应,之前转换脚本里定的class_map是什么顺序,这里就写什么顺序。
我一般建议path写绝对路径,尤其是刚上手时。相对路径在不同机器上解析结果不一致,换一台电脑就跑不通,排查起来很费时间。还有一个隐藏问题:路径里不要带中文和空格,Ultralytics对路径的解析在Windows上特别容易出问题,数据集文件夹命名用lung_data这种全小写加下划线的风格最稳。
3.2 从最小训练命令开始:epochs、imgsz、batch、patience四参数怎么定
数据和配置就位后,第一次训练不要一上来就堆大参数。800张图的数据集规模不大,重点是先把训练流程跑通,再关注指标。
yolo detect train \ data=lung.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ device=0 \ project=lung_exp \ name=run1model=yolov8s.pt会从官方权重仓库下载预训练模型,第一次运行需要联网。yolov8s在800张图的小数据集上比yolov8n更稳,精度上限更高;如果显存紧张再退回yolov8n。epochs=100是个合理的起点,训练过程中用patience=20做早停——连续20轮验证集指标不再提升就自动停止,省时间且防止过拟合。imgsz=640是YOLOv8的通行配置,但X光片原始分辨率通常超过1024,如果最终部署环境对分辨率不敏感,可以提到768或832。batch=16要看显存,后文会专门讲。project和name指定输出目录,训练结果会落在lung_exp/run1/下,包括weights权重、results.png损失曲线和混淆矩阵。
3.3 CPU和低显存GPU怎么跑:先用小模型验证pipeline再上完整训练
不少人是先在CPU机器上搭环境,跑通代码再切换到GPU服务器。Ubuntu 20.04上搭建CPU版YOLOv8环境并不复杂,装好Python和pip install ultralytics就能跑,但CPU训练800张图会非常慢,所以策略要调整:先用yolov8n.pt、imgsz=416、epochs=5跑一个微型训练,目的不是出精度,而是确认数据路径、标签读取、训练循环没有报错。这五轮跑完,看results.png里loss是不是在下降,如果loss正常下降,说明数据链路是通的。
显卡是GTX 1660 Ti这种6GB显存时,yolov8s加batch=16加imgsz=640大概率会OOM。实际处理时把batch降到8,同时确认amp=True——Ultralytics默认开启混合精度训练,能省不少显存。Windows用户还要注意一个特定问题:DataLoader的workers默认值在Windows上偶尔会导致训练卡死或报错,显式设置workers=0可以规避。如果CPU训练时看到进度条不走,多半也是这个问题。
4. 训练前先验货:把800张图的标注框画回去,验证标签是能用的
4.1 可视化巡检脚本:一张图看清框的松紧和出界
标注文件是人标的,人就会出错。最直接的校验方式是把标签框画回原图,用肉眼检查框和病灶的关系。写一个简单的巡检脚本,随机抽几十张图输出到临时目录。
import cv2 import glob for txt_path in glob.glob('lung_data/labels/train/*.txt')[:20]: img_path = txt_path.replace('labels', 'images').replace('.txt', '.jpg') img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cls, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) color = (0, 255, 0) if int(cls) == 2 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite('visual_check/' + txt_path.split('/')[-1] + '.jpg', img)这段代码的关键是把归一化坐标还原成像素坐标:中心点减去一半宽高得到左上角,加上一半宽高得到右下角。别小看这一步,坐标还原后框和病灶的位置是否吻合,直接暴露标注质量。重点看三种情况:框是不是完全在图片内,如果坐标换算错了,框会出界;框是不是紧贴病灶,如果整个肺叶都被圈进去,模型会学到错误纹理;同一张图上多个框是否覆盖了所有可见病灶。
实际操作中,400张训练图全看一遍不现实,抽20到30张足够发现问题。巡检时如果发现某类图片大面积错标,回头修标注的成本远低于训练后返工。
4.2 统计每个类和每张图的标签数:类别不均衡会误导后续调参
800张图看着不少,分到五类里可能严重不均。训练前先统计一下每个类别的框数量,这一步能避免后续看到mAP时误判模型能力。
from collections import Counter import glob counter = Counter() images_with_targets = 0 empty_files = 0 for txt_path in glob.glob('lung_data/labels/train/*.txt'): with open(txt_path) as f: lines = f.readlines() if len(lines) == 0: empty_files += 1 else: images_with_targets += 1 for line in lines: counter[int(line.split()[0])] += 1 print('类别统计:', counter) print('有目标的图片数:', images_with_targets) print('空标签文件数:', empty_files)统计结果会直接影响训练策略。如果某一类占比极低,比如结核只有几十个框,YOLOv8会把这类当成稀有前景,mAP会被其他类别拉平,而且val集里这类样本太少,评估结果方差巨大。常见做法是先看数据分布再决定要不要加权,而不是盲目调loss。如果五类大致均匀,800张图完全可以直接训练;如果严重不均,后续要考虑类别权重或过采样。
还有一个容易被忽略的指标:空标签文件数。如果一张X光片确实没有病灶,空标签是正常的,这类是hard negative,对模型有价值;但如果有几十个空文件,很可能有一部分是漏标,需要回到图片里人工确认。
4.3 分层划分train/val:别让某一类全跑进验证集
随机划分看起来公平,但在小数据集上容易出问题:类别分布不均匀时,可能某一类在训练集里有100个框,在验证集里只有3个,验证指标随机性极大。更稳妥的做法是分层抽样。
import random from pathlib import Path random.seed(42) label_files = list(Path('lung_data/labels').glob('*.txt')) label_files = [f for f in label_files if f.stat().st_size > 0] random.shuffle(label_files) val_count = int(len(label_files) * 0.2) val_files = set(label_files[:val_count]) train_files = [f for f in label_files if f not in val_files] for f in train_files: img = str(f).replace('labels', 'images').replace('.txt', '.jpg') dst_img = 'lung_data/images/train/' + Path(img).name Path(img).rename(dst_img) Path(str(f)).rename('lung_data/labels/train/' + f.name) # 同理处理val_files这段脚本按20%比例切出验证集,代码里random.seed(42)固定随机种子,保证每次跑结果一致,可复现。更精细的分层做法是先把文件按类别分组,再从每个类别里分别抽20%,确保验证集的类别比例和总体一致。Path对象在Windows和Linux上的路径拼接都比较安全,避免手写字符串路径踩斜杠坑。
划分完成后还要做一道自检:统计train和val里每个类别的框数,比例应该接近4比1。如果发现某个类别在val里缺失,回到划分步骤重跑,别嫌麻烦——小数据集上val泄漏或分布失衡导致的虚高指标,会误导你对模型真实能力的判断。
5. YOLOv8训练避坑实录:X光片肺病数据集踩过的五个坑
5.1 标签文件是空的:exif旋转、漏存json与空txt
现象:训练过程没有报错,但某些类别recall一直为0,打开对应txt文件发现是0KB。
原因:两种情况最常碰到。一是LabelMe标注后没有逐张保存,部分json文件内容为空,转换脚本没报错但产出了空txt;二是智能手机或部分医疗设备拍摄的X光片照片带有exif旋转信息,图像实际显示方向和像素存储方向不一致,画框时看着是正的,转成YOLO坐标后框错位,甚至因为坐标计算得到负值被判定为空。
解决:先统计空文件数量,如果比例不高,把空标签保留,作为负样本;如果比例高,回到标注源头补标。exif问题用PIL.ImageOps.exif_transpose处理一遍再转换,或者用mogrify -auto-orient批量修正。这类问题跑训练才发现的话,浪费时间且难以定位。
5.2 “No labels found”:路径、缓存和同名配对问题
现象:训练命令执行后几秒钟就报错,提示找不到标签文件,退出训练。
原因:三种可能。data.yaml里的path指向了错误的根目录;图片在images/train而标签被放到了train_labels这种自定义目录里,YOLOv8默认只认labels/train;还有一个很隐蔽的情况——Ultralytics会把数据集信息缓存到cache文件里,如果之前跑过一次错误路径的训练,缓存记录了旧路径,改好数据后不删缓存仍然报错。
解决:删掉数据集根目录下生成的*.cache文件,检查path是否为绝对路径,确认图片和标签目录的对应关系。命令行下用ls lung_data/labels/train/ | head快速看一眼标签是否存在,比翻报错日志更直接。
5.3 val泄漏导致mAP虚高:划分前先查病历
现象:训练曲线很漂亮,val loss低于train loss,mAP50超过0.9,但拿到新图上推理效果平平。
原因:随机划分时没有考虑数据来源。医学影像数据集里,同一个病人的多张X光片可能被分到了train和val两边,模型其实“见过”同一病人的影像特征,val指标虚高。800张图虽然不大,但这类情况在真实采集数据里经常发生。
解决:划分前先看文件名,常见的命名规则里有患者ID或检查序列号。如果有患者信息,按患者分组后再划分,保证同一个患者的片子只出现在一个集合里。只按文件名随机shuffle在这种数据集上是不可靠的。
5.4 结核和细菌性肺炎互相误判:关注框内纹理与肺野位置
现象:训练完成后看混淆矩阵,结核和细菌性肺炎两个类别之间有很大的误判块。
原因:这两个类别在X光片上的局部纹理非常接近:都是局灶性实变影,密度和形状没有明显边界。YOLOv8默认的640输入尺寸下,框内特征可能丢失了能区分它们的细节。如果标注的框画得太松,把正常肺纹理也圈进去,区分难度更大。
解决:先检查标注边界,确认框是否紧贴病灶;然后尝试提高imgsz到768或832,保留更多纹理细节。还有一个实用策略:不只看框内纹理,也关注病灶在肺野里的位置,结核常见上肺野,细菌性肺炎多在中下肺野。YOLOv8不会显式使用位置先验,但它能隐式学到,前提是标注框的位置是一致的——所以别为了好看把框画得横跨整侧肺野,那样等于把位置信息抹掉了。
5.5 CUDA OOM总在最后一步:显存预算的计算与自救
现象:训练开始时没问题,跑了几轮到某个节点突然报CUDA out of memory,或者整个进程被kill,看起来像训练卡死。
原因:显存占用是动态的,loss计算、梯度累积和验证阶段的峰值比平均占用高。X光片虽然是灰度图,但读取时被转成三通道,批量加载时的显存开销比预期大一截。6GB显存的卡跑yolov8s加batch=16加imgsz=640,正好卡在峰值边缘,随时可能爆。
解决:训练时用nvidia-smi -l 1盯着显存变化。爆了就按三个顺序调整:先降batch到8,这是影响最小的手段;再不行降imgsz到480;最后才换yolov8n。Ultralytics开启amp=True能省约30%的显存,默认是开的,别在配置里关掉。另外,validate阶段也会占显存,把batch=8同时传给train和val命令,避免出现训练不爆验证爆的诡异情况。
6. 从指标到部署:用confusion_matrix和ONNX验证这个模型真的能用
6.1 混淆矩阵告诉你的第一件事:谁和谁分不开
训练完成后,lung_exp/run1/目录下会生成confusion_matrix.png和results.png。看指标时别只盯着mAP——直接看混淆矩阵对角线以外的高亮块。X光片肺病检测里,如果结核被大量识别成细菌性肺炎,说明框内特征的区分度不够,这时再调分类头或加数据增强收益有限,不如回标注侧检查是不是某些框的边界有问题。
6.2 灰度图与CLAHE:X光片预处理的小技巧
X光片本质是灰度图,训练时可以直接以灰度图输入,也可以转成三通道让加载器兼容,二者对精度影响不大。但对比度对检测效果影响明显,肺纹理不清晰的病例上,预处理阶段做一次CLAHE对比度自适应增强,比多训50轮epoch更有效。
import cv2 img = cv2.imread('xray.jpg', cv2.IMREAD_GRAYSCALE) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) enhanced = clahe.apply(img)clipLimit控制对比度限制幅度,过大会把噪点同步放大,一般取2到3之间;tileGridSize取8×8,针对胸片这种大尺寸图像是个稳妥起点。增强后的数据作为另一个实验分支跑,和原始图对比mAP。
6.3 导出ONNX并做一次本地推理自检
训练完成后第一步不是急着上板子,而是导出ONNX在本地跑一次推理,确认权重在脱离PyTorch环境后依然能给出正确结果。
yolo export model=lung_exp/run1/weights/best.pt format=onnx imgsz=640导出的best.onnx用onnxruntime做一次单图推理,输入先用6.2的预处理逻辑做灰度归一化,输出是一个[1, 5, 8400]的张量,按置信度阈值筛出框,再复现出yolov8的坐标解码逻辑,确认框能落在病灶上。这一步走通了,后面不管是往RK3588还是其他板端迁移,剩下的工作都是针对推理框架做算子适配,数据这个源头已经没有问题。
医疗影像模型最终要面对的是新图上未知病例的考验。我现在每训完一版,都会单独留出自己肉眼见过但不参与训练的片子做一次“盲测”,确认模型不是背了数据集的题。这个习惯救过我太多次,也建议你保留。希望帮到你。
本文还有配套的精品资源,点击获取