mcpField
P
pydicom-medical-imaging

pydicom-medical-imaging

github.com/jaechang-hits数据与数据库384101 次安装6 天前更新
代码仓库

关于 pydicom-medical-imaging

pydicom-medical-imaging 是面向医学影像开发者和研究人员的 Python DICOM 工作流技能,涵盖元数据与像素数据访问、匿名化、格式转换及影像序列处理为 3D 数组。

主要功能

  1. 01读取和修改 DICOM 文件
  2. 02提取元数据和像素数组
  3. 03转换为 PNG、JPEG 或 TIFF
  4. 04移除可识别个人身份的信息
  5. 05将影像序列构建为 3D 数组

适用场景

  1. 01批量提取 DICOM 目录元数据
  2. 02准备影像以供可视化
  3. 03对患者文件进行去标识化
  4. 04将 CT 或 MRI 序列处理为体积

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill pydicom-medical-imaging

安装后在 Claude Code 中输入/pydicom-medical-imaging确认已加载。

附带文件references/dicom_standards.md

详细介绍

Pydicom 医学影像

概述

Pydicom 是一个纯 Python 库,可用于读取、写入和修改 DICOM(医学数字成像和通信)文件。它提供对 DICOM 元数据标签和 NumPy 数组形式像素数据的访问,支持 CT、MRI、X 射线、超声及其他医学影像模态。该库可处理压缩和未压缩的传输语法,并支持通过可选编解码器插件进行解码。

适用场景

  • 读取 DICOM 文件并提取元数据(患者信息、检查参数、成像设置)
  • 从 DICOM 图像中提取像素数据,以进行分析或可视化
  • 将 DICOM 图像转换为标准格式(PNG、JPEG、TIFF)
  • 通过移除受保护的健康信息(PHI)对 DICOM 文件进行匿名化处理
  • 修改 DICOM 元数据标签,以重新标记或更正数据
  • 从头创建 DICOM 文件(例如,将 NumPy 数组封装为 DICOM)
  • 将 CT/MRI 序列处理为 3D 体积数组,以进行重建
  • 从多帧 DICOM(电影/视频)中提取帧
  • 对于全切片病理图像(SVS、NDPI),请改用 histolab-wsi-processing
  • 对于 NIfTI 神经影像体积(.nii/.nii.gz),请改用 nibabel

先决条件

  • Python 包:pydicom、numpy、pillow
  • 可选编解码器:pylibjpeg + pylibjpeg-libjpeg(JPEG)、pylibjpeg-openjpeg(JPEG 2000)、python-gdcm(大多数格式)
  • 数据格式:符合 NEMA PS3.10 标准的 DICOM 文件(.dcm、.ima 或无扩展名)
pip install pydicom numpy pillow

# Optional: compression codec handlers (install as needed)
pip install pylibjpeg pylibjpeg-libjpeg   # JPEG Baseline/Lossless
pip install pylibjpeg-openjpeg             # JPEG 2000
pip install python-gdcm                    # Comprehensive codec support

快速开始

import pydicom
import numpy as np

# Read a DICOM file
ds = pydicom.dcmread("scan.dcm")

# Access metadata
print(f"Patient: {ds.PatientName}, Modality: {ds.Modality}")
print(f"Size: {ds.Rows}x{ds.Columns}, Bits: {ds.BitsAllocated}")

# Extract pixel data as NumPy array
pixels = ds.pixel_array
print(f"Pixel array shape: {pixels.shape}, dtype: {pixels.dtype}")

# Apply windowing for display (CT/MR)
from pydicom.pixel_data_handlers.util import apply_voi_lut
display = apply_voi_lut(pixels, ds)
print(f"Windowed range: [{display.min()}, {display.max()}]")

核心 API

模块 1:读取和访问元数据

使用属性名称或标签表示法读取 DICOM 文件并访问元数据。

import pydicom

# Read DICOM file (defer_size delays loading large elements)
ds = pydicom.dcmread("scan.dcm")
ds_lazy = pydicom.dcmread("large.dcm", defer_size="1 KB")

# Access by attribute name (standard DICOM keywords)
print(f"Patient Name: {ds.PatientName}")
print(f"Study Date: {ds.StudyDate}")
print(f"Modality: {ds.Modality}")
print(f"Image Size: {ds.Rows} x {ds.Columns}")

