Lung-PET-CT-Dx数据集获取与医学目标检测预处理指南
2026/9/16 21:19:24 网站建设 项目流程

我先说结论:在拿到 Lung-PET-CT-Dx 这份数据之前,我差点被“找数据”这件事劝退。做肺癌目标检测,特别是想在 PET/CT 这种多模态影像上训练检测模型的朋友,应该都有同感:公开的自然图像数据集一抓一大把,COCO、VOC 随便下,但医学影像领域,尤其是带真实医生勾画标注的肺部肿瘤数据,少得可怜。后来我把目光锁定在 TCIA(The Cancer Imaging Archive)上的 Lung-PET-CT-Dx 集合,才算是真正迈过了第一步。这篇文章我会把从 TCIA 获取这份数据集的完整流程梳理出来,包括注册、协议、三种下载方式、下载后的数据体检,以及为后续目标检测训练做准备的坐标转换和避坑经验。无论你是第一次接触 DICOM 和 PET/CT,还是已经在医学影像目标检测里扑腾过一段时间,这篇都能帮你省下不少瞎摸索的时间。

1. 这份数据能干什么:Lung-PET-CT-Dx 的结构与价值

1.1 集合里到底有哪些模态,标注是什么形式

Lung-PET-CT-Dx 是 TCIA 上的一个公开癌症影像集合,数据主体是肺癌患者的 FDG-PET/CT 扫描。FDG-PET 的原理是给患者注射带有放射性示踪剂的葡萄糖类似物,肿瘤组织代谢旺盛,会大量摄取,所以在 PET 图像上表现为高亮区域,这也是为什么它在肿瘤检测任务里比单纯 CT 更有判别力。

具体到模态,每个患者通常会包含几个不同的 Series:

  • CT:螺旋 CT 扫描,提供解剖结构,单位是 HU(Hounsfield Unit)。
  • PET:代谢图像,提供功能信息,单位经过校正后可以换算成 SUV。
  • CT_PET_Fused:部分设备会生成融合预览图,这个在目标检测里一般不直接用,但可以拿来人工核对位置。
  • RTSTRUCT:医生手工勾画的肿瘤轮廓,这是整个数据集里最值钱的部分。

RTSTRUCT 是 DICOM 格式的一种对象,里面存的不是像素掩膜,而是一系列世界坐标系下的轮廓点。你可以理解为医生的笔在医学影像上圈出来的边界线,而这条边界线最终可以转换成我们目标检测训练需要的边界框(bounding box)。整个集合覆盖了几百例患者,来自多个临床机构。多机构数据意味着不同扫描仪、不同采集协议,对于训练目标检测模型来说反而是好事——模型见过足够多样的数据,泛化能力才靠得住。

1.2 为什么医学目标检测不建议直接套用自然图像流程

很多人拿到数据后第一反应是像 COCO 一样,把图切成 2D 切片,然后丢给 YOLO。这个思路没错,但直接套会有几个认知偏差。

第一,医学图像是三维的。一个病灶在 CT 里是一个立体团块,往往横跨几十张切片。你用 2D 检测框去框它,需要先决定是每一张带病灶的切片都生成框,还是只挑中心切片。这两种策略做出来的数据集性质完全不同。

第二,目标检测的“目标”和自然图像不一样。自然图像里的目标有清晰的边缘、独立的语义,而肺部肿瘤和周围组织在 CT 上常常边界模糊,在 PET 上又容易出现高摄取的炎症伪影。检测器学到的不只是“找轮廓”,还要学会结合两种模态的信息做判断。

第三,RTSTRUCT 勾画的是轮廓,不是框。从轮廓到框之间的转换,涉及坐标系变换、层厚对应、方向矩阵处理,这些坑自然图像数据集里永远不会遇到。

我在后续章节会把这些点全部铺开,但第一步,还是得先把数据从 TCIA 下载下来。

2. 下载前的准入门槛:注册、条款与存储规划

