
简介面向医学影像研究与肺结节智能分析场景的 MATLAB 工具包专门处理 LIDC-IDRI 肺结节 CT 数据集中的注解与图像信息。工具包覆盖 DICOM 数据读取、XML 注解解析、掩模与像素映射生成、数据集拆分等全流程可帮助科研人员快速提取病灶区域并构造可用于检测、分割与特征提取的实验样本。资源共包含 24 个文件以 .m 脚本为主辅以 .txt 说明文档、.pl 与 .sh 辅助脚本压缩包仅 182KB轻量易部署。该资源已有 2947 人学习下载。借助其中的关键转换逻辑用户能从原始 XML 注解到像素映射、真实标注图像及拆分后子集获得一整套处理方案比如将专家标注矩阵转换为可视化的掩模图像、生成 ground truth 作为算法基准并配有许可证与说明文档适合医学图像算法研发、课程实验与科研复现等多种应用。1. LIDC肺结节数据集工具包与其花两周解析XML不如直接用它提取做医学图像处理的人十有八九都在LIDC肺结节数据集上栽过跟头。这个数据库里几百例胸部CT的DICOM文件好办真正磨人的是那套嵌套了三层以上的XML标注——四个医生对每一个结节的位置、直径、边缘、良恶性各写各的如果你的目标是快速从LIDC里提取肺结节做检测或分割训练光是把标注转成能用格式就能耗掉整整一周。今天拆的这个工具包就是把“读DICOM序列 → 解析XML标注 → 生成结节切片与mask → 输出为ROI或YOLO格式”整条链路写成了现成Python脚本适合正在做肺结节检测、分割课题但不想在数据预处理上反复折腾的研究生和算法工程师。这不是把文件堆给你就算完的里面的边界、坐标系陷阱我都替你踩了一遍。2. 先把底牌看清楚LIDC的目录结构与XML标注体系2.1 原始数据长什么样DICOM序列里的三个隐藏层级LIDC-IDRI原始数据集按病例编号组织每个病例文件夹里通常是一个包含多个子目录的DICOM序列子目录名字类似“000001.dcm”这种按InstanceNumber递增命名的文件。这里第一个新手误区是直接用操作系统默认顺序读取——很多DICOM文件名是补零的字符串一旦超过99999就会出现字典序和拍摄顺序不一致切片顺序错乱后提取出的结节位置全部作废。工具包首先做了两个事一是递归扫描病例目录下所有.dcm文件用pydicom读取每个文件的InstanceNumber和SliceLocation排序而不是靠文件名二是通过SeriesInstanceUID分组确保同一个病例只有一个CT序列进入后续流程。排序这一步在处理GE和西门子的数据时尤其重要前者的SliceLocation有时完全一样而依赖InstanceNumber后者则反过来只认其中一个字段就会翻车。读取和排序对应的核心代码在工具包的dicom_loader.py里逻辑是这样处理的import pydicom import os import numpy as np def load_dicom_series(series_dir): 读取整个DICOM序列并按InstanceNumber排序。 dicom_files [] for root, _, files in os.walk(series_dir): for fname in files: if fname.lower().endswith(.dcm): dicom_files.append(os.path.join(root, fname)) slices [] for fpath in dicom_files: try: ds pydicom.dcmread(fpath, forceTrue) slices.append(ds) except Exception as e: print(f[跳过] 无法读取 {fpath}: {e}) # 按InstanceNumber排序解决文件名补零导致的字典序问题 slices.sort(keylambda ds: int(ds.InstanceNumber)) return slices这段代码从左到右做三件事遍历目录收集所有DICOM文件逐个强制读取并容忍单个文件损坏最后按InstanceNumber数值排序并重建三维体数据。注意forceTrue参数有些CT存储单元的传输语法不标准不加这个参数pydicom会直接抛异常——这是我在一批老数据上实际遇到的二十三例里有三例必须靠它读取。2.2 XML标注处理ReadingSession、nodule与non-nodule三类标记各归何处LIDC的标注XML文件放在病例根目录下文件名类似0.xml根节点是ReadingSession里面套了unblindedReadNodule和nonNodule两类节点。unblindedReadNodule下每个结节有一个全局唯一IDnoduleID同一结节可能被多个医生标注而医生的边界框在roi节点里包含x坐标、y坐标和图像编号图像编号即像素坐标对应的切片索引。值得留意的是XML里还有characteristics节点记录了边缘、球度、内部结构等属性以及最关键的malignancy字段——1到5的整数1为良性5为恶性。工具包里的parse_lidc_xml.py会把这些信息全部结构化落到一个DataFrame里字段包括结节ID、医生编号、每一层切片的坐标点集、以及对应的DICOM切片文件引用。它没有做额外的数据清洗——比如有人会提前过滤掉只有单一医生标注的结节——因为工具的设计思路是先让你看到原始标注再按自己的标准筛选这比在黑匣子里替你拿主意安全得多。这里必须要区分两类标记nonNodule是医生明确认为“不是结节”的区域通常用来做负样本而unblindedReadNodule里的结节按惯例只考虑直径大于3mm且至少两个医生标注过的小于3mm的被归为“非结节”。工具包默认参数min_doctors2你可以改成1或3但改前先想清楚只按一个医生的标注假阳性率会明显上升。2.3 工具包文件清单七个脚本各管一段流程拿到这个工具包后解压出来是七个Python脚本和两个说明文档整体结构做了一个明确分层。dicom_loader.py负责DICOM序列读取与排序parse_lidc_xml.py负责XML解析和结构化输出extract_nodules.py是主流程调用前面两个模块输出ROI和maskconvert_to_yolo.py处理格式转换visualize_results.py检查提取结果utils.py提供公共函数config.py存放所有可调参数。脚本名职责输出格式dicom_loader.pyDICOM序列递归读取与排序pydicom对象列表parse_lidc_xml.py解析XML标注并结构化pandas DataFrameextract_nodules.py主干流程提取ROI和maskPNG与原子的npy文件convert_to_yolo.pyROI与mask转YOLO格式txt标签和JPEG图像visualize_results.py拼接验证图人工检查带轮廓叠加的PNGconfig.py所有参数集中配置常量定义这种分工对半路接手的人相当友好——比如你只想生成mask不需要ROI单独改extract_nodules.py里的generate_mask_only标志就完事不需要动其他任何文件。3. 从DICOM到肺结节ROI提取流程的每一步拆开细讲3.1 体素间距换算像素坐标到物理坐标的关键一步CT扫描的层厚和像素间距每台机器都不死一样LIDC数据里有0.5mm到3mm的层厚跨度。XML里记录的坐标是像素坐标系列、行、切片索引但是生成三视图或计算结节体积时需要把它换算成物理坐标。工具包在parse_lidc_xml.py里通过读取对应DICOM文件的PixelSpacing和SliceThickness完成转换两个参数可以在config.py里手动覆盖用于处理DICOM头缺失或损坏的情况。这一步骤容易被忽视但影响很大在层厚3mm的数据上如果你不管Z轴坐标的物理意义直接按像素索引裁切提取的结节切片在三维空间里会表现为等比拉伸的椭圆后续送到3D CNN里训练时会让网络学到错误的形态学先验。工具包对每个结节额外输出world_coords.npy保存物理坐标转换后的中心点和直径。3.2 按轮廓点集生成mask并裁切ROIXML里每个结节在单个切片上是一个多边形轮廓点集存储在edgeMap节点下。工具包用OpenCV的fillPoly把多边形变成二值mask然后以多边形外接矩形的中心为基准向外扩展一个固定边距config.py里的roi_margin_px默认20像素裁切出ROI图像。这个边距不是随便定的——CT图像里结节周围应该有足够的肺实质作为背景上下文但边距太大会引入过多无关结构影响数据增强和归一化。import cv2 import numpy as np def extract_roi_and_mask(ct_slice, contour_points, margin20): ct_slice: 2D CT图像numpy数组HU值 contour_points: XML里解析出的多边形坐标点Nx2 margin: 外接矩形向外扩展的像素数 mask np.zeros(ct_slice.shape, dtypenp.uint8) pts np.array(contour_points, dtypenp.int32) cv2.fillPoly(mask, [pts], 1) x, y, w, h cv2.boundingRect(pts) x_start max(0, x - margin) y_start max(0, y - margin) x_end min(ct_slice.shape[1], x w margin) y_end min(ct_slice.shape[0], y h margin) roi ct_slice[y_start:y_end, x_start:x_end] roi_mask mask[y_start:y_end, x_start:x_end] return roi, roi_mask这段代码用boundingRect求多边形外接矩形然后向外扩margin个像素最后切片得到ROI和对应的mask。这里有个细节值得注意坐标必须是(x, y)顺序XML里存的x和y恰好也是这个顺序但如果你从别的数据源拿标注一定要先确认是行先行坐标还是列先行坐标——工具箱里某开发者就因为没有确认这个顺序而废掉了一批提取结果仅有的一个案例整整两天白干。3.3 CT窗宽窗位提取前到底要不要调窗这是分岔口检测任务可以直接用原始HU值分割任务建议先做肺部区域裁剪而如果要做可视化质地分析就要先应用窗宽窗位。工具包在extract_nodules.py里默认不调整窗宽窗位输出的ROI保存为16位PNG保留原始HU精度但config.py里预留了window_center和window_width两个参数设为None表示不处理设为(-600, 1500)则是肺窗的标准值。选择肺窗会丢掉骨和纵隔的灰度细节但结节和周围肺实质的对比度最强多数检测模型在肺窗图像上训练收敛更快。这里没有标准答案建议先不调窗训练一个基线模型再对比调窗后的效果这样能客观判断前处理策略对任务的影响。3.4 单个切片还是全系列跨切片结节的处理逻辑LIDC结节平均跨3到5个切片医生通常会在每个切面上都勾画轮廓。工具包默认只提取有标注的切片并逐张输出也就是说最后的ROI数量等于“结节数乘以标注层数”。想每个结节只保留中心切片做2D训练的话config.py里select_center_sliceTrue会挑出轮廓面积最大的那一层3D训练则建议关闭该选项让所有标注切片按顺序保留。这两种模式下输出目录结构略有不同中心切片模式会在结节ID下按center.png命名全系列模式则是slice_001.png依次递增。4. 提取过程中的避坑清单坐标、间距与窗口三个方向的实际踩坑经验4.1 症状提取出的ROI图像里结节全在左上角位置完全错位现象程序跑完可视化时发现所有结节ROI的可视位置和原始CT切片上肉眼可见的结节位置严重偏移有些甚至超出图像范围。原因XML里的坐标是图像编号加行列号但行列号在LIDC的CT数据里是“列优先”还是“行优先”取决于生成厂商。部分GE扫描仪DICOM头里的PatientPosition是FFS头先进、仰卧行列语义和坐标轴方向存在翻转。直接用XML数值裁切就会位置错位。解决工具包内建的align_coordinates()函数会强制检查DICOM头的PatientPosition和ImageOrientationPatient矩阵。如果发现方向不是默认值就交换行列并做镜像翻转。实际应用时最好在批量提取前先单独取一个结节手动比对确认坐标一致再全量跑这个步骤能省下后面大量的返工时间。4.2 症状某些DICOM文件读取时报错读出来亮度全黑现象批量读取到某个病例时pydicom抛异常强制读取后图像数组全是0或一个固定值。原因LIDC里存在少量格式不规范的DICOM文件比如传输语法标注错误、像素数据被遮盖或者JPEG2000压缩未正确处理。另一个常见原因是DICOM头里的RescaleIntercept是-1024但像素数据本身已经存储成无符号整数直接显示时不加窗全黑。解决工具包在dicom_loader.py里定义了统一的归一化函数rescale_to_hu()强制读取每个文件的RescaleSlope和RescaleIntercept应用到像素数组再返回浮点型HU数组。如果这两个标签缺失默认用slope1, intercept-1024兜底。遇到实在损坏的文件工具包会输出一个名单让你决定是补齐还是丢弃而不是直接卡死流程。4.3 症状提取结果里出现两块完全一样的ROI结节重复了现象同一个结节被提取了两次以上而且提取出的ROI内容像素完全一样导致后续训练时训练集和验证集有重复样本指标虚高。原因两个医生的XML标注里noduleID不一致但几何位置重叠超过90%。LIDC的四医生独立标注机制是并行执行的没有共识裁决所以这种情况相当常见。工具包的deduplicate_by_iou()函数按IoU阈值默认0.75合并同一结节的多医生标注。解决合并策略不是简单的保留第一个而是把所有标注的多边形坐标点做平均生成一个新的共识轮廓再进入后续提取流程。这里要注意的是并通过“医生数量”做加权——四名医生都标注过的结节权重更高代码里已经内置了这个细节。4.4 症状输出的mask有大面积空洞边缘锯齿明显现象生成的二值mask在结节中心区域出现空心或斑驳缺口边界不光滑。原因LIDC XML里有些医生的轮廓点采样很稀疏相邻两点之间跨越了多个像素fillPoly在填充时把稀疏区域判断成曲线内部的凹陷造成填充不完整。解决工具包的parse_lidc_xml.py在填充前先对轮廓做了三次样条插值把轮廓点从原来的几十个增加到数百个再传给fillPoly。插值的代价是边缘比原始标注平滑一些但综合来看插值带来的mask质量提升远大于引入的微小偏差。如果你做的是像素级分割任务且对边界要求极其严格可以考虑把插值系数interp_scale调小默认值是3。5. 进阶用法把提取结果批量转成自己的训练格式并自动检查质量拿到干净的ROI和mask后大多数人要做的第一件事是转成自己模型的输入格式。工具包里内置的convert_to_yolo.py把ROI转成YOLO格式的归一化坐标标签——每个结节生成一个txt文件内容是“类别ID 中心点x 中心点y 宽度w 高度h”全部归一化到0到1区间。这里有个关键细节YOLO的坐标系原点是图像左上角而LIDC坐标原点是矩阵的左上角两者方向一致不需要额外翻转但归一化基准是ROI的宽高不是整个CT切片因为只有ROI才会被送进网络。代码里用roi_mask的boundingRect重新计算了一次目标框确保标签和ROI严丝合缝。转格式只是第一步更值得花时间的是自动质量检查。我一般会用visualize_results.py把提取出的每个结节的三张图拼在一起原切片上叠加医生轮廓、生成的mask、以及ROI裁切结果。跑完一批数据后先用程序判断mask面积与轮廓面积的差是否超过阈值再用肉眼抽检百分之五的图确认提取流程没有系统性偏移。某次批量提取过程中抽检发现一批来自同一台扫描仪的病例的mask普遍向左偏移两个像素最后定位到那台设备的PixelSpacing标注有误如果不是做这轮检查整个训练集都会录入误差而不自知损失会相当大。从那以后我每次批量提取LIDC或任何CT数据集前都会强制走一遍“单个病例验证 少量抽检 全量提取”的三段流程——这个习惯把提取阶段的问题消灭在训练之前省下的远远不止返工时间。希望这个工具包也能帮你少踩几个同样的坑。本文还有配套的精品资源点击获取