# Access by tag number (group, element)
print(f"Patient ID: {ds[0x0010, 0x0020].value}")

# Safe access with getattr (avoids AttributeError)
slice_thick = getattr(ds, 'SliceThickness', 'N/A')
print(f"Slice Thickness: {slice_thick}")

# Iterate all elements
for elem in ds:
    if elem.VR != 'SQ':  # Skip sequences
        print(f"  {elem.tag} {elem.keyword}: {elem.value}")
# Read DICOM directory (DICOMDIR)
from pydicom.filereader import dcmread

dicomdir = pydicom.dcmread("DICOMDIR")
for record in dicomdir.DirectoryRecordSequence:
    if record.DirectoryRecordType == "IMAGE":
        ref_file = record.ReferencedFileID
        # ref_file is a list of path components
        print(f"Image file: {'/'.join(ref_file)}")
模块 2:提取像素数据

将像素数据提取为 NumPy 数组,支持灰度、彩色、窗宽窗位和多帧数据。

import pydicom
import numpy as np
from pydicom.pixel_data_handlers.util import apply_voi_lut, apply_modality_lut

ds = pydicom.dcmread("ct_scan.dcm")

# Basic pixel extraction
pixels = ds.pixel_array  # NumPy ndarray
print(f"Shape: {pixels.shape}, dtype: {pixels.dtype}")

# Apply Modality LUT (rescale to Hounsfield Units for CT)
hu_pixels = apply_modality_lut(pixels, ds)
print(f"HU range: [{hu_pixels.min()}, {hu_pixels.max()}]")

# Apply VOI LUT (windowing for display contrast)
display = apply_voi_lut(hu_pixels, ds)
print(f"Display range: [{display.min()}, {display.max()}]")

# Manual windowing (when VOI LUT metadata is absent)
center, width = 40, 400  # Soft tissue window
lower = center - width / 2
upper = center + width / 2
windowed = np.clip(hu_pixels, lower, upper)
print(f"Manual window [{lower}, {upper}]")
# Color images (ultrasound, photos) — handle YBR color space
import pydicom

ds = pydicom.dcmread("ultrasound.dcm")
pixels = ds.pixel_array
print(f"Color shape: {pixels.shape}")  # (rows, cols, 3)

# Convert YBR to RGB if needed
photo_interp = ds.PhotometricInterpretation
if "YBR" in photo_interp:
    from pydicom.pixel_data_handlers.util import convert_color_space
    rgb = convert_color_space(pixels, photo_interp, "RGB")
    print(f"Converted {photo_interp} -> RGB")

# Multi-frame (cine/video DICOM)
ds_multi = pydicom.dcmread("cine.dcm")
frames = ds_multi.pixel_array  # Shape: (num_frames, rows, cols)
print(f"Frames: {frames.shape[0]}, Frame size: {frames.shape[1:]}")
模块 3:图像转换

将 DICOM 像素数据转换为标准图像格式,以便可视化和导出。

import pydicom
import numpy as np
from PIL import Image
from pydicom.pixel_data_handlers.util import apply_voi_lut

ds = pydicom.dcmread("scan.dcm")
pixels = ds.pixel_array

# Apply windowing
display = apply_voi_lut(pixels, ds)

# Normalize to 8-bit for standard image formats
if display.dtype != np.uint8:
    dmin, dmax = display.min(), display.max()
    if dmax > dmin:
        normalized = ((display - dmin) / (dmax - dmin) * 255).astype(np.uint8)
    else:
        normalized = np.zeros_like(display, dtype=np.uint8)
else:
    normalized = display

# Save as PNG
img = Image.fromarray(normalized)
img.save("output.png")
print(f"Saved output.png ({img.size[0]}x{img.size[1]})")

# Save as JPEG with quality control
img.save("output.jpg", quality=95)
# Batch conversion: directory of DICOM files to PNG
import pydicom
import numpy as np
from PIL import Image
from pathlib import Path
from pydicom.pixel_data_handlers.util import apply_voi_lut