2.1 注册账号时最容易卡住的几个环节

TCIA 的注册入口并不难找,在官网右上角点击 Login / Register,填姓名、邮箱、所属机构、职位类型,提交后邮箱里会收到验证链接。这里有两个容易被卡住的点。

一是邮箱选择。如果你用的是某些免费邮箱,验证邮件有概率被扔进垃圾箱,甚至被直接拒收。我自己的经历是,用机构邮箱注册后几分钟就收到邮件,换用个人免费邮箱时折腾了半小时。强烈建议用带 edu 或机构域名的邮箱注册,这也是 TCIA 数据使用政策里比较欢迎的方式。

二是注册完成后不要急着关页面。需要先登录,进到个人 profile 里确认账号已经处于 Active 状态。很多新手在注册后直接跑到 Data Portal 去下载,结果发现页面一直提示没有权限,其实只是账号还没激活或者没登录。

2.2 许可条款、引用规范和隐私红线

Lung-PET-CT-Dx 虽然属于公开集合,但 TCIA 在生成下载清单时还是会要求你确认数据使用条款。这里要提醒一点:不要为了图快直接点“同意”,稍微扫一眼里面的硬性要求。

TCIA 的数据使用协议通常包含两类约束。一类是隐私红线:数据已经做了去标识化,但你不得尝试重新识别患者身份,也不得将数据用于任何可能追溯到个人的目的。另一类是引用规范:在学术论文、公开项目中使用这份数据时,必须正确引用 TCIA 以及数据集的原始文献和 DOI。很多期刊审稿人会专门检查数据集引用,漏引会被要求返工。

另外,商业用途限制要格外注意。部分 TCIA 集合允许学术研究,但对商用有额外约束。如果你是在公司内部做预研,建议先让法务看一遍当时勾选的条款。

2.3 先算清楚磁盘与流量账

Lung-PET-CT-Dx 整个集合的体积并不小。TCIA 集合详情页通常会标注 Image Count 和 Total Size,实际解压后的体积会比压缩包大不少,因为 DICOM 文件几乎没有经过有损压缩。

我的建议是:动手指下载前,先打开集合页看清楚数据量,然后至少准备“预估体积 × 2”的磁盘空间。为什么是两倍?因为你先要保留压缩包或原始下载目录,再留出解压和转格式的空间。如果磁盘差几个 GB,下载到一半写满,那个感觉真的很酸爽。

网络也是个隐性成本。TCIA 服务器在海外,国内网络环境下,几十 GB 的数据分批下载往往要跑好几个小时,中间还可能断流。所以先规划好目录结构、确认出口带宽、计算好总时长,比下载本身更重要。

3. 从 TCIA 把数据拉下来的三套方案:图形工具、API 脚本与托管存储

3.1 图形客户端 NBIA Data Retriever,适合第一次就用

如果你只是想快速拿到一份数据先跑通流程,我推荐直接用 NBIA Data Retriever,这是 TCIA 官方提供的桌面客户端,Java 写的,支持 Windows / macOS / Linux。

操作链路是这样的:先在 TCIA 数据门户里找到 Lung-PET-CT-Dx 集合,勾选需要的患者(也可以全选),点击 Download 后,系统会生成一个 .tcia 格式的 manifest 文件。这个 manifest 本质上是一个清单,记录了所有需要下载的 Series 的标识信息。然后打开本地安装的 NBIA Data Retriever,把 manifest 拖进去,选择输出目录,它就会开始拉数据。

有几个实操细节值得注意:

  • 输出目录尽量不要带空格和中文,Data Retriever 对路径的兼容性偶尔会抽风。
  • 下载过程中如果某几个文件失败,可以先挂起再重新加载 manifest,它会对未完成的项继续尝试,但对已完成文件往往会跳过,不过有时候判断不够聪明,会重复下载。所以最稳妥的做法是下载完成后,用第 4 章的目录结构核对一遍。
  • 如果一次选了全部患者,manifest 会非常大,Data Retriever 启动时要等一会儿。建议第一次只勾选 5–10 个患者试跑,确认整个目录结构和你的预期一致,再全量拉取。

