ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv7肺病X光片检测实战:从标签校验到模型训练的完整流程

YOLOv7肺病X光片检测实战:从标签校验到模型训练的完整流程 简介面向肺炎影像识别与目标检测任务这份X光片肺病数据集提供800张原始胸部X光图片并使用YOLOv7格式完成标注覆盖细菌性肺炎、新冠病毒、正常肺、结核和病毒性肺炎五种类别适合医疗影像AI研究者、算法工程师以及相关课程实践。资源共1601个文件包含800个jpg图像、800个对应txt标注文件以及1个yaml配置文件其中txt为YOLO标签格式yaml定义类别名称与路径解压后即可接入YOLOv7训练流程。整包体积约25.51MB轻量紧凑便于下载与快速迭代。已有410人学习下载说明具备一定参考价值。通过该数据集用户可直接获得已标注的目标框信息与规范的数据目录结构省去人工标注环节可用于模型训练、效果评估或做数据增强、迁移学习等二次开发。1. 800张YOLOv7格式的X光片肺病数据集能跑但别把它当成商用模型一套X光片肺病数据集800张原始图片用YOLOv7标记可识别细菌性肺炎、新冠病毒、正常肺、结核、病毒性肺炎五类——这样的zip出现在算法工程师手里通常意味着一个研发起点而不是一个能直接上线的成品。800张对有监督的目标检测来说并不多五类分摊后每类平均只有一百多张它的实际价值是让训练YOLOv7检测肺部病灶的完整流程在半天内先跑起来。适合三类人准备做医学影像识别课题的学生、想拿公开小数据验证模型选型的研究者、手上有本地数据但缺少预训练底座的团队。用它之前请先接受两个事实标签格式未必统一五类之间在X光片上天然有混淆后面的每一个坑都从这里长出来。2. 先校验再训练YOLOv7标签格式与类别统计的检查脚本2.1 YOLOv7标注规则一张图对应一个同名txt五个数字全是相对坐标YOLOv7标注不是把框存成xml或json而是每个目标一行五列数字class_id x_center y_center width heightx_center、y_center是目标框中心的坐标width、height是框宽和框高四个值全部除以图片宽高做了归一化所以都是0到1之间的浮点数。一张X光片对应一个同名txtCXR_001.jpg对应CXR_001.txt文件名错一个字符模型就找不到标签。解压这个zip后先看目录结构。如果作者是按标准布局给的通常有images/和labels/两个平级目录外加一个classes.txt。classes.txt里就是类别顺序训练时对类别id的映射完全依赖这个文件的顺序顺序错了细菌性肺炎和新冠的标签会全部对调且训练不会报任何错。这中间有一个很容易看走眼的地方正常肺这一类的标注策略。YOLOv7是目标检测每一行代表一个有界框。正常肺如果不打框那它的txt就是空文件如果作者给正常肺打了双肺区域的大框那normal就会作为一个显式类别出现在classes.txt里。两种做法都能训练但评估口径完全不同。先确认作者选了哪一种再看下面的脚本。2.2 标签合法性检查脚本越界、空txt、类别ID越界一次扫完拿到zip后先别急着配环境第一步永远是跑一遍标签检查。我习惯用一个脚本把所有labels文件夹里的txt扫一遍统计每类框数量同时抓出坐标越界、宽高为负、字段数不对、类别id越界这几类常见问题。import os import glob from collections import Counter labels_dir labels class_names [ bacterial_pneumonia, covid19, normal, tuberculosis, viral_pneumonia, ] # 顺序必须以解压目录里的 classes.txt 为准这里只是最常见的一种排列 bad_files [] # 记录格式/数值异常的文件 empty_files [] # 记录空标签文件 cls_box_counter Counter() total_boxes 0 tiny_boxes 0 for txt_path in sorted(glob.glob(os.path.join(labels_dir, *.txt))): with open(txt_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_files.append(os.path.basename(txt_path)) continue for line in lines: parts line.split() if len(parts) ! 5: bad_files.append((os.path.basename(txt_path), f字段数{len(parts)})) continue try: cls_id int(parts[0]) x_c, y_c, w, h (float(v) for v in parts[1:]) except ValueError: bad_files.append((os.path.basename(txt_path), 数值解析失败)) continue if cls_id 0 or cls_id len(class_names): bad_files.append((os.path.basename(txt_path), f类别id{cls_id}越界)) continue if not (0.0 x_c 1.0 and 0.0 y_c 1.0): bad_files.append((os.path.basename(txt_path), 中心坐标不在[0,1])) continue if not (0.0 w 1.0 and 0.0 h 1.0): bad_files.append((os.path.basename(txt_path), 宽高非法)) continue cls_box_counter[cls_id] 1 total_boxes 1 if w * h 0.0005: tiny_boxes 1 print(总框数:, total_boxes) for cls_id in range(len(class_names)): print(f类 {cls_id} ({class_names[cls_id]}): {cls_box_counter[cls_id]} 个框) print(面积过小的框:, tiny_boxes) print(异常文件:, bad_files if bad_files else 无) print(空标签txt:, len(empty_files))逻辑说明先按行拆标签再逐个字段校验。中心点坐标必须落在[0,1]区间因为YOLO归一化后中心点不会超出图像范围宽高也必须在(0,1]之间等于0的框没有意义大于1说明归一化时用错了图片尺寸。面积阈值0.0005是按640×640输入图估算的相当于16×16像素在X光片病灶检测里这类小框绝大多数是标注噪声或误标需要抽查。参数说明class_names的顺序必须和作者给的classes.txt完全一致。如果不一致类别id对应的名字全错后面训练的混淆矩阵也没法看。建议把脚本里的class_names替换成classes.txt的实际内容而不是照抄我这里的猜测顺序。2.3 图片与标签配对检查不要漏看“有图无标签”的无框X光片标签内容合法不代表文件配对正确。如果某张图没有同名txtYOLOv7训练时会跳过这张图但你自己可能毫无感知直到发现800张图实际只训了750张。from pathlib import Path from PIL import Image images_dir Path(images) labels_dir Path(labels) image_files sorted(list(images_dir.glob(*.jpg)) list(images_dir.glob(*.png))) missing_label [] for img in image_files: txt labels_dir / (img.stem .txt) if not txt.exists(): missing_label.append(img.name) print(有图无标签:, len(missing_label), missing_label[:10]) sizes [] for img in image_files[:200]: with Image.open(img) as im: sizes.append(im.size) widths [s[0] for s in sizes] heights [s[1] for s in sizes] print(图片宽度范围:, min(widths), -, max(widths)) print(图片高度范围:, min(heights), -, max(heights))逻辑说明这一段遍历图片目录检查每个jpg/png是否有同名txt文件。如果缺失数量很多先判断是不是作者把正常肺统一留空还是真的漏标。医学X光片数据集里正常肺图片的txt为空是常见做法不代表错误。参数说明图片尺寸范围统计我一般只看前200张而不是全量遍历。X光片分辨率差异很大有些是1920×1280的高清胸片有些是512×512的缩略图。如果尺寸跨度超过四倍建议先统一把长边压到1600再训练否则dataloader每次做letterbox的缩放成本很高训练速度会被明显拖慢。3. 按患者拆train/val/test分层划分脚本和data.yaml配置3.1 为什么划分必须按患者而不是按图片验证集虚高的翻车内幕目标检测划分有两种做法按图片随机打散或按患者分组。很多数据集同一患者会有正位和侧位两张胸片如果这两张被随机分进train和val模型等于在验证集上做了开卷考试——它记住了患者特征而不是病灶特征。训练时val mAP能到0.85换到外部数据直接掉到0.5这种翻车基本就是这个原因。如果文件名里有患者ID比如patient_001_left.jpg和patient_001_right.jpg就要按patient_001这个前缀归组。800张的医学小数据集尤其要守这条底线。就算你这个数据集确实一人一张也按这个逻辑写一遍至少不会漏。3.2 按主导类别分层的患者级划分脚本只按患者分组还不够。五类分布很可能不均匀新冠可能只有40张如果随机划分测试集里可能一张新冠都没有。所以我习惯按每个患者的主导类别做分层再用sklearn的StratifiedShuffleSplit两次拆分达到约70% train、15% val、15% test。from pathlib import Path from collections import Counter, defaultdict import numpy as np from sklearn.model_selection import StratifiedShuffleSplit images_dir Path(images) labels_dir Path(labels) image_files sorted(list(images_dir.glob(*.jpg)) list(images_dir.glob(*.png))) def patient_id_of(filename: str) - str: # 如果文件名是 patient_001_left.jpg取 patient_001 这一段 return _.join(filename.split(_)[:2]) patient_to_images defaultdict(list) for img in image_files: pid patient_id_of(img.stem) patient_to_images[pid].append(img) # 给每个患者指定一个主导类别该患者所有标签中出现次数最多的类 patient_dominant {} for pid, imgs in patient_to_images.items(): cnt Counter() for img in imgs: txt labels_dir / (img.stem .txt) if not txt.exists(): continue for line in txt.read_text().splitlines(): if line.strip(): cnt[int(line.split()[0])] 1 patient_dominant[pid] cnt.most_common(1)[0][0] if cnt else -1 pids np.array(list(patient_to_images.keys())) dominant np.array([patient_dominant[pid] for pid in pids]) # 先分出 70% train剩下 30% 是 valtest 的临时池 outer_split StratifiedShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, temp_idx next(outer_split.split(pids, dominant)) # 再从临时池里平分 val 和 test inner_split StratifiedShuffleSplit(n_splits1, test_size0.5, random_state42) val_idx, test_idx next(inner_split.split(pids[temp_idx], dominant[temp_idx])) train_pids set(pids[train_idx]) val_pids set(pids[temp_idx][val_idx]) test_pids set(pids[temp_idx][test_idx]) def save_split(split_name, pid_set): out [] for pid in pid_set: out.extend(str(p) for p in patient_to_images[pid]) Path(f{split_name}.txt).write_text(\n.join(out) \n) save_split(train, train_pids) save_split(val, val_pids) save_split(test, test_pids) print(train患者数:, len(train_pids), val:, len(val_pids), test:, len(test_pids))逻辑说明先按文件名前缀归组再统计每个患者所有标签里的类别分布取最多的类作为该患者的分层标签。StratifiedShuffleSplit会按这个主导类别保持比例避免小类别在某个集合里消失。第一次拆分拿走30%作为临时池第二次把临时池一分为二最终train/val/test接近70/15/15。需要留意一个边界如果某个类别只有一个患者sklearn会在这里报错因为它无法在一个样本上同时构造出train、val、test。这种情况合理处理方式是把这个患者并入train集合val和test里放弃这个类别而不是强行拆分。遇到报错时按这个思路降级。3.3 划分后目录结构与data.yamltrain/val/test三处路径别写错划分完成后有三种组织方式。一种是直接在原目录上建子目录把图片搬进images/train、images/val、images/test标签同步搬进labels/train等。另一种是把上面生成的train.txt、val.txt、test.txt直接作为YOLOv7的train/val参数每行一个图片路径。我推荐用txt列表方式因为不用挪文件后面换划分时重跑一次脚本就行。对应的data.yaml模板如下train: /absolute/path/to/train.txt val: /absolute/path/to/val.txt test: /absolute/path/to/test.txt nc: 5 names: - bacterial_pneumonia - covid19 - normal - tuberculosis - viral_pneumonia参数说明train和val必须填绝对路径YOLOv7的dataloader对相对路径处理不统一容易在换目录后找不到图片。nc是类别数必须和names列表长度一致。names顺序必须再对一遍classes.txt这一步错了整个训练白跑一小时。4. 跑通YOLOv7肺病识别训练最小命令、小数据参数和第一个mAP4.1 最小训练命令从官方YOLOv7仓库直接起训到这一步标签检查完、数据也划分好了接下来的流程就是标准的YOLOv7训练自己的数据集。先确认官方YOLOv7环境能正常跑起来再跑最小训练命令。python train.py \ --data xray.yaml \ --weights yolov7.pt \ --batch-size 8 \ --epochs 100 \ --img 640 640 \ --workers 4 \ --project runs/train \ --name xray逻辑说明--weights yolov7.pt表示用COCO预训练权重初始化而不是从零开始。800张X光片从零训练必过拟合收敛也慢迁移学习是唯一务实起点。--img 640 640是训练和验证的统一输入尺寸YOLOv7在训练时会对原图做letterbox再缩放。参数说明batch-size设8是因为X光片虽被缩到640但原图分辨率大dataloader的解码开销高于普通自然图像8G显存的卡上batch 8比较稳妥。如果显存够大可以到16但不要为了追求batch去压图片尺寸病灶检测对细节敏感640×640已经是下限。如果你的YOLOv7版本对--img这个参数不识别换成--img-size再看python train.py -h确认。4.2 针对800张小数据集的五个必要调整freeze、mosaic、rect与batch800张图属于小样本直接按默认参数跑能出结果但mAP大概率不理想。我一般会做五个调整。第一是冻结backbone前几十层。用预训练权重起步后前面几个epoch让特征提取层学会的是COCO里的通用纹理而不是肺部结构。在小数据集上直接全量微调容易让backbone被少数样本带偏。YOLOv7的train.py支持--freeze 50含义是冻结前50层先训练检测头跑20个epoch后再去掉这个参数继续微调全部层。第二是保持mosaic增强开启。800张样本太少mosaic每次把四张图拼成一张训练相当于扩大了单次看到的上下文。但mosaic对胸片有一个副作用四张X光片拼接后病灶区域会被压缩小病灶更难学。如果训练到一半发现val mAP停滞可以关掉mosaic再续训几次有时反而涨点。第三是按需开启rect训练。X光片长宽比很夸张letterbox到640×640会留下大量灰边浪费计算量。YOLOv7训练时加--rect会在每个batch内按相近长宽比分组减少灰边。如果你的数据集图片尺寸差异大这个参数对训练速度和收敛都有帮助如果图片本身就比较统一开不开无所谓。第四是把epochs放到200到300。100个epoch在800张图上看每张图平均只被训练100次加上mosaic的裁剪实际有效迭代不够。我倾向先跑100个epoch看趋势再决定续训到多少而不是一次设满。第五是降低预期。五类肺病检测里新冠和病毒性肺炎在X光片上本来就难分正常肺的框法又可能不统一不要盯着总mAP看分开看每个类别的AP。4.3 训练完先看这四张图混淆矩阵、PR曲线、loss曲线和bad case训练结束不要只看最后一行mAP。打开runs/train/xray/目录里面有YOLOv7自动保存的结果图按下面顺序看。先看混淆矩阵。重点关注covid19和viral_pneumonia这两个类之间的互相串门比例。如果模型把新冠大片区域标成病毒性肺炎不是模型坏了而是X光片本来提供不了足够信息。再看normal类如果normal被识别成各种肺炎的比例很高大概率是正常肺的标签框法有问题回头看第2章的检查。再看PR曲线。每个类单独有一条曲线曲线越贴近右上角越好。正常肺如果曲线形状怪异先检查一下是不是标签里“正常肺”的框既包含双肺又包含心脏区域导致模型没学到稳定特征。loss曲线要看train和val两条线的开口方向。train loss持续下降而val loss在某个epoch后回升是典型过拟合。800张数据过拟合出现在50-80个epoch之间很常见这时候最优checkpoint往往不是最后一个而是val loss最低的那个。YOLOv7会在训练中保留best.pt但如果best是基于val loss选的留意它对应的epoch是否太早。最后挑几张bad case画框看。val批次里预测置信度低于0.3但实际有GT框的图用detect.py重新推理一次看看模型是漏检了还是框偏移了。这一步能直接暴露出标签里没发现的错误比如某张图标注框只框住了病灶的一半模型学到的自然也是“半框”。5. 五类肺病数据集绕不开的五个坑现象、原因与排查5.1 正常肺的“框法”没有统一为什么normal类mAP忽高忽低现象训练过程一切正常但normal类的precision和recall始终有一个偏低PR曲线抖动剧烈val mAP在不同seed下变化很大。原因正常肺在X光片里没有病灶可框作者的标注策略不一致。一部分图打了双肺大框一部分图是空txt还有一部分可能只框了一个肺。模型在“正常肺到底框哪里”这件事上接收到的信号是矛盾的。解决二选一然后全数据集统一。如果你想显式输出“正常肺”检测框就把每张正常图的双肺区域都标上两个单肺框或一个包含双肺的大框都可以但必须统一如果不想标框就把正常图对应的txt全部清空训练目标变成“异常区域检测”推理时无输出即视为正常。两种方案都能跑最怕的是混着来。5.2 新冠与病毒性肺炎在X光片上天然长得像现象混淆矩阵里covid19和viral_pneumonia互相串门两者的AP都低拆开bad case看模型把新冠的磨玻璃影标成了病毒性肺炎把病毒性肺炎的实变标成了细菌性肺炎。原因这是医学影像本身的边界不是模型问题。X光片上新冠早期表现就是磨玻璃影和普通病毒性肺炎几乎没有差异实变影在细菌性和病毒性肺炎里也都会出现。800张X光片样本量扛不起这种病原体级别的区分。解决降级任务。把它当成“正常、细菌性肺炎、结核、其他非细菌性肺炎”四类来评估新冠并入病毒性肺炎。如果一定要区分新冠需要CT切片或临床病史参与单靠胸部X光片的检测模型不现实。强行训练五个类只会得到一个看起来有五个输出、实际两个类靠猜的模型。5.3 标签坐标越界与异常框训练loss震荡的老油条式排查现象训练loss整体下降但震荡幅度大val集上预测框经常扁成一字形或者贴在图边缘。原因标签坐标里混入了越界数据。典型情况是标注工具导出时把像素值当成归一化值或者宽高写成了除以图宽但没有除以图高。解决别急着调超参数先跑第2章的检查脚本。找出所有x_c w 1或y_c h 1的框clip到边界并记录。如果异常框超过5%直接开训会把模型带偏最稳的办法是把这些标签导出成图片画框人工检查一轮。健康的数据集里这类问题应该一个都没有。5.4 同设备、同患者的“数据泄漏”让验证集虚高现象随机划分后val mAP能到0.85拿外部X光片一测只有0.5差距大到不正常。原因划分时没有按患者分组同一患者的正位片和侧位片分别进了train和val更隐蔽的是整个数据集如果来自同一台设备设备自带的影像后处理算法会形成一种“设备伪影指纹”模型学到的可能不是病灶而是这台设备产出的图像风格。解决先按第3章的脚本按患者ID重新划分。如果全部图片来自同一台设备那跨设备的泛化问题是无解的这时模型的真实能力要以外部数据为准而不是以val mAP为准。这个限制要写进项目结论里别等上线再发现。5.5 把检测框当成确诊结论现象demo图上检测框画得很漂亮五类标签齐全于是想直接接进临床系统或体检报告。原因800张图的样本量只能覆盖有限设备、有限人群、有限疾病阶段标注的框边界是标注者的主观判断模型学到的是“标注者认为哪里像病灶”不是病理诊断。解决把模型定位成辅助筛查或科研原型。如果它的输出要进临床流程需要拿本地多设备数据重新验证并走完整的医学软件合规路径。这一步不能省也别想用公开数据集绕过。6. 用best.pt对本地X光片推理一句话命令和两个置信度经验6.1 最小推理命令detect.py与两个阈值参数训练完拿到runs/train/xray/weights/best.pt之后真正的yolov7部署环节反而简单。官方仓库自带的detect.py就是最小推理入口python detect.py \ --weights runs/train/xray/weights/best.pt \ --source ./demo.jpg \ --conf-thres 0.30 \ --iou-thres 0.45 \ --project runs/detect \ --name xray_demo参数说明--conf-thres是置信度阈值YOLOv7默认0.25。医学检测场景我习惯提到0.30以上因为漏检比误检更危险宁可多给一个低置信度框让医生看也不要悄悄丢掉可能是病灶的区域。--iou-thres是NMS的IoU阈值默认0.45。X光片病灶框相对稀疏一般不用动如果你发现同一个病灶总被拆成两个相邻框可以适度提到0.5。6.2 批量推理后先做类别计数而不是只看效果图推理后多数人直接翻开带框的效果图但效果图看不出类别分布是否合理。我遇到过一个场景模型把整批测试图全预测成了“正常”看效果图才发现所有图上根本没有任何框而这批图里有大量肺炎病例。这类问题在类别不平衡时特别容易出现所以批量推理时加上--save-txt再统计一下输出python detect.py \ --weights runs/train/xray/weights/best.pt \ --source ./test_images \ --conf-thres 0.30 \ --save-txt \ --project runs/detect \ --name batch_test统计脚本很短from collections import Counter from pathlib import Path result_labels Path(runs/detect/batch_test/labels) cnt Counter() for txt in result_labels.glob(*.txt): for line in txt.read_text().splitlines(): if line.strip(): cls_id int(line.split()[0]) cnt[cls_id] 1 print(cnt)逻辑说明detect.py会把每个检测结果写到与图片同名的txt里行格式和训练标签一致。统计这些txt里的类别id数量就能快速判断模型是不是在偷懒——比如把一切都预测成正常或者某个类完全没输出。类别id对应关系必须与训练时的classes.txt一致。做医学影像检测这几年我吃过的最大亏都在代码跑通之后没有按患者划分导致验证集虚高、没检查标签导致类别错位、没做类别计数导致模型“空场”还不自知。现在拿到任何数据集我的第一个动作永远是先跑标签统计脚本而不是急着吹环境。这个习惯救过我很多次希望你也能在开工前先踩一遍这套校验流程而不是在训练到一半时才发现数据集的标签黑匣子里藏着一堆坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表