ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

LIDC-IDRI肺结节CT数据集详解:从DICOM解析到pylidc与预处理实战

LIDC-IDRI肺结节CT数据集详解:从DICOM解析到pylidc与预处理实战 简介这一工具包专为LIDC-IDRI肺结节CT数据集设计面向医学影像研究人员与算法开发者用于高效提取、转换和分析肺结节病灶的CT图像及专家标注信息。压缩包共24个文件以MATLAB脚本8个m为主附带文本说明、Perl脚本与Shell脚本等辅助文件整体仅182KB。脚本覆盖XML注解解析、掩模与真实图像生成、像素映射转换、大数据拆分、文件批量查找及路径修正等关键环节可显著降低处理LIDC数据集的入门门槛。资源已有2940人学习适合从事肺结节检测、分割、特征提取及机器学习算法评估的中高级研究人员参考使用。通过该工具包用户能直接复用现成的MATLAB函数完成从原始CT到可分析标注的完整流水线节省自行编写解析与预处理代码的时间并借助清晰的许可与说明文档快速上手。 第一次解压完从TCIA下载的LIDC-IDRI数据集我盯着满屏幕的DICOM文件夹和XML文件愣了很久。这个被无数肺结节深度学习论文引用过的公开数据集原始形态远没有论文里写得那么友好——一堆编号杂乱的子目录、以二进制结构存储的CT影像序列以及一套完全独立的XML标注体系三者之间的对应关系全靠自己摸索。如果不借助工具包光是读懂标注这一关就能劝退一大半新手。这篇文章把我这两年用LIDC数据集做肺结节检测和分割的经验整理出来核心回答两个问题数据集本身该怎么理解以及有哪些工具包能把这套流程从地狱难度降到普通人可操作。文中涉及的代码和踩坑记录都是我在实际项目中验证过的。1. 拿到LIDC压缩包后第一步该做什么1.1 先搞清楚数据集的原始形态LIDC-IDRI全称是Lung Image Database Consortium and Image Database Resource Initiative是目前公开可获取的规模最大、标注最完整的肺结节CT数据集之一。它包含约1018个病例的CT扫描累计影像大概在24万张以上。每个病例除了原始的DICOM影像外还会附带一个XML标注文件——这才是这个数据集最有价值也最难啃的部分。从数据组织看TCIA下载回来的是一个很大的manifest目录里面有LIDC-IDRI-0001到LIDC-IDRI-1018这样的子目录每个子目录里又按检查日期和CT序列拆成更小的文件夹。很多第一次接触的人在这里就绕晕了一个病例可能会包含多个CT序列比如薄层1mm扫描和厚层3mm扫描会同时存在如果不看Series Description或Series Instance UID很容易把不同序列当成同一个数据。建议下载后第一时间给每个病例建立索引表记录patient_id、序列UID、层厚、像素间距等信息这会在后面对齐标注时省下大量时间。1.2 标注文件里到底写了什么XML标注不是简单的一个结节列表而是四位经验丰富的胸部放射科医生各自独立标注的结果。每一位医生的反馈都完整保存他画了哪些结节、每个结节用什么轮廓表示、钙化程度怎么样、毛刺和分叶特征如何打分、恶性程度评几分全部一一对应放好。具体到结节定义LIDC把标注对象分成三类直径≥3mm的结节标注者要给出完整的边界轮廓这类结节构成绝大多数深度学习实验的ground truth直径3mm的小结节只记录一个粗略的质心位置没有完整轮廓一般当作定位信息使用还有一类是非结节指疑似结节但医生最后判断不是结节的区域。很多论文在做检测训练时会把这第三类作为难例负样本纳入训练因为和真实结节在形态上非常接近能有效降低误检率。这里要特别提醒同一个结节的标注可能多达四份四份轮廓往往不完全重合。这是由标注者的主观判断差异造成的也是后续使用pylidc工具包时不可避免的问题。2. pylidc把LIDC的标注变成代码的工具2.1 安装、初始化与目录关联pylidc是专门为LIDC-IDRI标注数据编写的Python库作用可以理解为XML翻译官——它会扫描你下载好的原始目录把XML里的所有标注信息解析出来存成一个轻量级查询接口然后你就能用Python对象的方式直接操作。安装非常直接pip install pylidc注意pylidc只是解析工具不包含原始CT数据所以TCIA上下载的DICOM文件仍然需要提前就位。第一次使用前要做目录关联把数据目录指给pylidc最简单的方式是把下载并解压后的manifest文件夹放到pylidc默认的数据路径下或者在代码里通过环境变量、全局变量指向数据位置。由于不同版本的pylidc对这个细节的处理略有差异建议用的时候先看一下当前版本的说明文档配置完成后执行一次查询能正常返回结果再开始下一步。2.2 核心API从Scan到Nodule再到轮廓pylidc的使用逻辑很清晰一句话总结就是Scan代表一个CT序列Annotation代表一位医生的一次标注Nodule代表多个医生标注聚类后的一个结节对象。我用得最多的组合是这样import numpy as np import pylidc from pylidc.utils import query scans query(patient_idLIDC-IDRI-0001) scan scans[0] # 一个Scan对象 vol, seg scan.load_to_volume() # 体数据 肺实质分割结果 for nodule in scan.cluster_annotations(): if len(nodule) 2: continue # 过滤掉只有一个医生标注的弱标签 print(结节体积:, nodule.volume, mm^3) for ann in nodule: for contour in ann.contours: # 取出轮廓点的x/y/z坐标实际属性名以当前版本文档为准 xs np.array(contour.x) ys np.array(contour.y) zs np.array(contour.z) # 用这些坐标去画切片掩码scan.load_to_volume()返回一个元组第一个是CT体数据通常shape为(切片数, 行, 列)第二个是同样尺寸的肺实质分割结果。pylidc会把DICOM的原始像素值转换成适合显示的HU数值这一点对不熟悉医学影像读数的开发者非常友好。2.3 版本兼容性这个真实的坑pylidc是典型的老牌工具库功能强但更新慢最大的隐患是和最新科学计算库的兼容性。我初学时在numpy 1.21环境下直接调用load_to_volume遇到np.double被移除导致的报错。这类错误网上讨论很多解决办法也简单要么把numpy降级到1.19以下要么手动把pylidc源码中涉及np.double、np.float的位置改成np.float64。我偏向后者因为项目里其它依赖通常不允许随意降级numpy。另外一个常见问题是pylidc默认会把解析出来的标注缓存到本地数据库中。如果数据目录很大首次构建索引会非常慢看起来像卡死你只需要耐心等它跑完后续查询就会很快。这一步也建议在训练集准备之前一次性完成避免中途反复扫描。3. 从DICOM到训练样本CT预处理流水线搭建3.1 HU值转换与窗宽窗位PACS系统里存下来的DICOM像素值基本都不是真实的CT值。读取时需要使用标签中的RescaleSlope和RescaleIntercept按公式HU pixel × slope intercept做转换得到的才是一张CT影像里真正有物理意义的密度值。肺结节的密度分布和骨头、软组织差异很大统一转成HU后模型才能学到可迁移的特征。同样重要的是窗宽窗位。肺结节的AI任务几乎都会用肺窗来观察常规设置是窗宽1500HU、窗位-500HU到-550HU。简单解释就是窗宽决定对比度范围窗位决定显示的中心值通过WindowLevel和WindowWidth对HU值做截断映射能把肺实质里细微的结节病变从背景里拉出来。pylidc的load_to_volume默认已经做了一部分类似处理但如果你用pydicom自己写读取管线这一步就不能省。3.2 体素重采样和坐标体系不同医院的CT扫描层厚不一即便LIDC内部也存在1mm、1.5mm、3mm等多种层厚。如果直接把这些数据丢进3D神经网络不同体素间距会让模型看到的病灶形状严重失真。我习惯用SimpleITK把扫描统一重采样到1mm×1mm×1mm的各向同性体素import SimpleITK as sitk image sitk.ReadImage(dicom文件夹路径) image sitk.Cast(image, sitk.sitkFloat32) spacing image.GetSpacing() size image.GetSize() new_spacing [1.0, 1.0, 1.0] new_size [ int(round(size[0] * spacing[0] / new_spacing[0])), int(round(size[1] * spacing[1] / new_spacing[1])), int(round(size[2] * spacing[2] / new_spacing[2])), ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(new_spacing) resampler.SetSize(new_size) resampler.SetInterpolator(sitk.sitkLinear) resampled resampler.Execute(image)重采样之后最重要的事是把标注坐标一起变换过去。DICOM的坐标系是LPS左右、前后、上下而numpy数组的index顺序通常和DICOM的轴向并不一致从pylidc拿到的轮廓坐标必须按相同的重采样比例进行缩放同时在轴向映射上做对应翻转或换轴。坐标对不上是所有预处理bug里最隐性的一种模型训练不会报错但所有预测结果在临床解释上全是错的。3.3 生成结节掩码与2D切片导出有了轮廓坐标生成掩码就简单了。对每个二维切片用skimage.draw.polygon把多边形内部填成1再放到3D矩阵对应的Z层上即可。如果目标是训练2D检测器比如用yolov8做切片级别的结节检测还需要把3D掩码映射到每一张2D切片上导出的同时记录边界框。我一般的流程是先对每个结节生成一个固定尺寸的3D patch例如32×32×32像素中心对齐结节质心训练3D网络时使用的是这些patch而不是整张CT图既能控制显存占用也能让模型聚焦病灶区域。这些patch和对应的3D掩码可以保存成npy或NIfTI格式后续训练3D分割网络时直接使用。4. 用工具包之前就要想清楚的几个事4.1 数据划分必须以患者为单位LIDC的1018个病例里很多病人不止一个CT序列。如果你简单按序列或扫描文件去划分训练集和测试集同一个患者的薄层扫描很可能同时出现在两边模型表面验证指标虚高一到外部数据就崩。正确做法是先提取全部patient_id去重再以患者为最小单位划分训练、验证、测试集。从query中拿到全部scan对象后可以用set收集唯一的patient_id然后做随机划分。这一点看似简单但在实际项目里仍然是最容易被忽视的问题。我在陪跑多个新手项目时发现不少同学是先看到数据集目录结构自然而然按文件夹划分了最后结果还很漂亮实际上全是过拟合的假象。4.2 标注者一致性阈值怎么选才算严谨四位医生的标注各不相同pylidc的cluster_annotations会把空间上重合的标注自动聚合成一个Nodule对象但具体要用至少几位医生同意的结节作为正样本需要你自己定。下表是我常用的一致性阈值参考一致性阈值正样本数量趋势标签质量适用任务至少1名医生最多含单医生主观噪声通用预训练、样本量优先至少2名医生较多噪声明显下降检测与初筛至少3名医生中等较稳定分割与多中心迁移4名医生全部一致很少最干净但样本稀缺小样本高精度评估官方口径下至少一名医生标注为≥3mm的结节大约有2669个如果收敛到至少3名医生一致数量会明显下降但很多论文宁可牺牲数量也要换标注质量尤其是做分割任务时。我的建议是检测任务可以先从至少2名医生一致起步把数据准备成多份版本训练后对比不同阈值下的指标波动。这个波动本身就能反映标注噪声对任务的影响程度。4.3 恶性程度评分怎么用XML里每个标注还带有恶性程度评分范围1到5分数越高恶性可能性越大。pylidc对单个Nodule对象暴露了malignancy属性取到的是几位医生评分的一个列表。如果你做分类任务常见的两种做法是把评分映射成二分类1-2为良性、4-5为恶性、3剔除或者直接在1到5的整数上做回归。我的经验是不要轻易把评分3的样本强行归到良性或者恶性因为医生在犹豫不决的病灶上给的边界评分恰恰包含了对临床诊断最有价值的模糊信息。把评分3单独作为一个轻度可疑类别或者做排序任务常常比硬编码二分类得到更稳定的模型。5. 把工具包组合起来才是完整方案5.1 肺实质分割工具的联动pylidc自带的load_to_volume虽然会返回一个肺实质分割结果但这个分割质量比较基础复杂病例容易出现漏分割。我在实际项目中会再叠加一个专门的肺实质分割模型比如基于nnU-Net训练的开源工具或者TotalSegmentator。先切出肺实质区域再在这个区域内做结节检测能去掉一大片胸壁、肋骨、血管带来的误检干扰模型收敛速度和最终精度都会有可感知的提升。5.2 从LIDC到外部数据集的迁移LIDC确实很适合做预训练和baseline但要清醒地看到它的局限数据来源集中、标注者人数有限、设备型号相对固定。我在把LIDC训练的模型迁移到自己合作医院的CT数据时发现直接fine-tune的效果一般需要重新做窗宽窗位和数据分布对齐。另一个技巧是在LIDC上训练阶段就刻意保留一部分层厚不均的样本让模型在体素间距变化的输入上更鲁棒这比只喂各向同性重采样数据效果好。5.3 一点个人经验总结最后说点真正的经验教训。工具包本质上只能帮你读取和组织数据不能替你去理解数据里的医学语义。pylidc用熟之后建议找几个病例把轮廓坐标画在DICOM切片上肉眼检查一遍感受一下不同医生画法的差异这会直接影响你怎么设阈值、怎么定loss、怎么做数据增强。我后来做肺结节AI项目的绝大部分方法论调整都不是从哪篇SOTA论文里抄来的而是在这种最朴素的看数据过程里产生的。本文还有配套的精品资源点击获取
返回列表