3.2 TCIA REST API 命令行批量拉取,适合做数据流水线

如果你打算把这个数据集做成一个可复现的数据流水线,或者需要反复下载不同子集,建议用 TCIA 的 REST API 写脚本。TCIA 提供了一套公开的查询接口,基础地址是:

BASE=https://services.cancerimagingarchive.net/services/v4/TCIA/query

先用几个简单的 GET 请求摸清数据范围:

# 查看集合列表 curl "$BASE/getCollectionValues" | jq . # 查看某个集合下的患者列表 curl "$BASE/getPatient?Collection=Lung-PET-CT-Dx" | jq . # 查看患者的检查列表 curl "$BASE/getStudy?Collection=Lung-PET-CT-Dx&PatientID=xxx" | jq .

拿到 StudyInstanceUID 之后,继续往下一层找 Series:

curl "$BASE/getSeries?StudyInstanceUID=1.2.xxx" | jq .

Series 这一层会返回 Modality、SeriesDescription、SeriesInstanceUID 等字段,这时就可以按需筛选,比如只下载 CT、PT 和 RTSTRUCT。确定要下载的 SeriesInstanceUID 后,用 getImage 端点拉取:

curl -u your_username:your_api_key \ "$BASE/getImage?SeriesInstanceUID=1.2.xxx" \ -o series.zip

这里有个细节:TCIA 部分端点对匿名访问有限制,建议注册后在个人中心生成 API Key,请求时用 Basic Auth 方式加上。返回的是一个 zip 包,里面是该 Series 下所有 DICOM 文件。

API 方式最大的好处是可脚本化、可重跑,而且能把“哪些患者、哪些系列、下载时间”这些信息全部记录成日志,对论文复现非常有帮助。我的习惯是先把 getSeries 返回的完整 JSON 存到本地,后面无论做数据统计还是排查缺失,都能直接对着这个 JSON 查。

3.3 进阶玩法:直接访问官方托管存储

TCIA 也在公有云对象存储上放置了数据文件,路径结构可以按 DICOM 的层级去拼接。对习惯 S3 风格工具的工程师来说,这种方式更适合大批量拉取,速度往往比 Data Retriever 更快,还可以用 aria2 这类多线程下载工具做加速。

具体做法是先从 manifest 或 API 拿到某个文件的相对路径,再拼上托管存储的根地址,构造出可直链下载的 URL。需要说明的是,TCIA 对直接拉取托管存储的路径规则并没有像 API 那样有完整的 OpenAPI 文档,所以这个方法更适合有一定经验、愿意自己排查路径的读者。

如果你只是想快速拿到一份可用的数据,不建议从这条路入手。官方 Data Retriever 和 API 已经覆盖了绝大多数场景,直接拉存储适合后面做大规模镜像或者需要断点续传优化的时候再考虑。

三种方案的对比可以看这个表:

方案适合场景上手难度可复现性
NBIA Data Retriever第一次尝试、小规模下载一般
TCIA REST API批量下载、数据流水线
公开托管存储直连高级用户、大并发拉取

4. 下载完先体检:目录结构、DICOM 核对与 RTSTRUCT 坐标转换

4.1 学会看每个患者的目录树,搞清 Series 划分逻辑

下载完成后,第一件事不是急着跑模型,而是把目录结构搞明白。TCIA 下载下来的目录通常是按“患者 → 检查 → Series → 文件”层层嵌套的,大致长这样:

Lung-PET-CT-Dx/ └── LUNG1-001/ └── 1.2.276.0.7230010.3.1.2.1234/ ├── 1.2.276.0.7230010.3.1.3.1001/ # CT Series │ ├── 1.2.276.0.7230010.3.1.4.2001.dcm │ ├── ... ├── 1.2.276.0.7230010.3.1.3.1002/ # PET Series │ ├── ... └── 1.2.276.0.7230010.3.1.3.1003/ # RTSTRUCT └── 1.2.276.0.7230010.3.1.4.3001.dcm

最底层的每一层 ID 都是 DICOM 里定义的唯一标识符。CT 和 PET 通常各自是一个或多个 Series,RTSTRUCT 可能是单独一个 Series。有的患者也可能有不止一次检查,所以同一个 PatientID 下可能有多个 StudyInstanceUID,每个 Study 下又挂多组 Series。

看到这种目录千万别晕,核心要记住一件事:目标检测需要的是“同一空间坐标系下的影像 + 标注”。CT 和 PET 因为是同机 PET/CT 采集,坐标体系基本对齐,但它们的 Series 是独立的,必须通过 StudyInstanceUID 来绑定。

4.2 用 pydicom 快速核对 CT、PET 与 RTSTRUCT

目录结构只是表象,真正要想确认每个 Series 是什么,得打开 DICOM 文件看元数据。我一般习惯用 pydicom 写个几十行的扫描脚本,一次性把所有文件过一遍:

import pydicom from pathlib import Path for dcm_path in Path("..." ).rglob("*.dcm"): ds = pydicom.dcmread(dcm_path, stop_before_pixels=True) print( ds.PatientID, ds.Modality, ds.SeriesDescription, ds.SeriesInstanceUID, )

输出里几个字段要重点关注:

DICOM 字段含义用途
ModalityCT / PT / RTSTRUCT / PET 等区分图像类型
SeriesDescription采集序列的文字描述人工确认更直观
SeriesInstanceUIDSeries 的唯一 ID建立 CT/PET/RTSTRUCT 对应关系
ImagePositionPatient图像在病人坐标系中的位置坐标转换必需

特别是 ImagePositionPatient(IPP),它是算世界坐标到像素坐标转换的关键输入。后面把 RTSTRUCT 轮廓转成目标框时,必须用到它。

体检时如果发现某个患者只有 CT 没有 PET,或者只有影像没有 RTSTRUCT,要记下来。这种情况在公开数据集里并不少见,早期筛选病例时就应该把这些缺模态的样本单独标记。

4.3 从勾画轮廓到边界框:坐标系换算的完整处理

接下来是整篇实操里最需要静下心看的一步:把 RTSTRUCT 里的轮廓点转成图像上的边界框。

RTSTRUCT 里的 ContourData 存的是病人坐标系下的三维坐标点,单位是毫米。比如:

rt = pydicom.dcmread("RTSTRUCT.dcm") for roi in rt.ROIContourSequence: for contour in roi.ContourSequence: data = contour.ContourData # [x1, y1, z1, x2, y2, z2, ...]

这些坐标是世界坐标,不是像素坐标。要转成像素坐标,必须知道对应 CT/PET 图像是怎么摆放的。

对于轴位图像,DICOM 的坐标系关系可以这样理解:每张切片的左上角有一个世界坐标 IPP,像素沿 X 方向的间距和方向由 IOP(Image Orientation Patient)和 PixelSpacing 决定。已知一个世界坐标点 P,想得到它在某张切片上的像素坐标 (u, v),可以用这个简化方式:

import numpy as np def world_to_pixel_2d(point, ipp, iop, spacing): # point: 世界坐标 [x, y, z] # ipp: 切片左上角世界坐标 # iop: 6 个方向余弦,前3个是行方向,后3个是列方向 # spacing: [行间距, 列间距] position = np.array(ipp, dtype=float) u_vec = np.array(iop[:3], dtype=float) * spacing[0] v_vec = np.array(iop[3:], dtype=float) * spacing[1] d = np.array(point, dtype=float) - position u = np.dot(d, u_vec) / np.dot(u_vec, u_vec) v = np.dot(d, v_vec) / np.dot(v_vec, v_vec) return int(round(u)), int(round(v))