def dicom_to_image(dcm_path, out_path, fmt="PNG"):
    """Convert a single DICOM file to standard image format."""
    ds = pydicom.dcmread(str(dcm_path))
    pixels = apply_voi_lut(ds.pixel_array, ds)
    dmin, dmax = float(pixels.min()), float(pixels.max())
    if dmax > dmin:
        norm = ((pixels - dmin) / (dmax - dmin) * 255).astype(np.uint8)
    else:
        norm = np.zeros_like(pixels, dtype=np.uint8)
    Image.fromarray(norm).save(str(out_path))

dcm_dir = Path("dicom_files/")
out_dir = Path("images/")
out_dir.mkdir(exist_ok=True)

for dcm_file in sorted(dcm_dir.glob("*.dcm")):
    out_file = out_dir / f"{dcm_file.stem}.png"
    dicom_to_image(dcm_file, out_file)
    print(f"Converted: {dcm_file.name} -> {out_file.name}")
模块 4:修改元数据和匿名化处理

修改 DICOM 属性并移除受保护的健康信息,以进行去标识化处理。

import pydicom
from pydicom.uid import generate_uid

ds = pydicom.dcmread("original.dcm")

# Modify attributes
ds.PatientName = "Anonymous"
ds.PatientID = "ANON001"
ds.InstitutionName = "Research Lab"

# Add new attribute
ds.add_new(0x00081030, 'LO', 'Research Study')  # Study Description

# Delete attribute
if 'PatientBirthDate' in ds:
    del ds.PatientBirthDate

# Generate new UIDs for de-identification
ds.StudyInstanceUID = generate_uid()
ds.SeriesInstanceUID = generate_uid()
ds.SOPInstanceUID = generate_uid()

# Save modified file (preserves original)
ds.save_as("modified.dcm")
print(f"Saved modified.dcm with new UIDs")
# PHI anonymization: remove patient-identifying tags (DICOM PS3.15 Annex E)
import pydicom
from pydicom.uid import generate_uid

PHI_TAGS = [  # Core set — extend per institutional policy
    'PatientName', 'PatientID', 'PatientBirthDate', 'PatientSex',
    'PatientAge', 'PatientWeight', 'PatientAddress',
    'OtherPatientIDs', 'OtherPatientNames',
    'InstitutionName', 'InstitutionAddress',
    'ReferringPhysicianName', 'PerformingPhysicianName',
    'OperatorsName', 'StudyID', 'AccessionNumber',
]

def anonymize_dicom(ds, prefix="ANON"):
    """Remove PHI tags and assign anonymous identifiers."""
    for tag in PHI_TAGS:
        if hasattr(ds, tag): delattr(ds, tag)
    ds.PatientName, ds.PatientID = f"{prefix}_Patient", f"{prefix}_ID"
    ds.StudyInstanceUID = generate_uid()
    ds.SeriesInstanceUID = generate_uid()
    ds.SOPInstanceUID = generate_uid()
    return ds

ds = pydicom.dcmread("patient_scan.dcm")
anonymize_dicom(ds, prefix="STUDY001").save_as("anonymized.dcm")
print("Anonymized: PHI tags removed, UIDs replaced")
模块 5:从头编写 DICOM

使用 NumPy 数组创建具有适当元数据的新 DICOM 文件。

import pydicom, numpy as np, datetime
from pydicom.dataset import FileDataset, FileMetaDataset
from pydicom.uid import ExplicitVRLittleEndian, generate_uid

# File meta header
file_meta = FileMetaDataset()
file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2'  # CT Image Storage
file_meta.MediaStorageSOPInstanceUID = generate_uid()
file_meta.TransferSyntaxUID = ExplicitVRLittleEndian

# Dataset with required attributes
ds = FileDataset("new.dcm", {}, file_meta=file_meta, preamble=b"\x00" * 128)
ds.SOPClassUID = file_meta.MediaStorageSOPClassUID
ds.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID
ds.StudyInstanceUID, ds.SeriesInstanceUID = generate_uid(), generate_uid()
ds.Modality, ds.Manufacturer = 'CT', 'Research'
ds.is_little_endian, ds.is_implicit_VR = True, False
dt = datetime.datetime.now()
ds.ContentDate, ds.ContentTime = dt.strftime('%Y%m%d'), dt.strftime('%H%M%S.%f')

