拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LIDC-IDRI肺结节数据集完全解析:从XML标注到坐标转换实战

简介面向Python医学影像与机器学习方向开发者这份资料围绕LIDC肺部CT数据集整理出完整的数据说明与标注信息可解决研究者理解LIDC-IDRI数据结构、读取XML/CSV标注与开展肺结节检测任务的入门难题。包内共1323个文件以1319个XML标注文件为主同时包含CSV、XLS、XLSX格式的元数据表和说明文档压缩包整体6.44MB内容精简便于快速查阅XML中记录结节坐标、直径与形状表格则汇总病人ID、扫描序列与放射科医生标注等字段。目前已有280人学习下载适合需要在Python中使用pydicom、pandas、lxml等库读取DICOM图像与注释信息的开发者。借助这份数据说明可省去逐份整理原数据集注释的时间更快进入结节检测、分割和特征提取环节并据此构建机器学习或深度学习模型用于肺癌早期识别实验。 第一次拿到LIDC数据集的人十有八九会愣住一堆.dcm文件配一个.xml文件名全是几十位的数字和点号翻遍网上资料也只能找到几句“这是肺结节公开数据集”这种车轱辘话。我当年啃这个数据集的时候光搞明白标签的坐标怎么对齐到CT图像上就折腾了快一周。所以今天这篇文章就专门把LIDC的数据说明和标签体系从头到尾捋清楚从文件结构到标注含义从坐标转换到实际解析代码写给所有要拿这个数据集做实验的朋友。LIDC能解决的问题很明确它是目前医学影像AI领域用得最多的肺结节公开数据集CT图像和放射科医生的标注都在里面训练结节检测、分割、分类模型都离不开它。适合谁看不管是做毕业设计的学生、刚入门医学影像的算法工程师还是想复现论文的老手这篇文章都能帮你省下踩坑的时间。1. LIDC-IDRI是什么一个数据集的来龙去脉1.1 数据集的诞生与定位LIDC的全称是Lung Image Database Consortium国内一般叫“肺图像数据库联盟”。它由美国国家癌症研究所NCI牵头联合多家学术机构共同建立后来和IDRIImage Database Resource Initiative合并所以完整的名字是LIDC-IDRI。这个项目从2000年左右开始启动目标很朴素给肺结节计算机辅助诊断研究搞一套带金标准标注的公开数据。当时做CAD计算机辅助诊断的人很痛苦各家医院数据格式五花八门标注也是各标各的论文里的结果根本没法横向对比。LIDC-IDRI就是奔着解决这个问题去的八个机构参与收集数据四个放射科医生独立标注最后形成一套公开可获取的参考标准。现在的医学影像AI论文只要是做肺结节的几乎都会在LIDC上跑一遍实验说它是这个领域的“ImageNet”也不过分。1.2 数据规模与获取方式LIDC-IDRI一共包含1018例胸部CT检查覆盖了7家医疗机构图像总量超过12万张完整下载大概200GB出头。这里面不仅包含CT图像还包含对应XML格式的标注文件标注内容由四位经验丰富的胸片放射科医生分两阶段完成第一阶段每个人独立标注第二阶段大家一起审阅自己的标注结果并允许修改。注意一个细节LIDC的1018例来自不同机构、不同厂商的CT设备扫描参数差异挺大。有的序列层厚是1.25mm有的是2.5mm重建算法也不一样。这意味着直接拿原始数据训练模型会天然面临域偏移问题。后面做数据预处理的时候统一重采样到各向同性分辨率是常规操作。获取方式上LIDC-IDRI已经托管在TCIAThe Cancer Imaging Archive上免费注册申请下载即可。下载的时候会看到两种文件组织方式一种是“DOI”版本按病例打包好另一种是原始的批次目录。建议直接下DOI版本文件组织更规整省得自己整理目录。2. 数据文件长什么样从目录结构到XML说明2.1 目录组织与UID匹配规则下载完解压之后你会发现每个病例是一个文件夹文件夹名是一个很长的数字字符串——比如1.3.6.1.4.1.14519.5.2.1.6279.6001.298806943288235819492599874324。这就是DICOM标准里的SeriesInstanceUID可以理解为这个CT序列在全世界范围内的唯一身份证号。文件夹里混着两类文件后缀.dcm的是CT图像切片一个文件夹少则一两百张多则五六百张与文件夹同名的.xml文件就是四位医生打的标注。这个命名规则里藏着第一个坑XML文件名里的UID必须和DICOM文件的SeriesInstanceUID一致但有的镜像站给出的XML文件名和目录名不完全对应。所以做批处理的时候不要只看文件名要读DICOM头里的SeriesInstanceUID标签来关联XML。另一个坑是有的病例存在多个序列。同一个患者可能同时扫了薄层和厚层或者平扫和增强一个病例文件夹下可能出现多个子目录。XML文件对应的到底是哪个序列需要看XML里的SeriesInstanceUID字段。如果忽略这一步直接拿XML里的坐标去匹配别的序列出来的结果一定是一团糟。2.2 XML标注文件的内部结构XML文件本质上是一个树状结构根节点叫ReadSession下面有多个ReadingSession每个医生的一次标注读取会话是一个。一个典型的XML结构简化后是这样的?xml version1.0 encodingUTF-8? ReadSession ReadingSession annotationVersion1/annotationVersion servicingRadiologistIDP_0_0_4_1-1/servicingRadiologistID source0/source unblindedReadNodule nodule roi imageSOP_UID1.3.6.1.4.1.14519.5.2.1.6279.6001.496905230862142391364951038276/imageSOP_UID inclusionTRUE/inclusion edgeMap xCoord1.23/xCoord yCoord4.56/yCoord zCoord-120.5/zCoord /edgeMap edgeMap xCoord2.34/xCoord yCoord5.67/yCoord zCoord-120.5/zCoord /edgeMap /roi /nodule characteristics subtlety5/subtlety internalStructure1/internalStructure calcification6/calcification sphericity5/sphericity margin5/margin lobulation5/lobulation spiculation1/spiculation texture5/texture malignancy5/malignancy /characteristics /unblindedReadNodule unblindedReadNonNodule nonNodule imageSOP_UID... /imageSOP_UID /nonNodule /unblindedReadNonNodule /ReadingSession /ReadSession里面最关键的几个节点unblindedReadNodule一个完整标注的结节包含nodule和characteristics两部分。nodule下的roi结节在CT图像上的轮廓区域每个roi对应一个CT层面的轮廓多边形里面是一串edgeMap坐标点。characteristics九项特征评分这是标签体系的核心。unblindedReadNonNodule标注为非结节的组织区域只有imageSOP_UID没有轮廓和评分通常用于“这地方像结节但实际不是”的负样本。XML在解析时要注意大小写和节点层级ReadingSession下可能有多个unblindedReadNodule而一个结节又可能跨越多个roi。如果只取了第一个roi结节的空间信息就丢了。3. 标签体系拆解结节、非结节与9项特征评分3.1 结节与非结节的标注方式LIDC的标签分成两大类结节和非结节。听起来简单但内部逻辑值得细说。结节的定义是“直径大于等于3mm的圆形或类圆形不透光区域”每个结节由1到4位医生独立标注同一个结节可以有多位医生的标注结果。每位医生可能在不同的读取会话中发现不同的结节所以XML里会出现多个unblindedReadNodule节点。非结节则是指那些“被医生注意到、但判断不是结节”的结构比如血管断面、胸膜增厚、淋巴结等。非结节没有轮廓坐标只有一个SOP实例UID指向它所在的CT切片。很多初学的人会把非结节当成负样本直接丢进分类器但这样做要小心非结节只包含“像结节但不是”的样本它不包含完全正常的区域直接当负样本会在数据分布上产生偏差。还有一类特殊情况是“直径小于3mm的结节”LIDC也记录但不在完整标注范围内只在XML的某些版本中作为blindedReadNodule或nonNodule出现。这类数据一般不建议用来训练检测模型因为标注信息不完整。3.2 9项特征评分的含义与使用建议这是LIDC标签体系最有价值、也最容易被忽略的部分。每个被完整标注的结节都有9个特征字段每个字段取值1到5钙化比较特殊取1到6具体含义如下字段中文含义评分含义subtlety显著性1极不明显5非常明显internalStructure内部结构1软组织2液体3脂肪4空气5钙化calcification钙化程度1无钙化6广泛钙化sphericity球形度1极不规则5近似球形margin边缘1边缘模糊5边缘锐利lobulation分叶程度1无分叶5明显分叶spiculation毛刺程度1无毛刺5明显毛刺texture纹理1非实性5实性malignancy恶性程度1高度怀疑良性5高度怀疑恶性使用建议方面先别急着把这九项全当标签用。不同任务的标签选择策略差异很大做良恶性分类最常用的就是malignancy。但注意这个评分是影像学的主观评估不是病理金标准所以很多论文会把评分1-2视为良性、4-5视为恶性、3分舍弃或者用多位医生的平均分来减少个体偏差。做结节检测malignancy就不重要了只需要知道结节的空间位置所有被至少一位医生标注为结节的roi都是正样本。做结节分割需要把多个医生的roi轮廓做融合常见做法是先按重叠度分组再取所有医生轮廓的并集或投票结果直接用单个医生的轮廓当ground truth会产生标注偏差。做特征预测任务比如预测毛刺程度可以把多位医生的评分取平均作为回归目标或者用majority vote作为分类目标。但要注意部分结节的评分字段可能缺失处理时要做好过滤。3.3 坐标系统世界坐标到像素坐标这个部分是整个数据集里最绕的地方值得多花点篇幅。XML里edgeMap的xCoord、yCoord、zCoord是患者坐标系下的三维坐标单位是毫米。患者坐标系的原点、方向由DICOM文件头里的ImagePositionPatient和ImageOrientationPatient决定。CT扫描时每个切片的像素坐标都能通过这两个标签映射到患者坐标反过来也能把患者坐标映射回像素坐标。以最常见的轴位CT为例假设某张切片的DICOM头信息如下ImagePositionPatient (-160.0, -160.0, -250.0)这个值是切片左上角像素在患者坐标系中的位置通常长这样。PixelSpacing (0.7, 0.7)像素在x和y方向的实际物理尺寸单位也是毫米两个值通常相等。切片矩阵大小为512x512。那么XML里一个世界坐标点(x, y, z)换算到这张切片上的像素行列坐标是col round((x - ImagePositionPatient[0]) / PixelSpacing[0]) row round((y - ImagePositionPatient[1]) / PixelSpacing[1])z坐标用来定位是哪一层的切片。找到z值接近ImagePositionPatient[2]的那张DICOM再用上面的公式换算x和y。看起来简单实际操作中有三个反复出现的坑。第一个坑是方向。DICOM的坐标系遵循LPSLeft-Posterior-Superior约定x轴指向患者左侧y轴指向背部z轴指向头部。如果你直接用上面公式得到的row是从图像顶部往下数的行号col是从左往右数的列号在普通轴位图像上和直观感受一致。但如果遇到矢状位或冠状位重建序列或者图像被旋转过公式就不灵了必须结合ImageOrientationPatient算方向余弦矩阵把世界坐标投影到图像平面。做LIDC这种轴位为主的CT直接用简化公式问题不大但代码里最好还是写通用版本。第二个坑是层间距不一致。LIDC里有些序列层间距是均匀的有些不是中间可能缺层或重叠。不能直接用z坐标除以层厚来数第几张切片正确做法是把所有切片的ImagePositionPatient[2]收集起来找一个和目标z最接近的切片。第三个坑是edgeMap的坐标不一定落在某一层的平面上。有些XML里同一层roi的所有点z坐标是一致的但有时极个别点会有微小偏差。处理时建议对z四舍五入或者取该roi所有点的中位z来确定切片索引。4. 实操解析用Python把LIDC标签读出来4.1 方案一pylidc一行读取对于不想自己造轮子的人来说直接上pylidc库是最省事的方案。这个库把LIDC的XML解析、坐标转换、可视化全都封装好了安装也简单pip install pylidc使用示例import pylidc # 按病例ID查找所有扫描 scans pylidc.query(pylidc.Scan).filter( pylidc.Scan.patient_id LIDC-IDRI-0001 ).all() scan scans[0] print(scan) # 显示患者、扫描信息 print(scan.slice_thickness, scan.pixel_spacing) # 获取所有完整的结节标注 nodules scan.cluster_annotations() print(f该病例共有 {len(nodules)} 个结节) # 取第一个结节的标注 anns nodules[0] # 这个结节的所有医生标注是Annotation对象列表 for ann in anns: print(ann.malignancy, ann.spiculation, ann.texture) bbox ann.bbox() # 返回 (x_start, x_end, y_start, y_end, z_start, z_end) print(bbox)cluster_annotations()会自动把不同医生对同一结节的标注聚在一起返回的是一个嵌套列表外层是结节内层是同一个结节的多位医生标注。这样就不用手动做标注匹配了。4.2 方案二手写XML解析器如果不想引入第三方库或者希望完全掌控解析逻辑用手写XML解析也很快。这里给一个最小可用的示例用标准库xml.etree.ElementTree来做import xml.etree.ElementTree as ET from collections import defaultdict def parse_lidc_xml(xml_path): 从LIDC XML中提取所有结节的轮廓和特征评分 tree ET.parse(xml_path) root tree.getroot() nodules [] for session in root.findall(ReadingSession): for nodule_elem in session.findall(unblindedReadNodule): # 特征评分 char nodule_elem.find(characteristics) characteristics {} if char is not None: for field in [ subtlety, internalStructure, calcification, sphericity, margin, lobulation, spiculation, texture, malignancy ]: characteristics[field] int(char.findtext(field, 0)) # 轮廓点集合每个roi对应一个CT切片的轮廓 nodule_node nodule_elem.find(nodule) rois [] if nodule_node is not None: for roi in nodule_node.findall(roi): sop_uid roi.findtext(imageSOP_UID) edge_points [] for edge in roi.findall(edgeMap): x float(edge.findtext(xCoord)) y float(edge.findtext(yCoord)) z float(edge.findtext(zCoord)) edge_points.append((x, y, z)) rois.append({ sop_uid: sop_uid, edge_points: edge_points, }) nodules.append({ characteristics: characteristics, rois: rois, }) return nodules这个函数返回每个结节的轮廓和评分后续要转成像素坐标再结合DICOM头信息处理即可。4.3 可视化验证把标注画在CT切片上解析完的标签第一件事不是直接训练而是可视化验证。画出来看看轮廓到底对不对能提前发现很多坐标转换的问题。用pydicom读DICOM用matplotlib画图import pydicom import numpy as np import matplotlib.pyplot as plt def draw_annotation_on_slice(dicom_path, edge_points, position, spacing): 把世界坐标轮廓投影到DICOM切片上并显示 ds pydicom.dcmread(dicom_path) image ds.pixel_array.astype(np.float32) # 窗宽窗位 image (image - 0.5 * (ds.WindowCenter ds.WindowWidth)) image image / ds.WindowWidth * 255 image np.clip(image, 0, 255) # 世界坐标转像素坐标 rows, cols [], [] for x, y, z in edge_points: col round((x - position[0]) / spacing[0]) row round((y - position[1]) / spacing[1]) rows.append(row) cols.append(col) plt.figure(figsize(6, 6)) plt.imshow(image, cmapgray) plt.plot(cols, rows, r-, linewidth2) plt.axis(off) plt.show()这里position就是DICOM的ImagePositionPatientspacing是PixelSpacing。画出来的轮廓如果和图像上的结节边缘基本吻合说明坐标转换没问题如果轮廓明显偏到一侧或者旋转了先检查方向余弦矩阵。5. 常见问题与避坑实录5.1 数据处理中踩过的坑LIDC我用过几轮每次都有新教训。把印象最深的几个问题整理成表格按发生频率排序问题现象原因与解决方案XML文件名和DICOM目录不匹配程序报找不到文件不要依赖文件名匹配读DICOM头里的SeriesInstanceUID做关联坐标偏一个方向轮廓整体偏移忘了处理ImagePositionPatient或者PixelSpacing赋值反了z坐标对应错切片轮廓出现在错误层面层间距不均匀不能用索引直接算要逐个找最近的SlicePosition同一个结节有多套标注结果不一致检测任务用聚类后的并集分类任务取平均值或投票特征评分有缺失数据突然变少评分缺失时pylidc返回0或NaN按需过滤非结节混入模型误检增多区分unblindedReadNodule和unblindedReadNonNodule别把非结节当正样本层厚不一致分割结果差训练前做体素重采样到1mm或0.75mm各向同性多序列混乱坐标对不上图像确认XML对应的序列不是每个病例只有一个序列小结节丢失检测召回率低部分结节没有完整轮廓标注用聚类最小包围盒兜底计算资源不够训练太慢剪裁到肺实质区域弃掉背景slice能省接近一半计算量这十个坑里面最要命的是多序列混乱。有个病例文件夹里面有两个序列一个层厚1.25mm一个层厚2.5mmXML标注对应的是薄层那个。我当时图省事直接用了厚层序列做训练坐标全部对不上花了整整两天才发现问题。现在我的处理流程第一件事就是打印出所有序列的SeriesInstanceUID和XML里的UID对一遍。5.2 关于标签一致性的经验LIDC的标注由四位医生独立完成这本身就决定了标签不是“唯一真理”而是一个带有主观性的集合。同一个结节A医生可能给恶性程度2分B医生给4分这不是数据错误而是真实临床中放射科医生也会存在的判断差异。做实验时怎么处理这种差异直接影响模型效果和论文结论。我的建议是分场景区别对待检测任务中只要有一位医生标注了就纳入正样本不要求所有医生意见一致分割任务中取所有医生轮廓的像素级并集这样可以减少漏分割良恶性分类任务中取评分平均值作为回归目标或者把多次评分一致或平均分明确的样本作为分类训练集平均分落在2.5到3.5之间的样本果断扔掉。另外一个小技巧是多中心数据的标注和影像质量差异也会影响模型泛化。LIDC里的数据来自七家机构如果只拿前两家的数据训练、后五家的数据测试AUC通常会掉几个点。建议做数据划分时明确记录病例来源ID测试集的中心和训练集尽可能有重叠否则结论会失真。5.3 解析性能优化建议LIDC全量1018例如果每个病例都暴力遍历XML和DICOM速度会很慢。实际处理时可以用两步走第一步先批量解析所有XML把每个结节的坐标和特征提取出来存成CSV或JSON第二步只对包含结节的切片读DICOM像素。这样90%的非结节切片完全不参与像素级处理时间能缩短好几倍。我自己的处理管线里XML解析完会生成一个中间格式长这样{ series_uid: 1.3.6.1.4.1.14519.5.2.1.6279.6001.298806943288235819492599874324, nodules: [ { malignancy_avg: 4.0, spiculation_avg: 3.5, rois: [ { slice_index: 87, polygon_pixel: [[123, 45], [124, 47], [128, 50]] } ] } ] }这个JSON生成一次以后所有实验直接读它不需要重新解析XML和DICOM头。推荐你也搞一个自己的中间格式后面跑实验的幸福感会提升很多。最后说一点我的个人体会。LIDC这个数据集表面上是一堆图像和一堆点坐标实际上里面藏着很多历史包袱和标注细节直接拿来跑深度学习肯定能跑但要想跑出扎实的结果还是得把这些标签的来龙去脉搞清楚。尤其是我第一次手动把XML里的世界坐标投到CT切片上、看到轮廓和结节完美重合的那一刻我才真正理解了DICOM坐标系统是怎么回事。这个数据的价值不只是“能用来训练”更在于它能逼着你去理解医学影像数据的基本功这是任何参数调优都替代不了的。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门