医学图像目标检测数据集的标准化:从肺结节CT到YOLOV5格式
简介面向医学图像目标检测学习与研究者提供肺结节CT图像YOLOV5格式数据集标注为单类别“肺结节”可直接用于模型训练与验证无需额外格式转换。压缩包共499个文件包含248张jpg图像与249个txt标签文件另附类别字典png及可视化脚本py整体仅5.14MB轻量易用。数据严格按YOLOV5目录划分训练集220张图片及对应标签测试集28张图片及对应标签结构清晰适合快速开展目标检测实验或教学演示。配套的Python可视化脚本可随机读取一张图片并绘制边界框保存至当前目录无需修改即可运行极大方便标注质量核查与结果展示。目前已有988人学习下载适合医学图像入门、YOLOV5流程练习及肺结节检测预研场景。1. 医学图像目标检测数据集的隐藏成本格式即标准拿到一批肺结节CT图像原始数据离用YOLOV5训练出自己的模型还有多远很多人把这一步想简单了结果在数据集上耗掉的时间比调模型还长。医学图像领域的数据发布格式基本都不是为深度学习的目标检测接口设计的——DICOM序列的存储与切片方向、标注文件里多放射科医生意见如何合并、世界坐标到像素坐标怎么换算、窗宽窗位在转图片时要不要考虑这些问题不解决哪怕原始数据是公开可下载的也无法直接喂给YOLOV5训练。本文围绕“医学图像之目标检测数据集(YOLOV5目录格式)肺结节CT图像目标检测数据集”这条线讲清楚把CT原始数据加工成YOLOV5能直接训练的标准目录要做哪些事、参数怎么定、坑在哪。希望读到这边的人是不满足于只下载别人打包好的现成数据集而是你手里正好有一批没有整理过的CT影像和结节标注想自己动手做成一份能支撑训练的数据集。整个过程会涉及DICOM与NIfTI的差异、LIDC-IDRI这类公开数据的标注结构、YOLO标注格式的坐标换算以及训练前的质量验证。2. 从原始CT到YOLOV5目录格式肺结节数据集重组的完整流程2.1 先理解医学图像的三种格式差异DICOM、NIfTI与PNG医学影像领域最常遇到的三种数据形态是DICOM、NIfTI和PNG。DICOM是医疗设备直接输出的原始格式一个CT检查往往包含几百张二维切片每一张除了像素数据外还带有患者信息、采集参数、像素间距PixelSpacing、切片厚度等元数据。NIfTI是把整个三维体数据打包成一个文件常见扩展名为.nii或.nii.gz很多公开医学数据集和开源分割工具都采用这种格式处理起来比逐个读DICOM方便。PNG是深度学习领域最常用的图像格式YOLOV5原生的数据加载器直接读取的就是普通图像文件。为什么强调这个差异因为很多肺结节CT数据集原始标注并不直接落在二维PNG上。以LIDC-IDRI为例它提供的标注是XML格式里面记录了四位放射科医生在三维空间里勾画的结节轮廓坐标单位是毫米坐标参考系基于原始CT体数据。你要把它转成YOLOV5能认的txt格式中间必须经过“三维世界坐标 → 像素坐标 → 归一化xywh”这一串换算每一步都有陷阱。另外还有一个隐蔽的问题CT图像的灰度值不是普通的0到255它存储的是亨氏单位HU取值范围通常从-1024到3071。直接把原始值丢给YOLOV5训练多数情况不会马上爆掉但对肺结节这种小目标、低对比度的检测任务来说不做窗宽窗位调整会明显影响模型收敛速度和最终mAP。一般做法是在转PNG时对CT值做截断再用线性映射把目标组织范围拉满。2.1.1 一个容易忽略的问题切片厚度与各向异性肺结节CT数据还有一个三维特性需要处理就是体素不一定是各向同性的。常见胸部CT的层厚是1mm或5mm但在X、Y方向像素间距可能是0.5mm到0.8mm。这意味着Z轴方向的空间分辨率明显低于XY平面。如果原始数据集里混着不同层厚的数据直接统一转成二维图像做检测模型会学到跟层厚相关的伪特征——它在厚层数据上训练出来的特征表现换到薄层数据上可能就不稳定。我一般会在生成数据集时记录每一例的层厚和像素间距方便后续做分层验证。2.2 用Python脚本把DICOM序列批量转换为PNG图像转换这一步的核心是选对库和参数。最常见的做法是用pydicom读取DICOM文件然后通过numpy做窗宽窗位调整最后用opencv或PIL保存为PNG。下面的脚本实现了一个最基础的转换器支持指定窗宽窗位并保留原始HU值。import os import pydicom import numpy as np import cv2 def dicom_to_png(dicom_path, output_path, window_width1500, window_level-600): ds pydicom.dcmread(dicom_path, forceTrue) # 提取像素数组可能是 (rows, cols) 或带帧数 pixel_array ds.pixel_array.astype(np.float32) # 如果存在RescaleSlope/Intercept还原为HU值 slope float(getattr(ds, RescaleSlope, 1)) intercept float(getattr(ds, RescaleIntercept, 0)) hu pixel_array * slope intercept # 窗宽窗位截断 lower window_level - window_width / 2.0 upper window_level window_width / 2.0 hu_clipped np.clip(hu, lower, upper) # 线性映射到0-255并转为8bit hu_scaled ((hu_clipped - lower) / (upper - lower) * 255.0).astype(np.uint8) cv2.imwrite(output_path, hu_scaled) # 示例遍历某个患者文件夹下的所有DICOM切片 dcm_dir case_001/ out_dir case_001_png/ os.makedirs(out_dir, exist_okTrue) for fname in sorted(os.listdir(dcm_dir)): if fname.lower().endswith(.dcm): dicom_to_png(os.path.join(dcm_dir, fname), os.path.join(out_dir, fname.replace(.dcm, .png)))这里的逻辑是先用pydicom读取DICOM文件和元数据再把像素值通过RescaleSlope与RescaleIntercept还原成HU值接着用窗宽窗位截断做对比度增强最终映射到8bit灰度保存为PNG。参数上有两个地方需要根据数据源调整第一window_width和window_level肺部CT一般用1500/-600左右但如果你只关注实性结节可以把窗宽收窄到800左右第二不同厂家的DICOM文件可能在pixel_array读取时返回不同的维度顺序需要确认输出图像的宽高方向与标注坐标一致。2.3 把XML结节标注转换为YOLO格式的txt标签LIDC-IDRI是肺结节CT领域被引用最多的公开数据集它的标注分布在多个XML文件里每个XML对应一个患者的CT检查里面记录了四位放射科医生标注的结节位置和轮廓坐标。这里的核心逻辑是先把医生标注的多边形点坐标从世界坐标系转换到像素坐标系再把多边形转换成外接矩形最后归一化成YOLO需要的中心点坐标和宽高。YOLO的txt标签格式是每行一个目标五个数字依次为类别id、归一化中心x、归一化中心y、归一化宽、归一化高。下面的代码展示了从XML到YOLO标签的转换思路重点看坐标换算的部分。import xml.etree.ElementTree as ET import pydicom import numpy as np def parse_lidc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() nodules [] for ann in root.findall(.//unblindedReadNodule): roi_list [] for roi in ann.findall(.//edgeMap): x float(roi.find(x).text) y float(roi.find(y).text) roi_list.append((x, y)) if roi_list: nodules.append(roi_list) return nodules def roi_to_yolo(roi_points, image_shape, pixel_spacing, origin): # 世界坐标转像素坐标 (x - origin_x) / spacing_x x_pixels [(px - origin[0]) / pixel_spacing[0] for px, py in roi_points] y_pixels [(py - origin[1]) / pixel_spacing[1] for px, py in roi_points] x_min, x_max min(x_pixels), max(x_pixels) y_min, y_max min(y_pixels), max(y_pixels) h_img, w_img image_shape[:2] # 归一化到[0,1]越界的点截断 cx ((x_min x_max) / 2.0) / w_img cy ((y_min y_max) / 2.0) / h_img bw (x_max - x_min) / w_img bh (y_max - y_min) / h_img return [0, cx, cy, bw, bh] # 从DICOM获取图像尺寸与像素间距 ds pydicom.dcmread(case_001/000001.dcm) pixel_spacing ds.PixelSpacing # [x间距, y间距] origin ds.ImagePositionPatient # 世界坐标系原点 # 注意这里假设图像本身不旋转如果CT的ImageOrientationPatient不是默认值需要做旋转校正这段代码的注释里特意保留了一个关键提示如果扫描仪产生的图像带有非默认的ImageOrientationPatient方向单纯用PixelSpacing做除法是不够的还要处理旋转矩阵。多数公开数据集会预处理掉这一点但如果你自己从医院拿原始DICOM这一步不能省。坐标换算完成后把label写入与PNG同名的txt文件YOLOV5目录结构就成型了。2.3.1 四位医生的标注怎么合并LIDC-IDRI的xml数据里同一结节会被多位医生标注标注结果可能不一样有的医生标了但其他医生没标。常见做法是只保留至少两个医生都标注的结节或者把多边形的并集作为最终标签。对目标检测而言取外接矩形会让不同标注差异变得不那么敏感因为矩形框本身的容错空间比分割掩码大。还有一个做法是按结节最大径排序过滤掉小于3mm的微小病灶——很多医生标注的结节其实小于检测阈值保留它们会让正样本里混入大量肉眼难辨的微小目标增加训练难度。2.4 构建最终的YOLOV5目录并划分训练验证集目录结构完全依赖YOLOV5官方datasets的约定即images目录存放PNGlabels目录存放同名txt训练集和验证集分别位于train和val子目录。划分比例一般用8:2或9:1但肺结节数据通常病例数量少一个患者会有几十张切片这会导致一个问题同一个患者的相邻切片极度相似如果随机划分训练集和验证集很可能同一患者的切片同时出现在两边造成数据泄露验证指标虚高。正确做法是按病例划分把同一患者的全部切片整体放入训练或验证集。# 最终目录结构示意 lung_nodule_dataset/ ├── images/ │ ├── train/ │ │ ├── case_001_000001.png │ │ ├── case_001_000002.png │ │ └── ... │ └── val/ │ ├── case_010_000001.png │ └── ... ├── labels/ │ ├── train/ │ │ ├── case_001_000001.txt │ │ ├── case_001_000002.txt │ │ └── ... │ └── val/ │ └── case_010_000001.txt └── lung_nodule.yamllung_nodule.yaml的内容是YOLOV5数据集配置文件指定路径、类别数和类别名。一个典型的配置如下。train: lung_nodule_dataset/images/train val: lung_nodule_dataset/images/val nc: 1 names: [nodule]这里只需要一个类别因为肺结节检测不做良恶性分类。如果你手里的标注还包含结节类型信息比如实性、磨玻璃、部分实性那nc就要相应增加。3. 针对肺结节特点的YOLOV5训练参数调整3.1 模型结构选型为什么肺结节场景不直接上yolov5xYOLOV5系列按网络深度和宽度分为s、m、l、x四个规格。很多初学者觉得模型越大效果越好但肺结节检测有其特殊性结节在CT图像中占比小往往只有几十到几百个像素模型的主要挑战不在于提取高语义特征而在于保留小目标的细节信息。yolov5s的浅层特征图如80×80网格对这个小目标检测已经够用而yolov5x虽然理论精度更高但训练和推理开销大BatchSize会被迫缩小反而影响收敛。我一般会先用yolov5s跑通流程确认数据和标注没问题再根据显存情况和验证集mAP决定是否升级到yolov5m。如果数据量很大且GPU显存充足yolov5m在医学小目标任务上通常比s有2%到4%的mAP提升但训练时间几乎翻倍。3.1.1 预训练权重与骨干网络迁移医学图像和自然图像差异巨大ImageNet预训练权重到底有没有用业界一直有争议。我自己的经验是在数据量少于几千张时加载yolov5s.pt权重比从头训练收敛更快但最终mAP差距不大数据量超过上万张后预训练权重带来的优势几乎消失。实践中我习惯保留预训练权重但把freeze层数调少——YOLOV5默认会冻结部分backbone在医学图像上建议减少冻结层数让backbone也参与更新。3.2 修改yolov5超参数文件mosaic、hsv增强与小目标先验YOLOV5的超参数配置写在hyp.scratch-low.yaml这类文件里训练时通过--hyp参数传入。对肺结节数据集最需要动的是数据增强相关参数和anchor设置。# hyp.scratch-medical.yaml (在yolov5/hyp目录下新建) lr0: 0.01 # 初始学习率 lrf: 0.01 # 最终学习率因子 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3.0 warmup_momentum: 0.8 box: 0.05 # box损失权重 cls: 0.5 # 分类损失权重 hsv_h: 0.015 # 色调增强CT灰度图不需要色彩扰动调低 hsv_s: 0.5 # 饱和度增强同样调低 hsv_v: 0.1 # 明度增强调低 degrees: 0.0 # 旋转角度CT图像长轴方向固定不旋转 translate: 0.1 # 平移 scale: 0.5 # 缩放 fliplr: 0.5 # 水平翻转 mosaic: 1.0 # mosaic增强保持开启 copy_paste: 0.0 # 医学图像不要用copy-paste容易制造伪影这里的关键理解YOLOV5的默认超参数是为自然图像调优的跟CT灰度图像的特性不匹配。hsv类的色彩增强对灰度图基本没意义但开了也不会造成太大问题系统性的隐患在旋转和缩放CT图像有明确的解剖方向大幅旋转会破坏人体结构的方向性先验虽然可以通过flip_lr做镜像增强但不应该使用超过15度的旋转。Mosaic增强对肺结节这种目标稀疏的场景很有用——它把四张图拼成一张训练样本相当于变相增大batch大小也让模型在上下文中学习目标。mosaic在一般任务上是正收益在肺结节上也是。3.3 针对小目标检测调整anchor与网络输入尺寸YOLOV5默认在训练时会根据你的数据集自动计算anchor尺寸。对肺结节而言结节的平均尺寸通常远小于COCO数据集的平均目标尺寸如果使用默认anchor会导致小目标在特征图上没有足够的anchor匹配。有三种做法一是用--noautoanchor关闭自动anchor手动指定适合肺结节的尺寸二是直接用--evolve跑一次anchor进化让YOLOV5按你的数据分布自行搜索三是改输入分辨率把默认的640×640提升到1280×1280但显存消耗会变成四倍。我在实操中倾向于先保留自动anchor因为YOLOV5的autoanchor对数据分布没那么敏感除非你发现大量小目标漏检的情况。真正更值得关注的是输入尺寸与切片尺寸的关系很多CT切片的原始尺寸是512×512用640×640作为训练尺寸意味着图像被放大1.25倍对显存和计算量的代价都不小。把训练尺寸设为512推理尺寸保持同值是小目标场景下性价比最高的选择。3.4 训练命令参考从零训练到输出评估在yolov5目录下执行训练需要传入数据集配置、模型配置、超参数配置、batch大小和epoch数。下面是实测中比较稳定的一组参数。python train.py \ --data lung_nodule.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 200 \ --img 512 \ --hyp hyp.scratch-medical.yaml \ --workers 8 \ --device 0参数含义说明--cfg指定网络结构文件yolov5s.yaml对应的就是s模型--weights指定预训练权重路径--img是训练图像边长YOLOV5内部会按矩形推理自动补边--hyp传入我们调整过的医学图像超参文件--workers是数据加载线程数Windows下如果报错注意把这个值调小到2或4因为Windows的multiprocessing对DataLoader支持不够好。训练结束后模型权重保存在runs/train/exp/weights/best.pt中。在验证集上跑评估的命令是python val.py \ --data lung_nodule.yaml \ --weights runs/train/exp/weights/best.pt \ --img 512 \ --conf-thres 0.001 \ --iou-thres 0.6这里的--conf-thres设得低是为了让更多候选框进入评估流程val脚本内部再做后处理。医学小目标检测任务中我习惯把conf-thres设成0.001而不是默认的0.25否则大量低置信度的真阳性会被直接过滤掉mAP会被低估。4. 训练中的三个典型故障与排查方法4.1 loss输出为nan标注坐标越界或图片出现全黑肺结节数据集发生lossnan的情况不多一旦发生十有八九是标签坐标越界。原因在于标注坐标经历了两次转换一次是毫米到像素一次是像素到归一化任何一次缺少像素间距信息或读取了错误的spacing都会让归一化后的xywh大于1或小于0。检查手段很直接统计labels目录下所有txt文件看每个文件中六个数值的分布范围。import os label_dir labels/train out_of_range 0 total 0 for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.split() if len(parts) ! 5: print(f格式错误: {fname}) continue cls int(parts[0]) vals [float(v) for v in parts[1:]] total 1 if cls 0 or any(v 0 or v 1 for v in vals): out_of_range 1 print(f越界: {fname} - {line}) print(f总标签数: {total}, 越界标签数: {out_of_range})这段检查脚本在训练前跑一次能在几分钟内确认标签是否存在越界或格式错误。还有一个可能被忽略的原因某些CT切片转成PNG后是全黑的因为窗宽窗位范围没含住任何值正常的组织比如把窗宽设到了0。这种情况下模型读入的是无效特征也会导致loss异常。检查方法是对每个PNG计算像素均值和标准差找出可能是空图像的样本直接剔除。4.2 正负样本不均衡小目标检测中单纯的背景过拟合肺结节检测相对其他医学任务来说正负样本不均衡问题不算极端但结节尺寸小导致可学习的特征有限模型容易把注意力放在背景结构上表现在验证集上就是查全率低但查准率高。对这种问题用focal loss不一定有效因为YOLOV5的损失本身对样本不均衡有处理。我更推荐两个方向一是增加正样本的裁剪块作为输入让模型对结节形态的观察更密集二是调整mosaic策略而不要调loss权重——mosaic增强天然增加了每张训练图中的有效目标数量对正样本稀缺场景有明显帮助。在超参数层面还可以通过增大--batch-size来缓解梯度噪声小batch下背景梯度占主导模型更新方向容易偏离小目标的最优解。如果显存不够先降分辨率而不是降batch。4.3 验证集上mAP高但实际效果差标注噪声比对医学影像标注的主观性比自然图像强得多尤其是磨玻璃结节这种边缘模糊的病灶不同医生的标注可能差别很大。当验证集mAP很高但推理时对真实数据表现差最常见的原因是验证集里存在标注偏差模型学会了拟合某一医生的标注风格。排查方式是把预测框和标注框同时画在原图上逐个看预测和标注的吻合程度如果出现系统性偏移比如预测框普遍偏大或偏小那大概率是标注标准不一致。缓解手段是把多位医生标注的外接矩形做交集或加权融合生成更稳定的ground truth。5. 一个必做的数据集质量验证技巧几何指标与可视化叠加在投入训练之前用可视化叠加和几何指标对数据集做体检能省下后面几轮无效训练的时间。我每次构建完一个新的肺结节数据集都会跑一遍下面的脚本检查标注框是否贴合物体的真实边界。import cv2 import numpy as np import os def draw_boxes(image_path, label_path, output_path): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.split() if len(parts) ! 5: continue _, cx, cy, bw, bh map(float, parts) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) # 避免边界框超出图像范围导致绘制异常 x1, y1 max(x1, 0), max(y1, 0) x2, y2 min(x2, w - 1), min(y2, h - 1) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) # 在框内填充透明度方便观察框范围是否覆盖完整结节 overlay img.copy() cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 255, 0), -1) img cv2.addWeighted(overlay, 0.15, img, 0.85, 0) cv2.imwrite(output_path, img)这段代码的原理很简单用opencv读取图片和标签按归一化坐标反算像素坐标画红色矩形框并做半透明绿色填充。运行后随机抽20到30张检查图片重点看三类问题一是框比实际结节大很多说明外接矩形计算或坐标换算有问题二是框的中心点不与结节中心对齐常见于多医生标注合并时采用并集策略产生偏移三是图像本身过暗或过亮导致结节区域无法辨认此时需要调整窗宽窗位重新生成PNG。几何指标检查则更精确一些。计算所有标注框的平均面积占整张图像面积的比例如果这个值低于0.5%说明你的数据集以小目标为主YOLOV5网络默认的接收野和anchor设置可能需要额外调整可以在训练参数中开启--multi-scale或者在推理阶段用切图每张图切为四个子图分别推理再合并结果的方式提升小目标检出率。另一个指标是标注框长宽比分布如果长宽比极端偏向某一方向说明切片采样可能存在方向偏差考虑在数据生成时增加冠状位和矢状位的重建切片。做完可视化叠加和几何指标检查确认数据质量没问题再进train.py训练你后面遇到的大部分模型性能问题都能归因到模型和超参数本身而不是被数据集里的隐蔽bug带偏排查方向。本文还有配套的精品资源点击获取