# Pixel data from NumPy array
pixels = np.random.randint(0, 4096, (512, 512), dtype=np.uint16)
ds.Rows, ds.Columns = pixels.shape
ds.BitsAllocated, ds.BitsStored, ds.HighBit = 16, 12, 11
ds.PixelRepresentation = 0  # Unsigned
ds.SamplesPerPixel, ds.PhotometricInterpretation = 1, 'MONOCHROME2'
ds.PixelData = pixels.tobytes()

ds.save_as("new.dcm")
print(f"Created DICOM: {ds.Rows}x{ds.Columns}, {ds.BitsStored}-bit")
模块 6:序列处理和 3D 体积

加载 DICOM 序列,按空间位置排序,并堆叠成 3D NumPy 数组。

import pydicom
import numpy as np
from pathlib import Path

def load_dicom_series(series_dir):
    """Load and sort a DICOM series by slice position."""
    dcm_files = []
    for f in Path(series_dir).iterdir():
        try:
            ds = pydicom.dcmread(str(f))
            dcm_files.append(ds)
        except Exception:
            continue  # Skip non-DICOM files

    if not dcm_files:
        raise ValueError(f"No DICOM files found in {series_dir}")

    # Sort by ImagePositionPatient (z-coordinate) or InstanceNumber
    try:
        dcm_files.sort(key=lambda x: float(x.ImagePositionPatient[2]))
    except (AttributeError, IndexError):
        dcm_files.sort(key=lambda x: int(x.InstanceNumber))

    print(f"Loaded {len(dcm_files)} slices, "
          f"Series: {getattr(dcm_files[0], 'SeriesDescription', 'N/A')}")
    return dcm_files

def series_to_volume(dcm_files):
    """Stack sorted DICOM slices into a 3D NumPy array."""
    from pydicom.pixel_data_handlers.util import apply_modality_lut
    slices = []
    for ds in dcm_files:
        pixels = apply_modality_lut(ds.pixel_array, ds)
        slices.append(pixels)
    volume = np.stack(slices, axis=0)
    # Calculate voxel spacing
    pixel_spacing = dcm_files[0].PixelSpacing
    if len(dcm_files) > 1:
        try:
            z0 = float(dcm_files[0].ImagePositionPatient[2])
            z1 = float(dcm_files[1].ImagePositionPatient[2])
            slice_spacing = abs(z1 - z0)
        except (AttributeError, IndexError):
            slice_spacing = float(getattr(dcm_files[0], 'SliceThickness', 1.0))
    else:
        slice_spacing = float(getattr(dcm_files[0], 'SliceThickness', 1.0))
    spacing = (slice_spacing, float(pixel_spacing[0]), float(pixel_spacing[1]))
    print(f"Volume shape: {volume.shape}, Spacing (z,y,x): {spacing} mm")
    return volume, spacing

# Usage
dcm_files = load_dicom_series("ct_series/")
volume, spacing = series_to_volume(dcm_files)
print(f"HU range: [{volume.min()}, {volume.max()}]")

核心概念

DICOM 数据模型

DICOM 以四级层次结构组织医学影像数据:

层级关键 UID描述
患者PatientID单个患者
检查StudyInstanceUID一次成像检查(可能包含多个模态)
序列SeriesInstanceUID一组采集序列(例如,T1 加权 MRI)
实例(图像)SOPInstanceUID一张图像/一帧(一个 DICOM 文件)

每个 DICOM 文件包含一个实例,其元数据标签按组组织。标签使用 (group, element) 表示法(例如,PatientName 对应 (0010,0010))。

传输语法和压缩

传输语法定义了 DICOM 数据的编码方式(字节序、VR 编码、像素压缩):

传输语法UID压缩所需处理器
隐式 VR 小端1.2.840.10008.1.2无否
显式 VR 小端1.2.840.10008.1.2.1无否
显式 VR 大端1.2.840.10008.1.2.2无否
JPEG 基线1.2.840.10008.1.2.4.50有损 JPEGpylibjpeg
JPEG 无损1.2.840.10008.1.2.4.70无损 JPEGpylibjpeg
JPEG 2000 无损1.2.840.10008.1.2.4.90无损 J2Kpylibjpeg-openjpeg
JPEG 20001.2.840.10008.1.2.4.91有损 J2Kpylibjpeg-openjpeg
RLE 无损1.2.840.10008.1.2.5RLEpydicom(内置)

