pydcm quickstart¶
import pydcm as pydicom mostly just works. Beyond that, one wheel mirrors a
whole stack of specialist tools — each section below is a runnable recipe.
Read & decode¶
import pydcm
ds = pydcm.dcmread("scan.dcm")
ds.PatientName # PersonName
ds.Rows, ds.PixelSpacing # 64, [0.3125, 0.3125]
ds.file_meta.TransferSyntaxUID
px = ds.pixel_array # NumPy — ANY transfer syntax, no codec plugins
decode() is the one-call path straight to an array:
arr = pydcm.decode("scan.dcm") # [frames, rows, cols(, samples)]
hu = pydcm.decode("ct.dcm", rescale=True) # float32 Hounsfield Units (modality LUT)
arr, meta = pydcm.decode("scan.dcm", with_meta=True) # geometry sidecar, no extra read
Every transfer syntax built in — JPEG-2000, JPEG-XL, HTJ2K and more
The decoder is compiled into the wheel (openjpeg / charls / openjph /
libjxl statically linked), so pixel_array / decode() read any
transfer syntax with no plugins — JPEG-2000, JPEG-LS, RLE,
baseline / lossless JPEG, and the newest JPEG-XL and
HTJ2K / High-Throughput JPEG 2000. Plugin-less readers raise on these;
pydcm just returns the array. And it encodes them too:
import pydcm
from pydcm.uid import JPEGXLLossless, HTJ2KLossless
ds = pydcm.dcmread("ct.dcm")
ds.compress(JPEGXLLossless) # transcode in place to JPEG-XL (lossless)
ds.save_as("ct_jxl.dcm")
ds = pydcm.dcmread("ct.dcm")
ds.compress(HTJ2KLossless) # …or High-Throughput JPEG 2000
ds.save_as("ct_htj2k.dcm")
Reads real-world vendor text — where strict decoders give up
Character-set handling is where exports from different countries and vendors
diverge most: inconsistent SpecificCharacterSet declarations, malformed
ISO 2022 escape sequences, non-standard charset aliases, mixed or missing
encodings. pydcm's native text engine covers the full range — Latin /
Cyrillic / Greek /
Arabic / Hebrew / Thai, Japanese (Shift-JIS, ISO 2022 IR 87 / 159),
Korean (EUC-KR), Chinese (GB18030 / GBK), UTF-8 — and decodes it
adaptively: lenient alias matching, tolerance of broken escape sequences,
and a fault-tolerant fallback. A PatientName that a strict iconv-based
reader rejects comes back as a correct Python
string. Every text value is decoded to UTF-8 at read time, so you never
touch encodings by hand.
Edit — byte-verbatim save¶
save_as patches the original file bytes, so Transfer Syntax, PixelData
(including compressed J2K / RLE), private tags and every untouched element
survive byte-for-byte:
ds = pydcm.dcmread("ct.dcm")
ds.PatientName = "Anon^Patient"
del ds.PatientBirthDate
ds.save_as("ct_anon.dcm") # only the named tags change
Editing patches the file, it doesn't rewrite it
Many DICOM writers re-encode the whole dataset on save — which can subtly change compressed PixelData, private tags or VR representation. pydcm edits the original bytes in place, so anything you didn't touch is byte-identical on disk. Ideal for PHI redaction: change the named tags, guarantee nothing else moved.
See Behaviour notes for the handful of deliberate behavioural differences.
A directory → 3D volume → NIfTI¶
vol = pydcm.load_series("ct_series/")
vol.pixels # [depth, rows, cols] float32 HU, spatially sorted
vol.spacing # (z, y, x) mm
vol.affine # 4×4 voxel→world
vol.to_nifti("ct.nii.gz") # validated affine (incl. gantry tilt)
Real-world values and geometry, computed in the engine
decode(rescale=True) returns the modality-LUT output — HU for CT — as
float32, and decode(with_meta=True) hands back the geometry the engine
already parsed (rescale, pixel spacing, image position / orientation, slice
thickness, window) with no extra read. load_series sorts slices by
position (IOP clustering + IPP projection) and returns the largest coherent
volume, so a stray localizer can't corrupt the stack — all in native code,
nothing re-derived in Python.
A directory → PyTorch¶
from torch.utils.data import DataLoader
ds = pydcm.DICOMDataset("study_dir/", to_torch=True) # torch stays optional
for batch in DataLoader(ds, batch_size=8, num_workers=4, shuffle=True):
... # [B, H, W] or [B, H, W, C]
DICOMweb (QIDO / WADO / STOW)¶
from pydcm import dicomweb
studies = dicomweb.search_studies("https://pacs.example.com", matches={"PatientID": "42"})
for part10 in dicomweb.iter_study("https://pacs.example.com", study_uid):
... # streaming retrieve, bounded memory
dicomweb.store_instances("https://pacs.example.com", [open("ct.dcm", "rb").read()])
DIMSE networking¶
import pydcm.dimse as pynetdicom
ae = pynetdicom.AE(ae_title="PYDCM")
assoc = ae.associate("pacs.local", 11112, ae_title="ANY-SCP")
assoc.send_c_echo()
assoc.send_c_store(pydcm.dcmread("ct.dcm")) # persistent: many ops, one association
assoc.release()
AE.start_server runs the SCP side with the same EVT_C_STORE / ECHO /
FIND / GET / MOVE event model.
Preprocessing¶
from pydcm import transforms as T
out = T.resample_cubic(vol, out_shape) # bit-exact B-spline order-3
seg = T.sliding_window_inference(vol.pixels, roi_size=(96, 96, 96), predictor=model)
Tier-1 ops are bit-exact for the classic B-spline convention; Tier-2 ops match the deep-learning (grid-sample) convention to ≤ 1 float32 ULP — same numbers in training and serving.
RT dosimetry¶
grid = pydcm.read_rtdose("rtdose.dcm")
dvh = pydcm.dvhcalc("rtstruct.dcm", "rtdose.dcm", roi_number) # ROI-for-ROI DVH
pydcm.write_rtdose(dose, affine=grid.affine, output="out.dcm") # conformance-clean
Whole-slide imaging¶
from pydcm import wsi
slide = wsi.open_slide("wsi_dir/")
region = slide.read_region((x, y), level=0, size=(512, 512)) # RGBA, level-0 coordinates
slide.associated_images["LABEL"]
For the complete capability map (radiomics, SEG / Parametric Map / TID 1500, waveforms, FHIR / HL7, MCP) see the home page and the API reference.