ARTICLE DETAIL

资讯详情

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

LIDC肺结节CT数据集处理工具包:从DICOM/XML到训练集实战

LIDC肺结节CT数据集处理工具包:从DICOM/XML到训练集实战 简介针对LIDC-IDRI肺结节CT数据集的专用处理工具包面向医学影像研究人员与算法开发者用于高效提取、转换和分析肺结节标注信息。压缩包共24个文件以MATLAB脚本.m为主辅以说明文档.txt、Perl脚本.pl和Shell脚本.sh整体仅182KB轻量易部署。工具包围绕LIDC数据集的XML标注与MAT矩阵转换设计可将专家标注转为掩模、真实图像或像素映射并支持注解拆分、文件查找与路径修正等预处理操作覆盖从原始XML到可视化掩模的完整流程。目前已有2940人学习下载。借助这批脚本使用者能快速解析多医生标注生成用于检测与分割任务的Ground Truth同时便于批量整理大规模数据为肺结节识别、特征提取及机器学习模型训练提供可靠数据支撑。 先申明一句我这次分享的不是什么花架子Demo而是一套我自己反复折腾过、踩过坑、最后沉淀下来能直接跑实验的“LIDC肺结节CT数据集处理工具包”。如果你正准备做肺结节检测、分类或者分割拿LIDC-IDRI这个数据集练手那这篇内容正好是你需要的。里面会包含数据下载的注意事项、DICOM和XML文件的解析方法、如何构建一个干净的训练集以及我在处理过程中遇到的各种问题。1. LIDC数据集到底是个什么来头1.1 数据集的背景与核心价值LIDC-IDRILung Image Database Consortium and Image Database Resource Initiative是目前医学影像分析领域最常用的公开肺结节CT数据集之一。这个数据集由美国国家癌症研究所牵头联合多家机构共同建立一共收录了1018例肺部CT扫描数据。每一例都包含完整的DICOM格式CT影像序列以及XML格式的标注文件。这个数据集的核心价值在于它的标注质量非常高。每个病例的标注工作由4位经验丰富的胸部放射科医生独立完成。医生们会在三个环节分别标注第一次标记“疑似结节”第二次标记“结节≥3mm”第三次标记“结节3mm”最终通过协商形成共识标注。这种多医生独立标注加共识协商的流程保证了标注的可靠性也让研究者可以做医生间一致性分析。1.2 数据集的结构和标注体系从数据组织角度来看LIDC数据集采用DICOMXML的双轨结构。DICOM文件存放CT图像序列XML文件存放所有标注信息。这种结构的好处是影像数据和标注数据分离方便灵活处理坏处是如果你没接触过这两个格式前期光处理数据就够你喝一壶的。标注体系是LIDC最值得研究的地方。对于每一个被标记为“结节≥3mm”的病灶XML文件里会记录详细的轮廓坐标点。这些轮廓点按照slice层面逐个绘制把医生勾画的结节边界都以像素坐标的形式保存下来。除此之外还有8个常用的语义特征评分包括内部结构、钙化程度、球形度、边缘、分叶、毛刺、纹理和malignancy恶性程度。这些特征全部采用1到5的整数评分为后续的深度学习任务提供了丰富的标签依据。2. 工具包的下载与项目结构2.1 下载流程与注意事项先说下载这块。LIDC数据集本身托管在The Cancer Imaging ArchiveTCIA网站上。下载方式有两种一种是在TCIA网站上手动勾选病例后下载另一种是使用TCIA提供的API接口批量下载。数量达到1018例全量数据大概是140GB左右所以除非你只是想体验一下解析流程否则不建议全量下载。我建议的做法是先下载10到20例数据把工具包跑通确认自己的环境没问题之后再根据实际需求下载全量数据。TCIA支持按病例编号下载比如你想下载LIDC-IDRI-0001这个病例直接在URL里指定编号就行。手动下载时注意一点如果网络不稳定大文件下载很容易半路断掉建议用支持断点续传的工具。注意TCIA对下载工具的限制比较严格有些下载软件会被服务器拒绝连接。我实测下来浏览器直接下载最稳或者用TCIA的NBIA Data Retriever客户端这个是官方推荐的。2.2 工具包的整体目录结构拿到原始数据之后你会发现每个病例的文件夹结构是长这样的LIDC-IDRI-0001/ ├── 1.3.6.1.4.1.14519.5.2.1.6279.6001.256332635829501968481959027350/ │ ├── 1.3.6.1.4.1.14519.5.2.1.6279.6001.169447908331318387418806571843/ │ │ └── 1.3.6.1.4.1.14519.5.2.1.6279.6001.147795564643582388401760440345/ │ │ ├── 000000.dcm │ │ ├── 000001.dcm │ │ └── ... ├── ... └── 1.3.6.1.4.1.14519.5.2.1.6279.6001.311147554967601381996993154505.xml这个嵌套层次很深DICOM文件被放在层层嵌套的目录中XML文件在一个单独的文件夹里。用代码处理时推荐用Python的pathlib.Path.rglob()方法递归搜索不要自己去拼接路径。我写的工具包目录结构如下lidc_toolkit/ ├── config.py # 配置文件存放路径和参数 ├── download.py # TCIA下载辅助脚本 ├── parse_dicom.py # DICOM图像读取模块 ├── parse_xml.py # XML标注解析模块 ├── dataset.py # 数据集构建与预处理 ├── visualize.py # 可视化工具 ├── requirements.txt # 依赖包列表 └── main.py # 主流程控制脚本3. 核心模块解析DICOM图像读取与预处理3.1 pydicom读取DICOM图像序列DICOM是医学影像的标准格式后缀通常是.dcm。一个CT扫描序列由几十到几百张连续的断层图像组成每一张对应一个特定的空间位置。读取DICOM文件首选pydicom库这是Python生态里最成熟的DICOM解析库。读取单个DICOM文件的方法很简单import pydicom dcm pydicom.dcmread(000000.dcm) pixel_array dcm.pixel_array # 得到原始像素值但读取整个序列需要额外处理。DICOM文件中有一个叫ImagePositionPatient的标签记录了这张图像在病人坐标系中的位置。要按正确顺序排列断层图像必须根据这个标签排序而不是依赖文件名import pydicom from pathlib import Path def load_dicom_series(series_dir): dicom_files list(Path(series_dir).glob(*.dcm)) slices [pydicom.dcmread(f) for f in dicom_files] # 按图像位置排序 slices.sort(keylambda s: float(s.ImagePositionPatient[2])) return slices3.2 像素值到HU值的转换这里有一个新手很容易踩的坑DICOM文件里的像素值并不是最终的CT值。CT图像的标准单位是HUHounsfield Unit范围通常是-1024到3071。空气的HU值是-1000水的HU值是0骨头的HU值在400以上。但在原始DICOM文件中像素值通常经过了缩放和偏移需要经过转换才能得到真实的HU值。转换公式非常简单def apply_rescale(dcm, pixel_array): # 从DICOM头文件中获取缩放参数 rescale_slope float(dcm.RescaleSlope) rescale_intercept float(dcm.RescaleIntercept) return pixel_array * rescale_slope rescale_intercept大多数LIDC数据的RescaleSlope是1RescaleIntercept是-1024意味着原始的0像素值对应-1024HU。但不要写死这个参数因为个别病例可能有不同的设置习惯性从DICOM头文件里读取才是最稳妥的。3.3 窗宽窗位与肺窗显示CT图像的HU值范围极大直接显示的话对比度会很差。临床上通过窗宽Window Width和窗位Window Center来控制显示范围。对于肺结节检测任务最常用的显示方式是肺窗窗位在-500左右窗宽在1500左右这个范围能很好地把肺部软组织显示出来。def apply_window(pixel_hu, window_center-500, window_width1500): window_min window_center - window_width / 2.0 window_max window_center window_width / 2.0 # 截断到窗口范围 img np.clip(pixel_hu, window_min, window_max) # 归一化到0-255 img (img - window_min) / (window_max - window_min) * 255.0 return img.astype(np.uint8)这个操作虽然简单但对后续深度学习训练很重要。很多开源模型输入的图像是经过窗宽窗位处理的如果你不做这一步就直接输入原始HU值模型训练很容易不收敛或者效果很差。4. 核心模块解析XML标注信息提取4.1 XML文件的结构分析LIDC的XML文件遵循特定的schema根节点是Response,里面嵌套了ResponseHeader和ResponseBody。核心信息都在ResponseBody的readingSession节点下每读一次就对应一个放射科医生的标注环节。每个读出会话包含unblindedReadNodule和nonNodule两种标注。unblindedReadNodule是重点它又包含两种子类型nodule: 标注为“结节≥3mm”的病灶包含详细的轮廓坐标和特征评分nonNodule: 标注为“非结节”或“疑似结节3mm”的病灶往往只包含位置信息和简单注解对于“结节≥3mm”的病灶每个nodule节点下有多个roiRegion of Interest子节点每个roi对应一个CT层面上的轮廓。roi里包含一个imageZposition表示该层面的空间位置以及一个edgeMap节点里面是一系列x和y坐标点构成了结节的轮廓边界。4.2 用ElementTree解析XMLPython解析XML最常用的库是xml.etree.ElementTree。它的缺点是直接解析带命名空间的XML会有点啰嗦。我写了一个辅助函数提取时忽略命名空间import xml.etree.ElementTree as ET def parse_lidc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() # 定义命名空间 ns {lidc: http://www.nih.gov} # 找到所有读取会话 reading_sessions root.findall(.//lidc:readingSession, ns) all_nodules [] for session in reading_sessions: # 每个医生的一次完整阅读 nodules session.findall(.//lidc:unblindedReadNodule/lidc:nodule, ns) for nodule in nodules: # 提取结节信息 nodule_id nodule.get(id) # 提取特征评分 characteristics nodule.find(lidc:characteristics, ns) malignancy characteristics.find(lidc:malignancy, ns).text # 提取各层面的轮廓 rois nodule.findall(.//lidc:roi, ns) contours [] for roi in rois: z_pos float(roi.find(lidc:imageZposition, ns).text) edge_map roi.find(lidc:edgeMap, ns) xs [float(x.text) for x in edge_map.findall(lidc:xCoord, ns)] ys [float(y.text) for y in edge_map.findall(lidc:yCoord, ns)] contours.append({ z: z_pos, xs: xs, ys: ys }) all_nodules.append({ nodule_id: nodule_id, malignancy: int(malignancy), contours: contours }) return all_nodules4.3 把轮廓坐标转换为掩码MaskXML里记录的轮廓坐标是像素坐标对应原始CT图像的坐标系。有了每个层面的轮廓点我们可以通过OpenCV的fillPoly函数将轮廓填充成二值掩码import cv2 import numpy as np def contour_to_mask(contours, image_shape): mask np.zeros(image_shape, dtypenp.uint8) for contour in contours: # 轮廓点坐标 points np.array([contour[xs], contour[ys]]).T points points.astype(np.int32) # 填充轮廓内部 cv2.fillPoly(mask, [points], 1) return mask关键点是轮廓坐标是整个肺结节在这个层面的投影边界。很多早期研究直接把三维结节简化为二维检测框或者中心点来用减少了对轮廓数据的依赖。但如果你做分割任务轮廓转掩码是无法绕开的步骤。5. 实战构建生成你的第一个可用数据集5.1 确定任务类型和标签方案在动手处理数据之前先明确你的任务类型。LIDC可以做三个方向的任务二分类判别一个候选区域是否存在恶性结节malignancy在3到5视为恶性1到2视为良性目标检测输出结节的位置中心坐标和大小直径分割输出结节的像素级掩码我建议第一次做的时候先做二分类因为最简单、起步最快。把malignancy评分作为标签评分1到2标记为良性0评分4到5标记为恶性1评分3因为是中等程度可以选择丢弃或者归入其中一类。实际训练中丢弃评分3的样本是最通用的处理方式。5.2 提取候选结节区块有了掩码之后下一步就是裁剪出包含结节的区域。最简单的方式是找到掩码的最小外接矩形然后在原图上裁剪这个区域。对于二维检测任务通常还会额外生成一个中心点热图这个留到后面训练时再说。def extract_nodule_crops(ct_volume, mask_volume, margin10): # 找到所有结节的连通区域 contours, _ cv2.findContours(mask_volume, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) crops [] for contour in contours: x, y, w, h cv2.boundingRect(contour) # 向外扩展margin像素保证包含完整的结节边缘信息 x_min max(0, x - margin) y_min max(0, y - margin) x_max min(ct_volume.shape[1], x w margin) y_max min(ct_volume.shape[0], y h margin) crop_img ct_volume[y_min:y_max, x_min:x_max] crop_mask mask_volume[y_min:y_max, x_min:x_max] crops.append((crop_img, crop_mask)) return crops这一步有几个细节值得注意。首先CT图像是三维体数据结节是三维结构所以需要同时对多个连续层面做裁剪。其次裁剪时预留的margin不能太大也不需要太小10像素是一个常见选择。最后裁剪的尺寸需要统一resize到固定大小比如64×64或者128×128方便后续模型输入。5.3 生成HDF5格式的数据集缓存医学影像数据的特点是文件数量多、单个文件大每次训练都重新解析DICOM和XML会非常浪费时间。我习惯把处理好的数据统一打包成HDF5格式这样后续训练加载数据只需要读一个文件速度快很多。import h5py def create_hdf5_dataset(output_path, samples, labels): with h5py.File(output_path, w) as f: # 创建数据集允许压缩 f.create_dataset(images, datasamples, compressiongzip, compression_opts4) f.create_dataset(labels, datalabels) # 存储metadata f.attrs[num_samples] len(labels) print(f已生成HDF5文件包含{len(labels)}个样本)关于数据增强不要在生成缓存的时候做。正确的做法是缓存只存放原始裁剪图像在模型训练时实时做随机旋转、翻转等增强操作。这样每次训练都能看到不同的数据变形模型泛化能力更好。6. 踩坑记录与排查方法6.1 DICOM序列顺序混乱这是最常见的坑。部分病例的DICOM文件名是乱序的直接按文件名排序读取会导致三维重建时图像错位。最好的做法还是通过ImagePositionPatient或InstanceNumber标签排序。如果同时存在这两个标签优先用ImagePositionPatient它表示实际空间位置更可靠。6.2 少数医生的标注缺失或XML文件为空根据数据集的规格说明每位医生可以独立决定标注内容所以有些病例只被一两个医生标注过甚至有的XML文件里没有任何nodule节点。处理时要有容错逻辑程序遇到空标注不能崩溃要跳过这一例并记录日志。我在工具包里设置了一个skip_invalidTrue参数默认打开。6.3 坐标偏移问题在把XML里的像素坐标映射到DICOM图像时有一个容易被忽略的细节XML里记录的坐标是相对于某个参考坐标系的而DICOM图像经过裁剪、翻转或重采样之后坐标系可能已经变了。解决的办法是在整个数据流程中保持原始坐标系不变。如果真的需要处理可以使用DICOM头文件里的ImageOrientationPatient和PixelSpacing进行坐标变换。我遇到过一个更实际的坑某些DICOM图像是16位存储的读出来像素数组是uint16类型但做数值运算时如果不小心转成了int8会导致图像残缺甚至全黑。处理这类问题时写完代码先跑几个病例看可视化结果确认图像和掩码对齐了再大规模批量处理。6.4 类别不平衡问题LIDC数据集中恶性结节和良性结节的比例并不是1:1。实际统计下来良性结节的数量略多于恶性结节但差距不算悬殊。不过如果你把“评分3”的样本丢弃之后样本数量会明显下降。针对这种情况我建议的做法是先按原始分布训练一个baseline观察各类别的recall然后再决定是否做数据扩充或者类别加权。不要一上来就做过度采样。7. 工具包的使用流程示例最后给出一个完整的使用流程。整个命令序列是这样的# 1. 配置路径 from config import LIDC_ROOT, OUTPUT_DIR # 2. 解析DICOM序列 from parse_dicom import load_dicom_series, apply_rescale slices load_dicom_series(LIDC-IDRI-0001/.../) volume np.stack([apply_rescale(s, s.pixel_array) for s in slices]) # 3. 解析XML标注 from parse_xml import parse_lidc_xml nodules parse_lidc_xml(LIDC-IDRI-0001.xml) # 4. 构建训练样本 from dataset import build_dataset build_dataset(LIDC_ROOT, OUTPUT_DIR, task_typeclassification, malignancy_positive4, malignancy_negative2, crop_size64) # 5. 可视化检查 from visualize import show_nodule_crop show_nodule_crop(sample_idx0, dataset_pathoutput/lidc_train.h5)以上流程跑通之后你就有了一个稳定可用的LIDC数据处理流水线。后续换模型、换任务类型只需要在这个基础上调整参数就行不用再从头处理原始数据。我在实际使用中还有一个心得处理医学影像数据时宁可多花时间在数据可视化上也不要着急直接进模型训练。把每个处理步骤的结果都保存下来看一眼能帮你发现很多隐藏的问题。很多看起来是模型效果差的问题根源其实在数据处理阶段就已经埋下了伏笔。本文还有配套的精品资源点击获取
返回列表