检查传输语法:ds.file_meta.TransferSyntaxUID。访问压缩文件的 pixel_array 前,请先安装相应的处理器。

常用 DICOM 标签

最常访问的标签(完整目录见 references/dicom_standards.md):

标签关键字VR描述
(0008,0060)ModalityCSCT、MR、US、CR、DX、PT、NM
(0010,0010)PatientNamePN患者全名
(0010,0020)PatientIDLO患者标识符
(0008,0020)StudyDateDA检查日期(YYYYMMDD)
(0020,000D)StudyInstanceUIDUI检查唯一标识符
(0020,0013)InstanceNumberIS序列中的图像编号
(0020,0032)ImagePositionPatientDSx、y、z 位置(mm)
(0028,0010)RowsUS图像像素高度
(0028,0011)ColumnsUS图像像素宽度
(0028,0030)PixelSpacingDS行、列间距(mm)
(0028,1050)WindowCenterDS显示窗中心
(0028,0004)PhotometricInterpretationCSMONOCHROME1/2、RGB、YBR_FULL
(0028,0100)BitsAllocatedUS8 或 16
值表示(VR)

VR 定义每个元素的数据类型。最常见的类型(完整表格见 references/dicom_standards.md):

VR名称Python 类型示例
CS代码字符串str"CT"、"MR"
DA日期str"20240115"
DS十进制字符串DSfloat"1.5"
IS整数字符串IS"42"
LO长字符串str"Study description"
PN人名PersonName"Doe^John"
SQ序列Sequence嵌套数据集
UI唯一标识符UID"1.2.840..."
US无符号短整数int512

常见工作流

工作流 1:批量提取元数据到 CSV

目标:遍历包含 DICOM 文件的目录,提取关键元数据字段,并导出为 CSV 清单。

import pydicom
import pandas as pd
from pathlib import Path

FIELDS = ['PatientID', 'Modality', 'StudyDate', 'SeriesDescription',
          'StudyInstanceUID', 'SeriesInstanceUID', 'InstanceNumber',
          'Rows', 'Columns', 'SliceThickness', 'BitsStored']

def extract_metadata(dcm_path):
    """Extract key metadata from a DICOM file."""
    try:
        ds = pydicom.dcmread(str(dcm_path), stop_before_pixels=True)
    except Exception as e:
        return {"file": str(dcm_path), "error": str(e)}
    rec = {"file": str(dcm_path)}
    for f in FIELDS:
        rec[f] = str(getattr(ds, f, ''))
    return rec

# Scan directory recursively
dicom_dir = Path("dicom_archive/")
records = [extract_metadata(f) for f in sorted(dicom_dir.rglob("*")) if f.is_file()]

df = pd.DataFrame(records)
df.to_csv("dicom_manifest.csv", index=False)
print(f"Extracted metadata from {len(df)} files")
print(f"Modalities: {df['Modality'].value_counts().to_dict()}")
print(f"Unique patients: {df['PatientID'].nunique()}")
工作流 2:将 CT 序列转换为 3D 体数据并可视化

目标:加载 CT 序列,构建以 Hounsfield 单位表示的 3D 体数据,并显示轴向、矢状和冠状视图。

import pydicom, numpy as np, matplotlib.pyplot as plt
from pathlib import Path
from pydicom.pixel_data_handlers.util import apply_modality_lut

# Load and sort series by z-position
dcm_files = []
for f in sorted(Path("ct_series/").glob("*")):
    try: dcm_files.append(pydicom.dcmread(str(f)))
    except Exception: continue
dcm_files.sort(key=lambda x: float(x.ImagePositionPatient[2]))

# Stack into 3D volume (Hounsfield Units)
volume = np.stack([apply_modality_lut(ds.pixel_array, ds) for ds in dcm_files])
ps = dcm_files[0].PixelSpacing
z_sp = abs(float(dcm_files[1].ImagePositionPatient[2])
           - float(dcm_files[0].ImagePositionPatient[2]))