这只是单张切片的换算。实际处理 3D volume 时,还需要找到轮廓 z 坐标对应的切片索引,方法是用所有切片的 IPP 的 z 值做最近邻匹配。拿到每个轮廓点的像素坐标后,取最小最大 x、y 就能得到边界框。

这里有个很重要的前提:RTSTRUCT 中勾画的坐标是基于 CT 图像的坐标系,而不是 PET。PET/CT 虽然是同机采集、空间对齐,但如果你要做 PET 上的目标检测,最好的做法还是把 PET Series 和 CT Series 的空间信息都读出来,确认两者的 Position 匹配,然后再统一用 CT 坐标去换算。

我当时在这里踩过一个坑:直接假设所有患者 PET 和 CT 的 IPP 完全一致,结果部分患者 PET 的层厚和 CT 不同,坐标换算后框偏了。后来把所有 Series 的 spacing 和切片数都打印出来,才发现不同模态的采集参数并不一样。所以这段代码一定要写到流程里,每次都用参数去算,不要做任何假设。

5. 构建检测数据集的前置避坑:数据划分、体素方向与空标注

5.1 训练/验证/测试划分必须以患者为粒度

很多人做自然图像目标检测时,习惯把图片随机 shuffle 后按 8:1:1 划分。这个习惯在医学影像上会出大问题。

同一个患者的 CT、PET、RTSTRUCT 之间高度相关,如果同一个患者的切片一部分进了训练集、一部分进了验证集,模型相当于提前“见过”了验证集的同源数据,指标会虚高,但到了真正的独立测试集上性能立刻缩水。

解决办法很简单:划分前先用 PatientID 聚合,把所有患者 ID 排序后按比例划分,不留任何一张切片越界。写代码时要注意,PatientID 可能是字符串,排序方式会影响分布,建议用基于随机种子的方式先打乱患者列表再切分。

import random patients = sorted(metadata["PatientID"].unique()) random.seed(42) random.shuffle(patients) n = len(patients) train_patients = patients[:int(n * 0.8)] val_patients = patients[int(n * 0.8):int(n * 0.9)] test_patients = patients[int(n * 0.9):]

这个顺序最好在数据处理一开始就固定下来,之后无论做增广还是调参,都用同一个划分文件,避免“不同实验用不同测试集”导致的结果不可比。

5.2 NIfTI 与 DICOM 的方向差异,坐标转换必须先查 affine

如果你打算把 DICOM 转成 NIfTI 再用 nnU-Net 或者 3D 检测框架训练,这里有一个特别容易出错的点。

DICOM 的坐标系统是 LPS(左、后、上),而 NIfTI 经过 dcm2niix 转换后通常是 RAS(右、前、上)方向。这会导致同一个世界坐标在转换前后的体素索引完全不同。如果直接从 RTSTRUCT 转出的坐标系硬套到 NIfTI 上,结果必然是框的位置错位。

正确做法是用 NIfTI 自带的 affine 矩阵做坐标变换:

import nibabel as nib import numpy as np img = nib.load("volume.nii.gz") affine = img.affine # 世界坐标转体素坐标 world_point = np.array([x, y, z, 1.0]) voxel = np.linalg.inv(affine) @ world_point voxel = np.round(voxel[:3]).astype(int)

有了这一步,不管底层是 LPS 还是 RAS,都能用统一的数学方式做映射。唯一要注意的是,RTSTRUCT 的轮廓点本身是世界坐标,这个坐标在 DICOM 体系里是 LPS;如果你已经在某个转换工具里把它变成了物理坐标,就要先搞清楚这个物理坐标是 LPS 还是 RAS,再喂给 affine。

