import pydicom
import os
from pathlib import Path
# 探查DICOM序列中的元数据冗余
dicom_dir = "dicube-testdata/dicom/sample_200"
# 我们仅选取前两个文件进行对比
dicom_files = list(Path(dicom_dir).glob("*"))[:2]
# 读取元数据(stop_before_pixels=True 避免加载像素数据,加快速度)
ds1 = pydicom.dcmread(dicom_files[0], stop_before_pixels=True)
ds2 = pydicom.dcmread(dicom_files[1], stop_before_pixels=True)
# 校验序列级元数据和实例级元数据的异同
patient_same = ds1.PatientName == ds2.PatientName
series_same = ds1.SeriesInstanceUID == ds2.SeriesInstanceUID
instance_same = ds1.InstanceNumber == ds2.InstanceNumber
print(f"两个切片的 PatientName 相同: {patient_same}")
print(f"两个切片的 SeriesInstanceUID 相同: {series_same}")
print(f"两个切片的 InstanceNumber 相同: {instance_same}")元数据存储机制
1. 传统 DICOM 的“历史包袱”:元数据冗余
DICOM 作为医学影像的基石,其设计源于上世纪末,采用了“一文件一图像(切片)”的存储模式。这种模式在当时是合理的,但随着影像设备采集的图像层数急剧增加(从几十层到数千层),其固有的元数据冗余问题成为了一个巨大的性能瓶颈和存储负担。
在一个包含数百个 .dcm 文件的 CT 序列中,绝大部分描述患者身份、检查信息、设备参数的元数据在每个文件中都被完整地复制了一遍。只有像切片位置、实例编号这类与特定切片相关的少数信息是变化的。
正如代码所示,绝大多数元数据是序列共享的,而 InstanceNumber 等则是切片独有的。这种设计不仅浪费了大量存储空间,更严重的是,当需要获取整个序列的某个信息时(例如窗宽窗位),程序必须遍历并解析数百个文件,导致了极低的 I/O 效率。
2. 拥抱标准:基于 DICOM JSON 构建
为了让 DICOM 更好地融入现代 Web 和数据科学生态,DICOM 标准委员会在 PS3.18 部分引入了 DICOM JSON 模型。它为 DICOM 元数据提供了标准化、人类可读的 JSON 表示,简化了非医疗软件对 DICOM 信息的解析与利用。
DiCube 没有重新发明轮子,而是完全遵从这一现代化标准来组织其内部的元数据。
import json
# PyDICOM原生支持将元数据导出为DICOM JSON
ds = pydicom.dcmread(dicom_files[0], stop_before_pixels=True)
dicom_json_str = ds.to_json()
# 解析并展示部分关键字段
json_data = json.loads(dicom_json_str)
# 字段的键是其十六进制的Tag编码
key_tags = ["00100010", "00080021", "00200013"] # PatientName, SeriesDate, InstanceNumber
for tag in key_tags:
if tag in json_data:
# VR (Value Representation) 字段描述了值的类型
vr = json_data[tag]["vr"]
value = json_data[tag].get("Value", ["N/A"])[0]
print(f"Tag {tag} (VR: {vr}): {value}")通过采用DICOM JSON标准,DiCube确保了其元数据的互操作性和未来兼容性。
3. DicomMeta的核心设计:共享与非共享元数据的分离
DicomMeta 是 DiCube 中负责高效管理元数据的核心类。它通过共享/非共享元数据分离机制直击传统 DICOM 的冗余痛点。
- 共享 (Shared) 元数据: 在整个图像序列中保持不变的信息,如
PatientID、StudyInstanceUID、Modality等。这部分数据只存储一次。 - 非共享 (Per-Slice) 元数据: 每个切片独有的信息,如
InstanceNumber、ImagePositionPatient、SliceLocation等。这部分数据会为每个切片单独存储。
当从DICOM文件夹加载数据时,DiCube会自动分析并分离这两类元数据。
import dicube
# 从DICOM文件夹加载数据,DiCube在内部自动完成元数据分离
dcb_image = dicube.load_from_dicom_folder(dicom_dir)
dicube.save(dcb_image, "temp_demo.dcbs")
# 获取DicomMeta对象有两种方式:
# 方式1:直接从DiCube文件加载元数据
meta = dicube.load_meta("temp_demo.dcbs")
# 方式2:从已加载的DiCubeImage对象中获取
meta = dcb_image.dicom_meta
# display() 方法可以清晰地展示元数据的分层结构
meta.display()这种智能分离机制带来了立竿见影的好处:元数据存储占用急剧减少,并且访问序列级信息(如患者姓名)时无需遍历,实现了O(1)时间复杂度的查找。
from dicube.dicom import CommonTags
# 检查共享元数据
patient_name = meta.get_shared_value(CommonTags.PatientName)
is_shared = meta.is_shared(CommonTags.PatientName)
print(f"PatientName: '{patient_name}' (是否为共享数据: {is_shared})")
# 检查非共享元数据
# get_values 会返回一个包含所有切片该字段值的列表
instance_numbers = meta.get_values(CommonTags.InstanceNumber)
is_shared_instance = meta.is_shared(CommonTags.InstanceNumber)
print(f"InstanceNumber (是否为共享数据: {is_shared_instance})")
print(f"共找到 {len(instance_numbers)} 个InstanceNumber")
print(f"范围从: {min(instance_numbers)} 到 {max(instance_numbers)}")4. 极致压缩:JSON + Zstandard
分离元数据解决了冗余,但如何高效地存储这些结构化的文本信息呢?DiCube 选择了由Facebook开发的现代化压缩算法 Zstandard (zstd)。
Zstd 擅长压缩具有重复模式的结构化文本数据(如 JSON),在压缩比与解压速度上均显著优于传统 gzip。
# 估算原始DICOM头文件的总大小
dicom_header_total_size = 0
all_files = list(Path(dicom_dir).glob("*"))
for dcm_file in all_files:
# 获取文件总大小
total_size = os.path.getsize(dcm_file)
# 读取DICOM文件获取pixel_array大小
ds = pydicom.dcmread(dcm_file)
if hasattr(ds, 'pixel_array'):
pixel_size = ds.pixel_array.nbytes
else:
pixel_size = 0
# 头文件大小 = 总大小 - pixel_array大小
header_size = total_size - pixel_size
dicom_header_total_size += header_size
# 获取DiCube压缩后的元数据大小
meta_json_str = meta.to_json()
import zstandard as zstd
compressor = zstd.ZstdCompressor(level=9) # 使用较高的压缩级别
compressed_meta = compressor.compress(meta_json_str.encode('utf-8'))
dicube_meta_size = len(compressed_meta)
print(f"原始DICOM头文件总大小 (估算): {dicom_header_total_size / 1024:.2f} KB")
print(f"DiCube Zstd压缩后元数据大小: {dicube_meta_size / 1024:.2f} KB")
print(f"元数据压缩比高达: {dicom_header_total_size / dicube_meta_size:.1f}x")超过 80 倍的压缩比!这表明 DicomMeta 的共享机制与 zstd 压缩形成了完美的组合拳,将元数据存储开销降至最低。
6. 性能的飞跃:秒级与毫秒级的较量
DicomMeta 的设计优势最终体现在了性能上。让我们来实际对比一下,从一个包含200个切片的序列中读取所有 InstanceNumber 所需的时间。
- 传统方式:需要循环打开、解析200个独立的文件。
- DiCube方式:只需打开1个文件,解压一小块元数据,并直接访问解析好的列表。
import time
# 传统DICOM方式:遍历所有文件
start_time = time.time()
dicom_instance_numbers = []
for dcm_file in all_files:
ds = pydicom.dcmread(dcm_file, stop_before_pixels=True)
dicom_instance_numbers.append(int(ds.InstanceNumber))
dicom_time = time.time() - start_time
# DiCube方式:一次性加载
start_time = time.time()
dicube_meta = dicube.load_meta("temp_demo.dcbs")
dicube_instance_numbers = dicube_meta.get_values(CommonTags.InstanceNumber)
dicube_time = time.time() - start_time
print(f"传统DICOM I/O耗时: {dicom_time * 1000:.2f} 毫秒")
print(f"DiCube DicomMeta 耗时: {dicube_time * 1000:.2f} 毫秒")
print(f"\n性能提升: {dicom_time / dicube_time:.1f} 倍")
# 清理临时文件
os.remove("temp_demo.dcbs")几十倍的性能提升是显而易见的。这背后是 I/O 模式的根本性变革:从多次、琐碎的文件操作,转变为一次性、集中的数据块读取。
7. 总结
DiCube 的 DicomMeta 机制并非简单的元数据存储,而是一套针对现代医学影像数据处理特点精心设计的解决方案。其核心优势在于:
- 智能去冗余:通过分离共享与非共享元数据,从根源上解决了DICOM的冗余问题,大幅减少存储占用。
- 拥抱现代化标准:基于DICOM JSON标准,并采用高效的Zstandard压缩,确保了兼容性与极致性能。
- 数量级的性能提升:将元数据读取从“遍历文件系统”的慢速操作,转变为“内存访问”级别的高速操作,为大规模数据分析和AI训练扫清了障碍。
这些设计共同构成了 DiCube 处理医学影像数据的坚实基础,使其在效率和易用性上远超传统的工作流。