print(f"Volume: {volume.shape}, spacing: {z_sp:.2f}x{float(ps[0]):.2f}x{float(ps[1]):.2f} mm")

# Display orthogonal views (soft tissue window)
vmin, vmax = -160, 240  # center=40, width=400
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
mid = [s // 2 for s in volume.shape]
axes[0].imshow(volume[mid[0]], cmap='gray', vmin=vmin, vmax=vmax)
axes[0].set_title(f"Axial (slice {mid[0]})")
axes[1].imshow(volume[:, mid[1], :], cmap='gray', vmin=vmin, vmax=vmax,
               aspect=z_sp/float(ps[1]))
axes[1].set_title(f"Coronal")
axes[2].imshow(volume[:, :, mid[2]], cmap='gray', vmin=vmin, vmax=vmax,
               aspect=z_sp/float(ps[0]))
axes[2].set_title(f"Sagittal")
for ax in axes: ax.axis('off')
plt.tight_layout()
plt.savefig("orthogonal_views.png", dpi=150, bbox_inches='tight')
print("Saved orthogonal_views.png")
工作流 3:批量匿名化流程

目标:匿名化目录中的所有 DICOM 文件,并使用新的 UID 保留序列结构。

import pydicom
from pydicom.uid import generate_uid
from pathlib import Path

PHI_TAGS = [
    'PatientName', 'PatientID', 'PatientBirthDate', 'PatientSex',
    'PatientAge', 'PatientWeight', 'PatientAddress',
    'OtherPatientIDs', 'OtherPatientNames',
    'InstitutionName', 'InstitutionAddress',
    'ReferringPhysicianName', 'PerformingPhysicianName',
    'OperatorsName', 'PhysiciansOfRecord', 'StudyID', 'AccessionNumber',
]

uid_map = {}  # Preserves study/series relationships across files
def get_mapped_uid(uid):
    if uid not in uid_map: uid_map[uid] = generate_uid()
    return uid_map[uid]

input_dir, output_dir = Path("original_dicoms/"), Path("anonymized_dicoms/")
output_dir.mkdir(exist_ok=True)
count, errors = 0, 0

for dcm_path in sorted(input_dir.rglob("*")):
    if not dcm_path.is_file(): continue
    try: ds = pydicom.dcmread(str(dcm_path))
    except Exception: errors += 1; continue

    for tag in PHI_TAGS:
        if hasattr(ds, tag): delattr(ds, tag)
    ds.PatientName, ds.PatientID = "ANONYMOUS", "ANON"
    ds.StudyInstanceUID = get_mapped_uid(ds.StudyInstanceUID)
    ds.SeriesInstanceUID = get_mapped_uid(ds.SeriesInstanceUID)
    ds.SOPInstanceUID = generate_uid()
    ds.save_as(str(output_dir / f"anon_{count:06d}.dcm"))
    count += 1

print(f"Anonymized {count} files, {errors} errors, {len(uid_map)} UIDs mapped")

关键参数

参数模块默认值范围 / 选项作用
defer_sizedcmreadNone"1 KB"、"1 MB"、整数(字节)延迟加载大于指定大小的元素
stop_before_pixelsdcmreadFalseTrue/False跳过像素数据加载(仅元数据)
forcedcmreadFalseTrue/False即使缺少 DICOM 前导码也强制读取
WindowCenter窗宽窗位来自文件任意数值显示窗中心(CT 中为 HU)
WindowWidth窗宽窗位来自文件> 0显示窗宽度
BitsAllocated写入168、16、32每像素分配的位数
BitsStored写入121-BitsAllocated实际有效位数
PixelRepresentation写入00(无符号)、1(有符号)像素值的符号属性
PhotometricInterpretation写入"MONOCHROME2"MONOCHROME1、MONOCHROME2、RGB、YBR_FULL色彩空间
TransferSyntaxUID写入Explicit VR LE参见传输语法表编码格式

最佳实践

  1. 仅处理元数据时使用 stop_before_pixels=True:避免在仅读取标签时加载大型像素数组。在批量提取元数据时,这能显著加快速度。

    ds = pydicom.dcmread("scan.dcm", stop_before_pixels=True)
    
  2. 对于可选标签,始终使用带默认值的 getattr():DICOM 文件中包含哪些标签差异很大。直接访问属性时,如果标签缺失,就会引发 AttributeError。

    # Good
    thickness = getattr(ds, 'SliceThickness', None)
    # Bad — will crash on files without SliceThickness
    thickness = ds.SliceThickness
    
  3. 处理 CT 数据时,先应用 Modality LUT,再进行窗宽窗位调整:原始像素值是存储值;先应用 apply_modality_lut() 将其转换为 Hounsfield 单位,再应用 apply_voi_lut() 以供显示。

  4. 访问压缩像素数据前先安装压缩处理器:检查 ds.file_meta.TransferSyntaxUID 并安装相应的处理器。如果未安装处理器就尝试访问 pixel_array,会引发 RuntimeError。

  5. 每次修改文件后都生成新的 UID:修改后绝不要重复使用原始 SOPInstanceUID。使用 pydicom.uid.generate_uid() 确保 UID 全局唯一。

  6. 使用 save_as(),而不是覆盖原文件:始终保存到新路径,以保留原始数据。DICOM 存档可能会进行完整性检查,若原文件被就地修改,检查可能会失败。

  7. 进行 3D 重建时,按 ImagePositionPatient 排序序列:InstanceNumber 并不总是可靠。ImagePositionPatient[2](z 坐标)可确保轴位 CT/MR 序列按正确的物理顺序排列。

常用方法

方法:处理压缩与解压缩

适用场景:读取压缩的 DICOM 文件,或压缩未压缩的文件。

import pydicom

ds = pydicom.dcmread("compressed.dcm")
ts = ds.file_meta.TransferSyntaxUID

# Check if compressed
print(f"Transfer Syntax: {ts}")
print(f"Compressed: {ts.is_compressed}")

# Decompress in-place (requires appropriate handler installed)
if ts.is_compressed:
    ds.decompress()
    print(f"Decompressed to {ds.file_meta.TransferSyntaxUID}")

# Access pixel data (works after decompression)
pixels = ds.pixel_array
print(f"Pixel shape: {pixels.shape}")
方法:处理 DICOM 序列

适用场景:访问嵌套数据结构,例如引用序列、手术代码或协议元素。

import pydicom

ds = pydicom.dcmread("structured.dcm")

# Access sequence elements (SQ VR = list of datasets)
if hasattr(ds, 'ReferencedStudySequence'):
    for item in ds.ReferencedStudySequence:
        print(f"  Referenced Study: {item.ReferencedSOPInstanceUID}")

# Access procedure code sequence
if hasattr(ds, 'ProcedureCodeSequence'):
    for code in ds.ProcedureCodeSequence:
        print(f"  Procedure: {code.CodeMeaning} ({code.CodeValue})")

# Create a sequence when writing DICOM
from pydicom.dataset import Dataset
from pydicom.sequence import Sequence

ref_item = Dataset()
ref_item.ReferencedSOPClassUID = '1.2.840.10008.5.1.4.1.1.2'
ref_item.ReferencedSOPInstanceUID = pydicom.uid.generate_uid()
ds.ReferencedImageSequence = Sequence([ref_item])
方法:提取多帧图像

适用场景:从电影、视频或增强型多帧 DICOM 文件中提取单独的帧。

import pydicom
import numpy as np
from PIL import Image
from pathlib import Path

ds = pydicom.dcmread("multiframe.dcm")
frames = ds.pixel_array  # Shape: (num_frames, rows, cols) or (num_frames, rows, cols, 3)
num_frames = frames.shape[0]
print(f"Total frames: {num_frames}, Frame size: {frames.shape[1:]}")

# Extract all frames as images
out_dir = Path("frames/")
out_dir.mkdir(exist_ok=True)

for i in range(num_frames):
    frame = frames[i]
    # Normalize to uint8
    if frame.dtype != np.uint8:
        fmin, fmax = frame.min(), frame.max()
        if fmax > fmin:
            frame = ((frame - fmin) / (fmax - fmin) * 255).astype(np.uint8)
        else:
            frame = np.zeros_like(frame, dtype=np.uint8)
    Image.fromarray(frame).save(out_dir / f"frame_{i:04d}.png")

print(f"Extracted {num_frames} frames to {out_dir}/")

故障排查

问题原因解决方案
RuntimeError: No available image handler压缩传输语法未安装编解码器安装相应的处理器:pip install pylibjpeg pylibjpeg-libjpeg(JPEG)、pip install pylibjpeg-openjpeg(JPEG 2000),或 pip install python-gdcm(所有格式)
AttributeError: 'Dataset' has no attribute 'X'文件中不存在可选标签使用 getattr(ds, 'X', default),或在访问前检查 'X' in ds
InvalidDicomError: File is missing DICOM preamble非标准 DICOM 文件或非 DICOM 文件尝试使用 pydicom.dcmread(path, force=True) 跳过前导码检查
像素值错误(没有负 HU 值)未应用 Modality LUT在分析前应用 apply_modality_lut(pixels, ds)——原始存储值与实际 HU 值不同
图像显示反转(明暗颠倒)MONOCHROME1 光度解释检查 ds.PhotometricInterpretation;对于 MONOCHROME1,使用 np.max(pixels) - pixels 进行反转
加载大型序列时出现 MemoryError一次性将所有切片加载到内存中分批处理切片;扫描元数据时使用 stop_before_pixels=True;对于大型元素,使用 defer_size
3D 体数据的切片顺序不一致按 InstanceNumber 而非位置排序按 ImagePositionPatient[2] 排序,以确保物理顺序正确
PatientName 中出现乱码字符编码不匹配检查 SpecificCharacterSet 标签;pydicom 会自动解码,但有些文件的字符集声明不正确
设置 PixelData 时出现 TypeError像素数组的字节格式错误使用 pixel_array.tobytes(),并确保数据类型与 BitsAllocated 匹配(16 位使用 uint16)

捆绑资源

references/dicom_standards.md

整合的 DICOM 标签目录和传输语法参考。包含患者人口统计信息、检查/序列标识、图像几何信息、像素数据编码、窗宽窗位参数以及模态专用标签(CT 亨氏参数、MR 序列参数、设备标识、时间)的完整标签表。包含压缩类型和处理器安装信息在内的完整传输语法 UID 表。值表示(VR)类型参考。

  • 涵盖内容:common_tags.md 中的所有标签类别(患者、检查、序列、图像、像素、窗宽窗位、CT 专用、MR 专用、设备、时间);transfer_syntaxes.md 中的所有传输语法 UID、压缩格式和处理器映射;VR 类型目录
  • 迁移至其他部分的内容:核心概念中的关键标签表(最常见的 20 个);核心概念中的传输语法摘要表(最常见的 8 个);核心概念中的 VR 摘要表
  • 省略内容:分步处理器安装教程(已在先决条件和故障排除中介绍);对每个标签冗长的文字说明(表格内容清晰,无需额外说明)
原始文件处置情况(5 个文件):
  1. SKILL.md(434 行)——已迁移:概述、工作流、最佳实践、常见问题经过重组,纳入新的 SKILL.md 格式
  2. references/common_tags.md(229 行)——(b)已整合:关键的 20 个标签表纳入核心概念;完整目录纳入 references/dicom_standards.md
  3. references/transfer_syntaxes.md(353 行)——(b)已整合:摘要表(8 种语法)纳入核心概念;完整 UID 表和处理器详情纳入 references/dicom_standards.md
  4. scripts/anonymize_dicom.py(138 行)——(c)已吸收:PHI 标签列表和匿名化函数纳入核心 API 模块 4;批处理流水线纳入工作流 3
  5. scripts/dicom_to_image.py(173 行)——(c)已吸收:转换函数纳入核心 API 模块 3(dicom_to_image helper);批量转换纳入模块 3 的第二个代码块
  6. scripts/extract_metadata.py(174 行)——(c)已吸收:元数据提取函数纳入工作流 1(批量提取到 CSV)

相关技能

  • histolab-wsi-processing——全切片病理图像处理(SVS、NDPI);用于数字病理图块提取
  • nibabel (planned)——用于脑部 MRI 体积分析的 NIfTI 神经影像格式
  • matplotlib-scientific-plotting——用于 DICOM 图像和 3D 体积切片的出版级可视化

参考资料

评论

支持 Markdown