这个坑隐蔽在“看起来没问题”里。我建议你在第一次做完转换后,用 3D Slicer 或者 ITK-SNAP 加载原始影像和转换后的边界框,人工抽查几个病例,确认轮廓和框确实贴在病灶上,再批量处理。可视化这一步花不了多少时间,但能省掉后续训练时无数排查问题的时间。

5.3 空标注和尺度失衡,比模型参数更值得担心

检查完坐标,还要统计标注分布。医学影像数据集最常见的坑是“很多图像没有标注”或者“绝大部分目标都很小”。

Lung-PET-CT-Dx 的 RTSTRUCT 是医生手工勾画的肿瘤轮廓,存在几种情况:有的患者肿瘤很小,只有几个像素的直径;有的患者肿瘤很大,或者有多个病灶;还有的 Series 虽然有影像,但并没有对应的标注文件。

如果你想做 2D 切片级别的检测,首先要决定“哪些切片算正样本”。一个常见策略是:只有包含 ROI 中心的那些切片才生成边界框;距离中心太远的切片虽然有病灶边缘,但信息量不足,可以作为难负样本。统计每个切片的病灶数量之后,你会对数据分布有更直观的判断。

用一句话概括:拿到数据后,先花半天时间把数据体检和统计做完,强过直接开训练然后对着一个离奇的 mAP 发呆一星期。

6. 离 YOLO 训练还差一步:标注汇总与数据统计

6.1 设计一个统一标注清单,喂给常用检测框架

把 RTSTRUCT 转成边界框之后,接下来就是标准的目标检测数据工程问题了。无论是 YOLOv8 还是其他检测框架,基本都需要一个“图像路径 + 类别 + 归一化框坐标”的标注文件。

我的习惯是先做一个中间表示的 CSV:

patient_idseries_uidslice_indeximage_pathclass_idx_miny_minx_maxy_max

注意这里的 x_min 等坐标是相对于单张切片的整数像素坐标,后续在写训练脚本时再统一归一化到 0–1。CSV 的好处是便于检查,如果哪一行的坐标超出图像尺寸,一眼就能发现。

然后再写一个转换脚本,把 CSV 按检测框架需要的格式输出。以 YOLO 为例,每张切片对应一个 .txt 文件,每一行是:

class_id cx cy w h

其中 cx、cy、w、h 都是归一化后的值。转的时候要小心边界坐标不要超过 [0,1],否则训练时可能会产生 NaN loss。

6.2 用基础统计判断这份数据是否值得投入训练

数据整理完毕,开工训练之前,建议先跑一遍基础统计:

  • 总共有多少个有效病例、多少张带标注切片;
  • 每个病例平均多少个病灶,最大最小面积;
  • 病灶尺寸分布直方图,小目标占比;
  • 每种模态(CT/PET)的切片总数和分辨率。

这些数字会直接影响训练策略。比如病灶普遍偏小,就意味着要重点处理小目标检测问题,可能要用更大分辨率的输入、或者专门做切片级增广;再比如 PET 和 CT 的强度分布差异大,就必须在数据预处理阶段做各自独立的规范化,这个我后面专门写一篇具体聊。

我做这件事的体感是:Lung-PET-CT-Dx 这份数据虽然要折腾的地方不少,但信息量足够扎实,尤其是医生勾画的轮廓质量,比很多自动标注数据高一个档次。用这份数据训练出来的模型,至少是“见过真正肿瘤”的模型,而不是在自然图像上过拟合出来的玩具。

最后分享一个我在整个流程里最受益的小习惯:每下载一个患者,就在本地维护一个 CSV 记录它的 PatientID、StudyInstanceUID、有哪些 Modality、文件数量、平均体积。后面但凡遇到“训练到一半发现某个患者数据缺失”或者“验证集里混进了训练集同源数据”这种问题,这个 CSV 就是你的救命稻草。医学影像数据集从来都是“数据准备 80%,模型训练 20%”,把前面这 80% 做扎实,后面的实验才会真正顺畅。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询