Skip to content

API reference

Generated from pydcm's docstrings. The top-level pydcm module re-exports the core read / write / decode API plus every specialist surface below.

Read / write / decode

The core read / write / decode API. See Behaviour notes for the short list of deliberate behaviours worth knowing.

pydcm

pydcm — decode DICOM pixels for NumPy / PyTorch.

A compiled native extension decodes EVERY transfer syntax (JPEG / JPEG-2000 / HTJ2K / JPEG-LS / JPEG-XL / RLE) to native integer pixels — or Hounsfield units — with no separate codec plugins to install, and assembles a directory of slices into a 3D volume.

import pydcm
arr = pydcm.decode("scan.dcm")             # ndarray [frames, rows, cols(, samples)]
hu  = pydcm.decode("ct.dcm", rescale=True)  # float32 Hounsfield units
vol = pydcm.load_series("ct_dir/")          # spatially-ordered 3D HU volume

from torch.utils.data import DataLoader
ds = pydcm.DICOMDataset("study_dir/", to_torch=True)
for batch in DataLoader(ds, batch_size=8, num_workers=4):
    ...

NOT a medical device — not for clinical or diagnostic use; research/engineering only.

decode

decode(
    path,
    frame: int = 0,
    *,
    rescale: bool = False,
    to_torch: bool = False,
    with_meta: bool = False
)

Decode a DICOM file's pixels to an array.

Parameters

path : str | os.PathLike A Part-10 DICOM file (any transfer syntax). frame : int 1-based frame to extract; 0 (default) returns all frames. rescale : bool True → real-world values (HU for CT) as float32 (per-frame rescale applied); False (default) → native stored integers (lossless). to_torch : bool Return a torch.Tensor instead of a NumPy array. with_meta : bool Also return the geometry sidecar (rescale_slope/intercept, pixel_spacing, image_position/orientation_patient, slice_thickness, window_center/width, modality, *_instance_uid, …).

Returns

ndarray (or Tensor), shape [frames, rows, cols(, samples)] — or (array, meta) when with_meta=True.

dcmread

dcmread(
    fp,
    defer_size=None,
    stop_before_pixels=False,
    force=False,
    specific_tags=None,
    *,
    charset_override: str = "",
    **kwargs
) -> Dataset

Read a DICOM Part-10 file into a :class:Dataset.

fp may be a path, an os.PathLike, raw bytes, or a readable binary file-like object (e.g. io.BytesIO). charset_override forces a SpecificCharacterSet when a file omits/misdeclares one. stop_before_pixels / defer_size / specific_tags are accepted for signature compatibility (pixels are always lazy here). A non-DICOM input raises :class:~pydcm.errors.InvalidDicomError unless force=True.

dcmwrite

dcmwrite(
    filename,
    dataset: Dataset,
    /,
    __write_like_original=None,
    *,
    implicit_vr=None,
    little_endian=None,
    enforce_file_format=False,
    force_encoding=False,
    overwrite=True,
    **kwargs,
) -> None

Write dataset to path as Part-10 (name).

pixel_array

pixel_array(src, *, index=None, **_kw)

Decode stored pixels from a Dataset / path / binary file-like.

PALETTE COLOR returns stored indices; use :func:apply_color_lut for RGB.

iter_pixels

iter_pixels(src, **_kw)

Yield each frame's pixel array lazily.

apply_modality_lut

apply_modality_lut(arr, ds)

Stored pixel values → MODALITY values (PS3.3 C.11.1).

Modality LUT Sequence when present, else Rescale Slope/Intercept, else the array unchanged. Mirrors pydicom.pixel_data_handlers.util.apply_modality_lut.

apply_voi_lut

apply_voi_lut(arr, ds, index: int = 0)

Modality values → PRESENTATION values (PS3.3 C.11.2).

A VOI LUT Sequence supersedes Window Center/Width when present (C.11.2), matching both the standard and pydicom.pixel_data_handlers.util.apply_voi_lut. The return is on the LUT's own output range (0 … 2**bits−1) for the LUT branch and on the window's [0,1] range scaled to the input's own span for the window branch — the same convention pydicom uses, so a caller that scales by 2**(16 - bits) (what dcmtk's dcmj2pnm --use-voi-lut writes) keeps working.

index picks the item of a multi-item sequence (GE mammography ships NORMAL/HARDER/SOFTER).

generate_uid

generate_uid(
    prefix: str | None = PYDCM_ROOT_UID, entropy_srcs=None
) -> UID

Return a unique :class:UID.

With entropy_srcs the result is deterministic for that input; otherwise it is random. Defaults to pydcm's registered root (PYDCM_ROOT_UID); pass an explicit prefix for a different root, or prefix=None for a 2.25. UUID-derived UID. Reuses the native canonical generator (_native.mint_uid).

Pixels

pydcm.pixels

Pixel helpers (pydcm.pixels). Decoding reuses the native engine; the LUT/windowing/colour helpers apply the standard PS3.3 formulas to an array.

unpack_bits

unpack_bits(ds)

Decode 1-bit PixelData to one uint8 value per sample via the native engine.

decode_uncompressed

decode_uncompressed(ds)

Compatibility helper; all pixel decoding is delegated to the native engine.

pack_bits

pack_bits(arr, pad: bool = True) -> bytes

Pack a binary {0,1} :class:numpy.ndarray into bytes for 1-bit Pixel Data (PS3.5 §8.1.1, little bit order — inverse of :func:unpack_bits).

apply_rescale

apply_rescale(arr, ds)

Apply the linear Modality LUT (arr * RescaleSlope + RescaleIntercept). Use :func:apply_modality_lut when a Modality LUT Sequence may be present; this is the rescale-only path.

pixel_array

pixel_array(src, *, index=None, **_kw)

Decode stored pixels from a Dataset / path / binary file-like.

PALETTE COLOR returns stored indices; use :func:apply_color_lut for RGB.

iter_pixels

iter_pixels(src, **_kw)

Yield each frame's pixel array lazily.

apply_modality_lut

apply_modality_lut(arr, ds)

Modality LUT (Rescale Slope/Intercept → e.g. Hounsfield units), PS3.3 C.11.1.

apply_voi_lut

apply_voi_lut(arr, ds, index=0)

VOI LUT / windowing (PS3.3 C.11.2).

Applies a VOI LUT Sequence if present, else Window Center/Width with the VOILUTFunction (LINEAR / LINEAR_EXACT / SIGMOID). The output is scaled to [0, 2**BitsStored - 1] (NOT [0,1]).

apply_voi

apply_voi(arr, ds, index=0)

Apply a VOI LUT Sequence (0028,3010) if present; else return arr unchanged.

apply_windowing

apply_windowing(arr, ds, index=0)

Linear/sigmoid Window Center/Width (PS3.3 C.11.2.1.2).

convert_color_space

convert_color_space(
    arr, current, desired, *, per_frame=False
)

Convert between RGB and YBR_FULL/YBR_FULL_422 (PS3.3 C.7.6.3.1.2).

apply_color_lut

apply_color_lut(arr, ds=None, palette=None)

Map PALETTE COLOR stored indices to RGB through the native engine.

apply_presentation_lut

apply_presentation_lut(arr, ds)

Apply a Presentation LUT (Sequence or INVERSE shape) to arr.

Returns P-values; if no Presentation LUT module is present, returns arr unchanged. Modality/VOI LUTs (if any) must be applied first.

as_pixel_options

as_pixel_options(ds, **kwargs)

Return the Image Pixel module element values from ds as a dict.

compress

compress(
    ds,
    transfer_syntax_uid,
    arr=None,
    *,
    encoding_plugin="",
    encapsulate_ext=False,
    generate_instance_uid=True,
    **kwargs
)

Compress ds in place to transfer_syntax_uid (delegates to the native engine).

decompress

decompress(
    ds,
    *,
    as_rgb=True,
    generate_instance_uid=True,
    decoding_plugin="",
    **kwargs
)

Decompress ds's Pixel Data in place to native encoding (delegates to native).

set_pixel_data

set_pixel_data(
    ds,
    arr,
    photometric_interpretation,
    bits_stored,
    *,
    generate_instance_uid=True
)

Set ds's Pixel Data + Image Pixel module elements from arr.

File-sets (DICOMDIR)

pydcm.fileset

DICOMDIR / File-set reading (pydcm.FileSet).

A File-set is a DICOMDIR plus the instance files it indexes. dcmread already parses the DICOMDIR's DirectoryRecordSequence (PATIENT→STUDY→SERIES→IMAGE), so this is a thin navigation layer over those records — it reuses dcmread for both the DICOMDIR and each referenced instance; no separate DICOM parsing.

fs = pydcm.FileSet("/media/DICOMDIR")
for inst in fs:                     # FileInstance per leaf record
    ds = inst.load()               # -> a Dataset (reuses dcmread)
for inst in fs.find(PatientID="1"):
    ...

FileSet

A DICOMDIR File-set — iterate :class:FileInstance leaves, find + load.

Construct from a DICOMDIR path or an already-read DICOMDIR :class:Dataset.

find

find(load: bool = False, **filters)

Return instances matching filters (record attributes; with load=True the referenced file is read and its values are matched too).

find_values

find_values(element, instances=None)

Distinct values of element across the (given or all) instances.

add

add(ds_or_path) -> None

Stage an instance (a :class:Dataset or a file path) for :meth:write.

write

write(path, file_set_id='PYDCM_FILESET') -> str

Write the staged instances + a conformant DICOMDIR under path; returns the DICOMDIR path. Instances staged from a file are copied byte-verbatim.

The DICOMDIR itself is built by the native engine: it reads each instance's key attributes, groups them PATIENT→STUDY→SERIES→leaf, and emits a conformant Explicit-VR-LE directory with correct inter-record byte offsets. This wrapper only does the filesystem side — assign each instance a media File ID, place the file, and write the returned DICOMDIR.

FileInstance

One referenced instance (a leaf DICOMDIR record).

Attribute access falls back through the record hierarchy: the instance/IMAGE record first, then its SERIES, STUDY and PATIENT records — so inst.PatientID, inst.StudyInstanceUID etc. work without loading the file.

load

load() -> Dataset

Read and return the referenced :class:Dataset (reuses dcmread).

Conformance validation

Validate a file against the DICOM standard. validate is the full report (element-level layers — VR, value multiplicity, enumerated values, value format, SpecificCharacterSet, pixel geometry, LUT — plus IOD / module / conditional and the SR content tree); iod_validate is the narrower IOD-only view. Both return a list of findings ([] = conformant).

pydcm

pydcm — decode DICOM pixels for NumPy / PyTorch.

A compiled native extension decodes EVERY transfer syntax (JPEG / JPEG-2000 / HTJ2K / JPEG-LS / JPEG-XL / RLE) to native integer pixels — or Hounsfield units — with no separate codec plugins to install, and assembles a directory of slices into a 3D volume.

import pydcm
arr = pydcm.decode("scan.dcm")             # ndarray [frames, rows, cols(, samples)]
hu  = pydcm.decode("ct.dcm", rescale=True)  # float32 Hounsfield units
vol = pydcm.load_series("ct_dir/")          # spatially-ordered 3D HU volume

from torch.utils.data import DataLoader
ds = pydcm.DICOMDataset("study_dir/", to_torch=True)
for batch in DataLoader(ds, batch_size=8, num_workers=4):
    ...

NOT a medical device — not for clinical or diagnostic use; research/engineering only.

Volumes, geometry & ML

pydcm.volume

Directory of slices → one spatially-ordered 3D volume.

Thin orchestration over the native volume engine: IOP clustering, IPP-projection Z-sort, and N-D dimension discovery all happen in the compiled _core extension. Nothing about the geometry is reimplemented here — Python only enumerates the files, hands them to the engine, and wraps the result as NumPy.

Volume dataclass

An assembled 3D volume. pixels is float32 Hounsfield/real-world values from :func:load_series; from :func:from_nifti it keeps the file's dtype (e.g. an integer label mask).

to_nifti

to_nifti(path) -> str

Write this volume to NIfTI-1. .nii.gz extension → gzip, else .nii.

Thin call into the native dcm_nifti engine — the voxel→world affine (LPS) is flipped to RAS inside the writer; nothing is recomputed here. The NIfTI datatype follows pixels' dtype (float32 / uint8 / int16 / uint16, so integer label masks stay integer); other dtypes are cast to float32.

to_nrrd

to_nrrd(path, *, gzip: bool = False) -> str

Write this volume to single-file NRRD (.nrrd) — 3D Slicer's native format. LPS-native, so the voxel→world affine maps straight to NRRD space directions/space origin (no RAS flip). gzip=Trueencoding: gzip. Datatype follows pixels (label masks stay integer).

to_metaimage

to_metaimage(path, *, compress: bool = False) -> str

Write this volume to single-file MetaImage (.mha) — the ITK / nnU-Net / MONAI interchange format. LPS-native (direction cosines → TransformMatrix, origin → Offset). compress=TrueCompressedData=True.

Axis dataclass

One non-spatial axis of a :class:Volume4D — what the 4th dimension means.

kind is the semantic label ("temporal", "bvalue", "direction", "echo", "cardiac", "stack", "frametype", "velocity", "energy"); values holds one sorted value per step (e.g. trigger times, b-values, echo times).

Volume4D dataclass

A 4-D stack: n_volumes co-registered 3-D volumes sharing one world grid.

The 4th dimension is the non-spatial axis (or axes) that varies across the series — time (cardiac cine, perfusion, fMRI), b-value/direction (DWI), echo time (multi-echo), cardiac phase, stack, spectral energy. :attr:dimensions labels it; when more than one axis varies the volume index decomposes row-major (slowest-first) across them — see :meth:coords.

coords

coords(i) -> dict

The non-spatial coordinate of volume i as {kind: value} (e.g. {"bvalue": 1000.0, "direction": 3.0} or {"temporal": 7.0}).

axis

axis(kind: str) -> Axis | None

The :class:Axis with the given kind ("temporal" …), or None.

to_nifti

to_nifti(path) -> str

Write the 4-D stack to NIfTI-1 (.nii.gz → gzip). Thin call into the native dcm_nifti N-D writer — the LPS affine is flipped to RAS inside; the 4th axis becomes NIfTI's time/volume dimension.

to_nrrd

to_nrrd(path, *, gzip: bool = False) -> str

Write the 4-D stack to single-file NRRD (.nrrd). The 4th axis becomes a non-spatial NRRD axis (kind: list, no space direction). gzip=Trueencoding: gzip.

to_metaimage

to_metaimage(path, *, compress: bool = False) -> str

Write the 4-D stack to single-file MetaImage (.mha); the 4th axis is the slowest MetaImage dimension. compress=TrueCompressedData=True.

load_series

load_series(
    path,
    *,
    recursive: bool = True,
    pattern: str | None = None
) -> Volume

Assemble the DICOM slices under path into one ordered 3D HU volume.

path is a directory (or list of files). Files are grouped/sorted by the native engine; the largest coherent volume is returned (so a stray localizer or mixed series does not corrupt the stack). For a plain CT/MR series directory that is simply the volume.

load_4d

load_4d(
    path,
    *,
    recursive: bool = True,
    pattern: str | None = None
) -> Volume4D

Assemble the DICOM slices under path into one 4-D stack [T, Z, Y, X].

The native engine clusters by orientation/grid, Z-sorts by IPP projection, and discovers the varying non-spatial axes (time / b-value / direction / echo / cardiac phase / stack / energy) — each becomes the 4th dimension and is labelled in :attr:Volume4D.dimensions. path may be a directory, a single enhanced multi-frame file, or a list of files. A plain 3-D series yields T == 1 with an empty dimensions list.

For a DWI series where you also need the FSL .bval/.bvec gradient table, use :func:pydcm.load_dwi; this returns the geometry + dimension semantics for any 4-D organisation, not just diffusion.

from_nifti

from_nifti(path) -> Volume

Read a NIfTI-1 file (.nii/.nii.gz) back into a :class:Volume.

The file's RAS sform is flipped to our LPS convention by the native reader, so the returned affine matches what :func:load_series produces. Voxels keep the file's dtype (e.g. float32 image, integer label mask).

bids_sidecar

bids_sidecar(path) -> dict

Extract a BIDS JSON sidecar (the standard BIDS metadata written next to a .nii) from one DICOM instance — timing (in seconds), sequence, and geometry fields.

Returns a dict of the present fields (e.g. RepetitionTime, EchoTime, FlipAngle, Manufacturer, ImageOrientationPatientDICOM).

PhaseEncodingDirection (BIDS i/i-/j/j-) is emitted when the vendor records the polarity (Siemens CSA / 0021,111C; GE 0018,9034; UIH 0065,1058); its sign follows this writer's no-row-flip storage (no row flip), so the i sign equals the reference while the j sign is the negation — verified on the dcm_qa suite, where our volume equals the reference with the rows flipped, so each sidecar correctly describes its own array. Unknown polarity gives only the unsigned PhaseEncodingAxis; a 3-D non-EPI scan emits neither.

SliceTiming (seconds) is emitted for a Siemens mosaic from the CSA MosaicRefAcqTimes (the whole schedule lives in one instance). EffectiveEchoSpacing/TotalReadoutTime (seconds) are emitted for Siemens EPI (1/(BW·N) and ES·(N−1), N=NumberOfPhaseEncodingSteps) for full-resolution and phase-oversampled EPI.

pydcm.diffusion

DWI gradient tables and volume loading.

The public table is produced by the same native series plan as :func:load_dwi: one entry per assembled 3-D acquisition volume, with the real IPP-derived slice axis. This matters for tilted stacks and also prevents a per-slice list from being mistaken for an FSL per-volume .bval / .bvec table.

diffusion_table

diffusion_table(files, *, output_prefix=None, rotate=True)

Collect DWI b-values + gradient directions into FSL .bval / .bvec.

a directory, a single file, or a list of instances. Entries are one

per assembled 3-D diffusion acquisition and are ordered by acquisition (earliest InstanceNumber, with a stable frame-order tiebreak).

rotate: take the gradient against the image axes — the native conversion, which projects patient-frame encodings, maps GE/Canon classic logical axes, and leaves UIH's image-frame private vector unprojected. The b0 zero vector stays zero. False returns the vector as the vendor stored it, for callers doing their own geometry. output_prefix: also write <prefix>.bval and <prefix>.bvec.

Returns (bvals[N], bvecs[3, N]).

The rotated bvecs are in the VOXEL convention: they pair with pixel data in DICOM row order, which is what :func:load_dwi returns and what to_nifti / :func:save_dwi write (our NIfTI writer copies rows verbatim and puts LPS→RAS in the affine alone). Pairing them instead with a NIfTI written rows-bottom-up mirrors every tensor about the row axis and changes no scalar map — use _core.read_diffusion's gradient_fsl for that case.

load_dwi

load_dwi(series, *, recursive=True, order='gradient')

Load a single-frame DWI series as a 4-D volume + gradient table.

Groups the slices by their diffusion (b-value + gradient — the standard top-level tags 0018,9087 / 0018,9089, falling back to the Siemens CSA header; non-DWI frames are skipped), assembles each direction's 3-D volume, and stacks them. Returns (data[V, Z, Y, X], bvals[V], bvecs[3, V], affine) — bvecs rotated into the image/voxel frame, ready for dti_*.

"gradient" (default) sorts volumes by gradient, b0 first — fully

deterministic; "acquisition" orders them by each direction's earliest InstanceNumber. The .bval/.bvec stay aligned either way.

Enhanced-MF DWI is split through its per-frame MR Diffusion functional groups by the same native series engine.

save_dwi

save_dwi(
    series,
    output_prefix,
    *,
    recursive=True,
    order="acquisition"
)

Convert a single-frame DWI series to NIfTI + FSL .bval/.bvec ( DWI deliverable). Writes <prefix>.nii.gz (4-D), <prefix>.bval and <prefix>.bvec; returns their paths.

order defaults to "acquisition" so the volume order is deterministic (pass "gradient" for deterministic b0-first ordering).

pydcm.transforms

Deterministic medical-image transforms — pydcm's "ITK".

Thin marshalling over the native transform engine (CPU C/C++). This is the same preprocessing whether you prepare training data here or deploy in the browser (where dcmmodel runs the identical ops as WGSL, CI-verified equal), so preprocessing cannot drift between train and serve.

Scope: the load-bearing deterministic ops — spatial (resample_to_spacing and resize with label-safe nearest; the exact index ops crop, pad, crop_foreground, flip), intensity (normalize_zscore, scale_intensity_range), post (argmax) — plus a minimal :class:Compose. Random augmentation is intentionally not here (use MONAI/torchio for training aug).

Spatial ops operate on a :class:~pydcm.volume.Volume (pixels[z,y,x] + LPS affine) and return a new Volume; intensity ops likewise. Geometry is never recomputed in Python — the native engine owns it.

Cross-framework conventions. Most ops are convention-free (identical across skimage/torch/ITK). Two things genuinely diverge between frameworks: the resampling interpolation (skimage.resize spline vs torch grid_sample vs SimpleITK) and the gaussian importance map. To get a self-consistent pipeline for one framework, import a preset instead of mixing primitives::

from pydcm.transforms import nnunet as T   # skimage.resize spline + nnU-Net gaussian
from pydcm.transforms import monai as T   # torch grid_sample + MONAI gaussian

The preset binds the divergent ops to that framework and passes the rest through. See docs/transforms_references.md for the per-op authoritative reference + precision.

Compose

Apply a sequence of transforms left-to-right (MONAI Compose).

Each entry is a callable Volume -> Volume; use functools.partial or a lambda to bind parameters, e.g. Compose([lambda v: resample_to_spacing(v, 1.0), normalize_zscore]).

resample_to_spacing

resample_to_spacing(
    vol: Volume,
    spacing,
    *,
    is_label: bool | None = None,
    interp: str = "linear"
) -> Volume

Resample vol to an axis-aligned LPS grid at spacing mm.

spacing is a scalar (isotropic) or (x, y, z). is_label defaults from dtype (integer → label → nearest); pass it to override. interp is "linear" / "cubic" / "nearest" (ignored for labels — always nearest).

resample_to_reference

resample_to_reference(
    moving: Volume,
    reference: Volume,
    *,
    is_label: bool | None = None,
    interp: str = "linear",
    fill: float = 0.0
) -> Volume

Resample moving onto reference's grid (its shape + affine).

This is how you invert a preprocessing chain: a prediction computed in some processed space (resampled / reoriented / cropped) is mapped back onto the original :class:~pydcm.volume.Volume's grid by passing that original as reference. It works through any sequence of geometric transforms (it uses the affines, not a recorded op-stack) and handles an oblique reference grid. is_label defaults from dtype (integer → nearest).

Bit-exact with SimpleITK's sitk.Resample(moving, reference, sitkLinear) / sitkNearestNeighbor — the resample-to-reference pattern medical models use — both INSIDE and OUTSIDE the moving extent: reference voxels whose moving index leaves [-0.5, dim-0.5) on any axis are set to fill (SimpleITK's defaultPixelValue), not edge-extrapolated. fill=0 is background — correct for a label map or a prediction mapped onto a larger grid; pass e.g. -1024 for CT air.

affine

affine(
    vol: Volume,
    matrix,
    *,
    is_label: bool | None = None,
    interp: str = "linear"
) -> Volume

Apply a voxel-space affine to the image (MONAI Affine). matrix is a 4×4 array — the forward source-voxel → output-voxel transform (rotate/scale/shear/translate), in engine voxel order (x, y, z) i.e. (W, H, D). The output keeps the source's grid, so content moved outside the field of view is clipped. is_label defaults from dtype (integer → nearest).

resample_separate_z

resample_separate_z(vol: Volume, out_shape) -> Volume

nnU-Net anisotropic separate-z resample to out_shape (D, H, W): per-slice in-plane cubic B-spline + nearest through-plane (the low-res Z axis), with an fp64 prefilter — the precision-faithful path for thick-slice MR/CBCT (matches nnU-Net / scipy map_coordinates). For images (the in-plane spline blends labels).

resample_cubic

resample_cubic(vol: Volume, out_shape) -> Volume

Isotropic cubic B-spline resample to out_shape (D, H, W) — order=3 B-spline, fp64, bit-exact with skimage.resize(order=3, mode='edge', clip=True) (the nnU-Net image reference — note this is skimage.resize, NOT scipy.ndimage.zoom). For images.

resample_nearest

resample_nearest(vol: Volume, out_shape) -> Volume

Nearest-neighbour resample to out_shape (D, H, W) (order=0, half-pixel — matches skimage.resize(order=0)) — the nnU-Net label path (no class blending).

resample_grid_sample

resample_grid_sample(vol: Volume, out_shape) -> Volume

Trilinear resample to out_shape (D, H, W) in torch grid_sample(align_corners=False, bilinear, padding_mode='border') convention — half-pixel (j+0.5)*src/dst-0.5. This is MONAI Resize's backend (F.interpolate): verified index-for-index vs monai.transforms.Resize (9→6 → 0.25,1.75,3.25,…). It is NOT MONAI Spacing — a spacing change in Spacing samples at scale*j (affine/world convention, 9@1.0→1.5 → 0,1.5,3,4.5,6,7.5); for that use :func:resample_to_spacing. fp64 internally; agrees with torch to ≤1 fp32 ULP (torch's affine_grid uses a non-reproducible SIMD linspace). For images.

resize_2d

resize_2d(
    vol: Volume, out_hw, *, filt: str = "bilinear"
) -> Volume

2-D resize, applied per slice in-plane to out_hw (out_H, out_W) (each (H, W) slice → (out_H, out_W); depth/slices preserved). All paths are deterministic — the CPU result and a GPU/WGSL port agree bit-for-bit (train/serve parity). filt:

  • "bilinear" (default): plain bilinear, no anti-aliasing, half-pixel ((o+0.5)*scale-0.5), clamp, double. This is the convention the deployment actually uses (ai_segmeation's resizeBilinearU8 / zoom2d.wgsl) and the same kernel as cv2.INTER_LINEAR / F.interpolate(bilinear, align_corners=False). Bit-exact with resizeBilinearU8; ≤1 fp32 ULP vs cv2/torch (those use SIMD/FMA and disagree ≤1 ULP with each other — no single bit-exact target exists, so this clean form is the reproducible spec).
  • "bicubic": PIL.Image.resize bicubic (anti-aliased on downscale), bit-exact with PIL — MedSAM2's Python preprocessing (each windowed CT slice → 512²). uint8 → PIL fixed-point, float → PIL float convolution.
  • "pil-bilinear": PIL bilinear (anti-aliased), bit-exact with PIL.

A uint8 Volume gives integer output (resizeBilinearU8 truncates; PIL rounds); window/cast beforehand as the model does.

resample

resample(
    vol: Volume,
    out_shape,
    *,
    backend: str = "skimage",
    is_label: bool | None = None
) -> Volume

Resample to out_shape (D, H, W) under a framework's interpolation convention: backend="skimage" (cubic B-spline, Tier-1 bit-exact — bit-identical to skimage.transform.resize(order=3, mode='edge', anti_aliasing=False), which is what nnU-Net's default_resampling uses; NOT scipy.ndimage.zoom, whose half-pixel coordinate convention differs — hence the name is skimage, not scipy) / "torch" (grid_sample, Tier-2 ≤1 fp32 ULP — also matches F.interpolate(mode='trilinear', align_corners=False) to ≤1 ULP, nnU-Net's opt-in torch resampling backend) / "itk" (SimpleITK linear/nearest in double — bit-exact vs SimpleITK on realistic grids; ITK B-spline is deferred). Labels — or is_label=True — force nearest, which is convention-free. The single entry for choosing the lineage; :func:resample_cubic / :func:resample_grid_sample are its skimage / torch backends.

resize

resize(
    vol: Volume,
    out_shape,
    *,
    is_label: bool | None = None,
    interp: str = "linear"
) -> Volume

Resample vol to exactly out_shape (D, H, W) voxels over the same field of view (MONAI Resize). is_label defaults from dtype.

resize_with_pad_or_crop

resize_with_pad_or_crop(
    vol: Volume,
    size,
    *,
    mode: str = "constant",
    value: float = 0.0
) -> Volume

Force the volume to exactly size (z, y, x) by centre-cropping axes that are too large and centre-padding axes that are too small (MONAI ResizeWithPadOrCrop).

reorient

reorient(vol: Volume, axcodes: str = 'LPS') -> Volume

Reorient so increasing voxel index runs toward the target world directions (MONAI Orientation). axcodes is 3 letters from L/R, P/A, S/I — the world is LPS, so "LPS" is the engine-canonical orientation. Exact axis permutation + flips; world coordinates are unchanged. Raises on an oblique affine.

crop

crop(vol: Volume, start, size) -> Volume

Crop the [start, start+size) box; start/size are (z, y, x) (numpy axis order). Exact — no interpolation. The affine origin shifts to keep world coordinates.

pad

pad(
    vol: Volume,
    lo,
    hi,
    *,
    mode: str = "constant",
    value: float = 0.0
) -> Volume

Pad lo/hi voxels before/after each axis ((z, y, x)). mode is "constant" / "edge" / "reflect" (MONAI SpatialPad/BorderPad).

crop_foreground

crop_foreground(vol: Volume, *, margin: int = 0) -> Volume

Crop to the bounding box of non-zero voxels, expanded by margin (MONAI CropForeground). Returns vol unchanged if every voxel is zero.

flip

flip(vol: Volume, axis) -> Volume

Reverse voxel order along the flagged axes. axis is a (z, y, x) triple of bools (MONAI Flip). The affine updates so world coordinates are unchanged.

transpose

transpose(vol: Volume, axes) -> Volume

Permute the voxel axes (numpy/MONAI transpose); axes is a permutation of (0, 1, 2) in (z, y, x) order — output axis i is input axis axes[i]. Exact (no interpolation); the affine's axis columns permute so world coordinates are unchanged. This is nnU-Net's transpose_forward; the inverse (transpose_backward) is transpose(v, np.argsort(axes)).

center_crop

center_crop(
    vol: Volume, size, *, is_label: bool | None = None
) -> Volume

Center-crop to size (z, y, x) (MONAI CenterSpatialCrop; start = dim//2 - size//2). A size larger than the source is clamped to the source (crop only, no pad). Exact; the affine origin shifts to keep world coords.

spatial_pad

spatial_pad(
    vol: Volume,
    size,
    *,
    mode: str = "constant",
    value: float = 0.0,
    is_label: bool | None = None
) -> Volume

Centered pad to at least size (z, y, x) (MONAI SpatialPad symmetric). Axes already ≥ size are untouched. modeconstant/edge/reflect.

divisible_pad

divisible_pad(
    vol: Volume,
    k,
    *,
    mode: str = "constant",
    value: float = 0.0,
    is_label: bool | None = None
) -> Volume

Centered pad so each axis becomes a multiple of k (MONAI DivisiblePad — e.g. make dims divisible by 2^depth for a U-Net). k is a scalar or (z, y, x).

rotate90

rotate90(
    vol: Volume,
    k: int = 1,
    axes=(1, 2),
    *,
    is_label: bool | None = None
) -> Volume

Rotate k*90° in the plane of axes (numpy (D, H, W) axis indices; default (1, 2) = the in-plane H–W axes) — MONAI Rotate90 / np.rot90. Exact (no interpolation); the affine updates so world coordinates are kept.

normalize_zscore

normalize_zscore(
    vol: Volume, *, nonzero: bool = False
) -> Volume

z-score normalize voxels (→ float32). nonzero=True ignores zero voxels (MONAI NormalizeIntensity(nonzero=True)).

scale_intensity_range

scale_intensity_range(
    vol: Volume,
    a_min: float,
    a_max: float,
    b_min: float,
    b_max: float,
    *,
    clip: bool = True
) -> Volume

Linearly remap [a_min, a_max] → [b_min, b_max] (→ float32). CT windowing (MONAI ScaleIntensityRange). clip bounds the output to [b_min, b_max].

normalize_ct

normalize_ct(
    vol: Volume,
    clip_lo: float,
    clip_hi: float,
    mean: float,
    std: float,
) -> Volume

Clip to [clip_lo, clip_hi] then z-score with fixed mean/std (→ float32). This is nnU-Net CTNormalization (clip to the dataset's [0.5, 99.5] percentiles, normalize by the dataset mean/std) and equivalently MONAI NormalizeIntensity(subtrahend=mean, divisor=std) preceded by a clip.

rescale_robust

rescale_robust(
    vol: Volume,
    *,
    dst_min: float = 0.0,
    dst_max: float = 255.0,
    f_low: float = 0.0,
    f_high: float = 0.999
) -> Volume

FreeSurfer/FastSurfer "conform" ROBUST intensity rescale to [dst_min, dst_max] (→ float32). A 1000-bin histogram picks a robust source range, ignoring the f_low fraction of all voxels at the bottom and (1 - f_high) of the non-zero voxels at the top (mri_convert defaults f_low=0, f_high=0.999), then x → clip(dst_min + scale*(x - src_min)). This is the intensity step of the FastSurfer / SynthSeg / DL-DiReCT brain "conform" pipeline — the orientation + 1 mm resample steps are :func:reorient + :func:resample_to_spacing; cast the result to uint8 for those models. Bit-faithful to conform.py getscale()+scalecrop().

scale_intensity_range_percentiles

scale_intensity_range_percentiles(
    vol: Volume,
    lower: float,
    upper: float,
    b_min: float,
    b_max: float,
    *,
    clip: bool = True
) -> Volume

Like :func:scale_intensity_range, but a_min/a_max are the per-image lower/upper percentiles (0..100, np.percentile linear) — MONAI ScaleIntensityRangePercentiles. → float32.

adjust_contrast

adjust_contrast(vol: Volume, gamma: float) -> Volume

Gamma contrast ((x-min)/(range+1e-7))**gamma * range + min (MONAI AdjustContrast). → float32.

gaussian_smooth

gaussian_smooth(vol: Volume, sigma) -> Volume

Separable Gaussian smoothing (MONAI GaussianSmooth). sigma is a scalar (isotropic) or (z, y, x) in voxels; sigma<=0 on an axis skips it. → float32.

argmax

argmax(
    probs: ndarray, affine: ndarray, *, channel_dim: int = 0
) -> Volume

Channel-wise argmax of a probability/logit array → a label :class:Volume.

probs is channel-first [C, z, y, x] by default (torch/MONAI convention); set channel_dim for another layout. Output is uint8 (C ≤ 256) else uint16.

connected_components

connected_components(
    vol: Volume, *, connectivity: int = 6
) -> Volume

Label each connected component of the non-zero foreground with a distinct id (1..N) → uint16 (≈ scipy.ndimage.label). connectivity is 6/18/26.

keep_largest_connected_component

keep_largest_connected_component(
    vol: Volume,
    *,
    connectivity: int = 6,
    per_class: bool = True
) -> Volume

Keep only the largest connected component, zeroing the rest (MONAI KeepLargestConnectedComponent). per_class=True keeps each non-zero class's own largest CC; False treats all non-zero as one foreground.

fill_holes

fill_holes(vol: Volume, *, connectivity: int = 6) -> Volume

Fill holes — background regions fully enclosed by a label — by setting them to that label (MONAI FillHoles). Each non-zero class is filled independently. connectivity (6/18/26) is that of the background.

as_discrete

as_discrete(vol: Volume, *, threshold: float) -> Volume

Binarize at threshold (value > threshold → 1) → uint8 label (MONAI AsDiscrete(threshold=...); the sigmoid-output counterpart of :func:argmax).

remove_small_objects

remove_small_objects(
    vol: Volume,
    *,
    min_size: int,
    connectivity: int = 6,
    per_class: bool = True
) -> Volume

Zero connected components smaller than min_size voxels (MONAI RemoveSmallObjects). per_class=True prunes each non-zero class independently.

one_hot

one_hot(
    vol: Volume,
    num_classes: int,
    *,
    channel_first: bool = True
) -> np.ndarray

Integer-label Volume → one-hot float32 array (MONAI AsDiscrete(to_onehot=...)). channel_first=True[C, z, y, x] (torch); else [z, y, x, C]. Pure NumPy — a training-side helper, not a browser-inference op.

sliding_window_positions

sliding_window_positions(
    spatial_shape, roi_size, *, overlap: float = 0.25
) -> np.ndarray

Patch origins for sliding-window inference over a (D, H, W) volume with window roi_size and fractional overlap ∈ [0, 1) — MONAI dense_patch_slices / _get_scan_interval (fixed scan interval, last patch shifted inward). Returns an (n, 3) int array of (z, y, x) origins. Raises if roi exceeds spatial.

gaussian_importance_map

gaussian_importance_map(
    roi_size,
    *,
    sigma_scale: float = 0.125,
    convention: str = "nnunet"
) -> np.ndarray

Gaussian blend-weight window of shape roi_size (D, H, W) — separable product of 1D sampled gaussians, sigma = roi*sigma_scale. → float32.

convention picks the framework's map (the two are not interchangeable):

  • "nnunet" (default) — center roi//2, peak 1, min = natural corner. Bit-exact with nnU-Net V2 get_gaussian / the deployed ai_segmeation maps.
  • "monai" — center (roi-1)/2, unnormalized (peak ≈0.91), min clamped to max(min, 1e-3). Matches MONAI compute_importance_map(mode='gaussian').

sliding_window_inference

sliding_window_inference(
    image,
    roi_size,
    predictor,
    *,
    overlap: float = 0.25,
    mode: str = "gaussian",
    sigma_scale: float = 0.125,
    convention: str = "nnunet",
    padding_mode: str = "constant",
    cval: float = 0.0
) -> np.ndarray

Run predictor over sliding windows and blend the patch outputs — the deterministic reproduction of MONAI sliding_window_inference.

NOT for production serving — this is a pure-NumPy CPU reference (prototype / parity oracle). The serving path is :class:pydcm.infer.Service (native dcminfer GPU pipeline); register a model there and use segment(volume=/image=) instead.

image is (D, H, W) or (C_in, D, H, W). predictor maps one patch (same leading shape as image, spatial roi_size) to logits/probs (C_out, *roi_size). Windows use :func:sliding_window_positions; overlaps are combined with a mode='gaussian' importance map (sigma_scale + convention, 'nnunet'/'monai' — see :func:gaussian_importance_map) or mode='constant' (uniform) weight, accumulated and normalized by the summed weight. If the volume is smaller than roi_size it is padded (padding_mode/cval) and the result is cropped back. Returns (C_out, D, H, W).

pydcm.torchdata

Directory → samples. A directory of DICOM files is, for PyTorch, just a list of instances; DICOMDataset walks it and decodes one image per __getitem__.

One sample = one file. Single-frame files yield [rows, cols(, samples)]; multi-frame files yield [frames, rows, cols(, samples)]. To instead collapse a directory into one spatially-ordered 3D volume, use :func:pydcm.load_series.

DICOMDataset

Map-style dataset over the DICOM files under root.

DataLoader-compatible via __len__ / __getitem__ WITHOUT importing torch, so torch stays optional. __getitem__ returns a NumPy array (or, with to_torch=True, a torch.Tensor); pass a transform to override that and shape each sample however your model wants. rescale=True yields HU.

scan

scan(
    root,
    *,
    recursive: bool = True,
    pattern: str | None = None
) -> list[Path]

Discover DICOM instance files under root (a directory or a single file).

pattern — a glob (e.g. "*.dcm") selects by name only. When None, files are detected by extension OR the DICM preamble (also catching the extension-less files clinical exports often produce). Returns a sorted list.

pydcm.radiomics

pydcm radiomics — IBSI features over an ROI, plus a feature extractor.

Three surfaces over the one native radiomics engine:

  • pydcm.radiomics(image, mask=..., roi=...) — pydcm's own one-call API.
  • from pydcm.radiomics import featureextractor — the conventional RadiomicsFeatureExtractor(...).execute(img, mask) extractor API, so a pipeline written against that interface needs only change its import path; returns an OrderedDict.
  • @pydcm.radiomics.feature(...) — register a custom feature in Python; it runs over the SAME preprocessed + discretised grid the native engine used, so a researcher can add a feature or override a formula without recompiling. Applies to array inputs and to RadiomicsFeatureExtractor.execute.

ROI

The preprocessed + discretised ROI handed to a custom feature: the exact grid the native IBSI extractor ran over (same resample / normalise / resegmentation and the same gray-level discretisation). Arrays are (nz, h, w); ROI voxels have mask 1 and level [0, nb), the rest mask 0 and level -1.

intensities property

intensities

1-D preprocessed intensities over the ROI voxels.

levels property

levels

1-D gray levels [0, nb) over the ROI voxels (non-negative, bincount-ready).

n property

n

ROI voxel count.

RadiomicsFeatureExtractor

extractor over pydcm's native IBSI engine.

execute

execute(image, mask, label=None, voxelBased=False)

Return an OrderedDict of features over the ROI.

feature

feature(class_name='firstorder', name=None)

Register a custom radiomic feature (decorator).

The decorated function receives an :class:ROI and returns a scalar; the value joins every result dict as "<class_name>_<name>" (name defaults to the function's name). Use as @feature, @feature("glcm") or @feature("firstorder", name="my_stat"). Applies to array inputs and to RadiomicsFeatureExtractor.execute (the compatibility extractor).

Naming a custom feature after a standard one — @feature("firstorder", name="Mean") — OVERRIDES that feature's value in the result, which is how you change a built-in formula without recompiling.

clear_features

clear_features()

Drop all registered custom features.

registered_features

registered_features()

The keys ("<class>_<name>") of the currently registered custom features.

radiomics

radiomics(
    image,
    mask=None,
    *,
    roi=None,
    spacing=None,
    bins=32,
    value_range=(-1024.0, 3071.0),
    bin_width=0.0,
    resample=0.0,
    normalize=False,
    normalize_scale=1.0,
    log_sigma=None,
    wavelet=False,
    averaged=True,
    resegment=None,
    resegment_sigma=False,
    resample_bspline=False,
    voxel_array_shift=0.0,
    filters=None,
    distances=None
)

The IBSI radiomic feature set over an ROI — 10 classes (firstorder / glcm / glrlm / glszm / gldm / gldzm / ngtdm / shape / ivh [intensity-volume histogram] / local intensity), with the standard radiomics feature names.

Two call styles:

  • From files (the convenient path) — image is a DICOM path: pixels are decoded to real-world values (HU) and spacing is read from the image geometry (PixelSpacing / SliceThickness). Give the ROI as either mask = a co-framed mask DICOM path (non-zero = inside) or roi = (min, max) real-world-value thresholds.
  • From arrays (the low-level primitive) — image is a real-world-valued array (e.g. decode(..., rescale=True)) and mask a non-zero-inside array of the same shape; pass spacing = (x, y, z) mm yourself.

Preprocessing (IBSI, off by default): resample > 0 resamples the ROI to isotropic voxels of that size in mm (trilinear image / nearest mask); bin_width > 0 uses fixed-bin-width discretisation (vs the fixed bins count); normalize z-score-normalises intensities (× normalize_scale). Filters multiply the feature set under standard image-type prefixes: log_sigma = LoG sigma(s) in mm (log-sigma-<s>-mm-3D_…); wavelet=True = the coif1 SWT 8 sub-bands (wavelet-LLH_…). With any filter, every key (incl. the original) is prefixed.

Both 2D (H, W) or 3D (slices, H, W). Returns {feature_name: value}.

pydcm.dce

pydcm DCE-MRI — dynamic contrast-enhanced pharmacokinetic modelling.

A thin NumPy surface over the native DCE engine. Scope is the validated slice: the Parker population AIF, spoiled-GRE signal→concentration conversion, and the Tofts / Extended-Tofts / Patlak tissue models, fitted per voxel.

Typical use (concentration already computed)::

import numpy as np, pydcm.dce as dce
t = np.arange(0, 6, 0.025)              # minutes, injection at t=0
cp = dce.parker_aif(t)                  # population plasma AIF (mM)
maps = dce.fit(conc_4d, t, model="ext_tofts")   # conc_4d: (T, H, W)
ktrans = maps["ktrans"]                 # (H, W) float32, 1/min

From spoiled-GRE signal instead of concentration::

maps = dce.fit(signal_4d, t, input="spgr",
               t1_0_s=1.4, tr_s=0.005, fa_deg=25.0, r1=4.5)

Units: time in minutes (t=0 = injection; negative times = pre-contrast), Ktrans in 1/min, ve/vp dimensionless fractions, concentration in mM.

population_aif

population_aif(times_min, model='parker', hct=0.0)

Population arterial input function (plasma, mM) by name.

model is one of :data:AIFSparker (2006), georgiou (2019), fritz_hansen (1996), weinmann (dose-scaled bi-exp), mcgrath (2009, preclinical). times_min in minutes (t=0 = injection). The published forms are already plasma, so hct defaults to 0 (verbatim); set it only to convert a measured whole-blood curve. Returns a 1-D float64 array, length of times.

parker_aif

parker_aif(times_min, hct=0.0)

Parker (2006) population AIF (plasma, mM) — population_aif(..., 'parker').

hct defaults to 0 (the Parker curve is already plasma); set it only to convert a measured whole-blood curve. Returns a 1-D float64 array.

forward

forward(
    times_min,
    cp,
    model="ext_tofts",
    *,
    ktrans=0.1,
    ve=0.3,
    vp=0.02
)

Synthesise a tissue concentration curve Ct(t) from known PK parameters.

Uses the exact piecewise-linear-AIF convolution. cp is the arterial plasma curve at times_min. Returns Ct (mM), same length as times_min.

measure_aif

measure_aif(
    series,
    mask,
    tr_s,
    fa_deg,
    *,
    t1_blood_s=1.44,
    r1=4.5,
    hct=0.45,
    n_baseline=None
)

Extract a measured plasma AIF from an arterial ROI in a 4-D signal series.

The clinical alternative to a population (Parker) AIF: average the ROI's spoiled-GRE signal per time frame, invert to blood concentration with the blood baseline T1, and convert whole-blood → plasma via /(1 - hct).

series is (T, H, W) or (T, Z, Y, X) signal; mask is a non-zero ROI over the spatial dims (broadcast to the series' spatial shape). Returns the plasma AIF Cp(t) (mM, length T) — pass it to :func:fit / :func:fit_series as aif=. n_baseline (pre-contrast frames for S0) is auto-detected from the bolus rise when None.

t1_map_vfa

t1_map_vfa(volumes, flip_angles_deg, tr_s, *, mask=None)

VFA / DESPOT1 baseline-T1 map from a multi-flip-angle SPGR acquisition.

volumes is (F, H, W) — the spoiled-GRE signal at each of the F flip angles (same TR); flip_angles_deg is (F,). Returns {"t1", "m0", "fitted"} with t1 an (H, W) map in seconds — feed it to :func:fit / :func:fit_series as t1_map= for the SPGR path, instead of assuming a single baseline T1.

signal_to_conc

signal_to_conc(
    signal, n_baseline, t1_0_s, tr_s, fa_deg, r1
)

Invert the spoiled-GRE steady-state signal → tracer concentration (mM).

signal is a 1-D series; the first n_baseline samples form the pre-contrast S0. t1_0_s/tr_s in seconds, fa_deg in degrees, r1 the relaxivity (L·mmol⁻¹·s⁻¹).

fit_curve

fit_curve(
    times_min,
    ct,
    cp,
    model="ext_tofts",
    *,
    fit_delay=False,
    delay_bounds=(0.0, 0.5)
)

Fit a single tissue curve. Returns {ktrans, ve, vp, rmse, iters, ok, delay}.

ct measured tissue concentration, cp arterial plasma — both at times_min (mM). Patlak is solved in closed form; Tofts / Extended-Tofts use Levenberg–Marquardt. With fit_delay=True, a bolus-arrival delay (min) is jointly estimated over delay_bounds (the AIF is time-shifted), which removes the need for t=0 to be the exact injection instant.

fit

fit(
    series,
    times_min,
    model="ext_tofts",
    *,
    input="concentration",
    aif=None,
    hct=0.0,
    mask=None,
    enhance_thresh=0.0,
    t1_0_s=1.4,
    tr_s=0.005,
    fa_deg=25.0,
    r1=4.5,
    n_baseline=0,
    t1_map=None,
    fit_delay=False,
    delay_bounds=(0.0, 0.5)
)

Voxel-wise PK fit over a 4-D (T, H, W) series → parameter maps.

Returns {"ktrans", "ve", "vp", "rmse"} — each an (H, W) float32 map — plus "fitted" (the count of voxels actually fitted), and "delay" (min) when fit_delay=True. Maps a model does not estimate (ve for Patlak, vp for Tofts) are zero.

Parameters

series : (T, H, W) array — concentration, or raw signal when input='spgr'. times_min : (T,) acquisition times in minutes (t=0 = injection); frames must be time-ordered with the pre-contrast baseline first. model : 'tofts' | 'ext_tofts' | 'patlak'. input : 'concentration' (default) or 'spgr' (convert signal→conc per voxel). aif : optional measured plasma AIF (T,) in mM; default uses the Parker curve. hct : haematocrit for the Parker→plasma path (default 0 = use Parker verbatim). mask : optional (H, W) array; non-zero voxels are fitted. enhance_thresh : skip voxels whose peak enhancement is below this (noise gate). t1_0_s, tr_s, fa_deg, r1, n_baseline : SPGR conversion params (input='spgr'). t1_map : optional (H, W) per-voxel baseline T1 (s) overriding t1_0_s.

fit_series

fit_series(
    source,
    times_min=None,
    model="ext_tofts",
    *,
    input="concentration",
    aif=None,
    hct=0.0,
    mask=None,
    enhance_thresh=0.0,
    t1_0_s=1.4,
    tr_s=None,
    fa_deg=None,
    r1=4.5,
    n_baseline=0,
    t1_map=None,
    fit_delay=False,
    delay_bounds=(0.0, 0.5)
)

Fit a whole DCE series → parameter-map volumes.

source may be a DICOM directory / file-list / single enhanced-multiframe file (assembled via :func:pydcm.load_4d), a loaded :class:pydcm.Volume4D, or a raw array shaped (T, Z, Y, X) or (T, H, W). The whole volume is fitted in one native parallel call (Z slices fanned across cores in C++).

Returns {"ktrans", "ve", "vp", "rmse"} — each a (Z, H, W) float32 volume — plus "fitted" (total voxels fitted), "times_min" (the time grid used), and "delay" (min) when fit_delay=True.

Timing & sequence params: pass times_min explicitly (minutes; t=0 = injection), else they are read from the DICOM tags — the dynamic-time grid from AcquisitionDateTime → AcquisitionTime/ContentTime → TriggerTime (whichever varies), or the per-frame FrameAcquisitionDateTime for an enhanced-multiframe file; RepetitionTime / FlipAngle (via :func:pydcm.bids_sidecar) for the SPGR conversion. input='spgr' still needs a baseline t1_0_s / t1_map and r1 (T1 is not a stored tag). mask and t1_map may be (H, W) (same for every slice) or (Z, H, W).

IMPORTANT — injection alignment: auto-derived times are relative to the FIRST frame (t[0]=0); the Parker AIF assumes t=0 at injection. With pre-contrast baseline frames, pass times_min with baseline at negative t, supply a measured aif, or use fit_delay=True to absorb the offset.

write_param_maps

write_param_maps(
    reference,
    result,
    params=("ktrans", "ve", "vp"),
    *,
    dtype=None,
    output_dir=None
)

Emit DCE parameter VOLUMES as DICOM Parametric Maps (multi-frame when 3-D).

Thin convenience over :func:pydcm.write_paramap that supplies the correct per-parameter units (Ktrans 1/min, ve/vp dimensionless, delay min): each requested map in result (the dict from :func:fit / :func:fit_series) is written against reference — the source DCE slices, one per Z plane — so a (Z, H, W) volume becomes one multi-frame Parametric Map.

Returns {param: Part-10 bytes}, or {param: written path} when output_dir is given. dtype (e.g. "uint16") is forwarded to write_paramap for integer-quantised storage.

pydcm.dsc

pydcm DSC-MRI — dynamic susceptibility contrast perfusion.

A thin NumPy surface over the native DSC engine. A bolus of contrast transiently drops the T2*-weighted signal; the tissue concentration relates to the arterial input by C(t) = CBF·(C_a ⊗ R)(t), so recovering CBF / MTT / Tmax needs DECONVOLUTION — done by truncated SVD of the AIF convolution matrix:

* ``ssvd`` — standard truncated SVD, causal Toeplitz (Østergaard 1996).
* ``csvd`` — block-circulant SVD, delay-insensitive (Wu 2003).
* ``osvd`` — oscillation-index SVD, per-voxel adaptive threshold (Wu 2003).

CBV is the area ratio ∫C / ∫C_a (no deconvolution).

Typical use (concentration already computed)::

import numpy as np, pydcm.dsc as dsc
t = np.arange(0, 60, 1.0)               # seconds
maps = dsc.fit(conc_4d, t, aif, method="osvd")  # conc_4d: (T, H, W)
cbf = maps["cbf"]                       # (H, W) float32

From raw T2*-weighted signal instead of concentration::

maps = dsc.fit(signal_4d, t, aif, input="raw", te_s=0.030, n_baseline=10)

Units: time in SECONDS; MTT/Tmax seconds; CBF/CBV are relative (calibration-free — absolute mL/100g/min needs the caller's k·ρ·(1−Hct) scaling).

signal_to_conc

signal_to_conc(signal, n_baseline, te_s)

DSC signal → ΔR2* concentration: c(t) = −ln(S(t)/S0)/TE.

S0 is the mean of the first n_baseline (pre-bolus) frames. Returns the ΔR2* curve (signed; baseline ≈ 0, bolus > 0), same length as signal.

cbv

cbv(ct, ref, dt_s)

Blood volume as the trapezoidal area ratio ∫ct / ∫ref.

No deconvolution, and dt cancels — it is taken only so the call site states its units. What ref is decides what the number MEANS, and the two conventions are not interchangeable even though the arithmetic is one ratio:

  • the AIF gives CBV in the Østergaard/stroke sense, equal to CBF·MTT under the central-volume theorem;
  • a normal-appearing white matter ROI mean gives rCBV in the brain-tumour sense — Boxerman et al. (Neuro-Oncology 2020) and the ASFNR recommendation, which need no AIF at all.

Integrate over the window you mean: pass a slice of the curve to stop after the first pass, which is where the endpoint half-step actually matters.

cumtrapz

cumtrapz(y, dt_s)

Running trapezoidal integral of y, starting at 0.

first_pass_end

first_pass_end(conc, n_baseline, k_sigma=0.0)

How many leading samples make up the bolus's FIRST PASS.

Recirculation — the second, lower bump as the bolus comes round again — is not part of the first transit and inflates every area that includes it. ASFNR/Welker 2015 names both conventions (integrate every acquired point, or stop after the first pass) and prescribes neither, so this is EVIDENCE for choosing a window, not the window.

Takes the ΔR2* concentration curve, not raw signal. The yardstick is the curve's own pre-contrast scatter, so n_baseline must cover pre-contrast frames and be at least 2; k_sigma 0 selects the library default of 4.

Returns the count — usable directly as the length to pass to :func:leakage_correct and :func:cbv, and it must be the same one for both: K2 is fitted to whatever stretch of curve it is shown, and correcting over one range while integrating over another costs more than half the benefit of correcting at all (CCC 0.9825 against 0.9309 on the GBM-DSC-MRI reference object). Returns the whole length when the curve never turns back up, or -1 when the inputs cannot support the reading.

leakage_correct

leakage_correct(ct, ref, dt_s)

Boxerman–Schmainda–Weisskoff leakage correction of one curve.

Fits ct ≈ K1·ref − K2·∫ref against a NON-LEAKING reference curve and returns ct + K2·∫ref — the curve with the extravasation term added back. Returns {"corrected": [T], "k2": float}.

Which voxels are non-leaking is the caller's decision: whole brain works in vivo because leaking voxels are a small minority of it, but an unmasked reference is destroyed by air, where the log floor sends ΔR2* to ~460 s⁻¹. For a phantom or a masked volume, pass the NAWM ROI mean.

measure_aif

measure_aif(series, mask, te_s, *, n_baseline=None)

Extract a ΔR2* arterial input function from an arterial ROI.

Averages the ROI's T2-weighted signal per time frame and converts to ΔR2 with :func:signal_to_conc. series is (T, H, W) or (T, Z, Y, X); mask is a non-zero ROI over the spatial dims. Returns the AIF C_a(t) (length T) — pass it to :func:fit / :func:deconvolve as aif=. n_baseline (pre-bolus frames) is auto-detected from the bolus DROP when None.

NB: this returns the whole-blood ΔR2* AIF. Absolute quantification applies the ρ·(1−Hct) corrections downstream; relative maps need none.

deconvolve

deconvolve(ct, aif, dt_s, method='osvd', *, reg=0.0)

Deconvolve ONE tissue curve against an AIF → perfusion parameters.

Returns {"cbf", "cbv", "mtt", "tmax", "residue", "ok", "oi", "frac", "cut", "tail", "quality"}. reg is the SVD truncation threshold (sSVD/cSVD) or the oscillation-index target (oSVD); reg=0 selects the method default (0.20 sSVD, 0.10 cSVD, 0.035 oSVD). MTT/Tmax in seconds; CBF/CBV relative.

Check ok. The deconvolution produces numbers for almost any input, so a non-finite sample, a threshold that discards the whole spectrum, or a residue that never leaves zero would otherwise come back as cbf = mtt = tmax = 0 — which on a map is indistinguishable from a voxel with no perfusion. Those return ok=False with the reason in quality, a bitmask of dcm_dsc.h's DSC_Q_*; they do NOT raise, because a caller looping over voxels meets them on ordinary background. Only a shape or context failure (bad lengths, dt <= 0, a degenerate AIF) raises.

The remaining fields say how much regularisation went into the answer: oi the achieved oscillation index, frac the truncation threshold actually used, cut the fraction of the curve's energy the truncation discarded, and tail the residue at the last sample as a fraction of its peak (large means the acquisition ended before the tissue cleared, so MTT is truncated low).

fit

fit(
    series,
    times_s,
    aif,
    method="osvd",
    *,
    input="concentration",
    reg=0.0,
    te_s=0.03,
    n_baseline=0,
    mask=None,
    enhance_thresh=0.0,
    leakage=False
)

Voxel-wise DSC deconvolution over a (T, H, W) series → perfusion maps.

Returns {"cbf", "cbv", "mtt", "tmax", "ttp"} — each an (H, W) float32 map — plus "fitted" (voxels deconvolved). Tmax is the residue peak time (deconvolution); TTP the enhancement peak time (semi-quant). The AIF matrix is SVD-factorised once and reused across voxels. leakage=True applies Boxerman-Schmainda correction against the slice-mean reference (tumour DSC).

Parameters

series : (T, H, W) — ΔR2 concentration, or raw T2 signal when input='raw'. times_s : (T,) acquisition times in seconds (uniform spacing). aif : (T,) arterial input ΔR2 on the same grid (required — DSC has no population AIF). method : 'ssvd' | 'csvd' | 'osvd' (default). input : 'concentration' (default) or 'raw' (convert signal→ΔR2 per voxel). reg : SVD threshold / oSVD oscillation target; 0 → method default. te_s, n_baseline : ΔR2* conversion params (used when input='raw'). mask : optional (H, W) array; non-zero voxels are fitted. enhance_thresh : skip voxels whose peak |Δconc| is below this (noise gate).

fit_series

fit_series(
    source,
    times_s=None,
    aif=None,
    method="osvd",
    *,
    input="concentration",
    reg=0.0,
    te_s=None,
    n_baseline=0,
    mask=None,
    enhance_thresh=0.0,
    leakage=False
)

Fit a whole DSC series → perfusion-map volumes.

source may be a DICOM directory / file-list / enhanced-multiframe file (assembled via :func:pydcm.load_4d), a loaded :class:pydcm.Volume4D, or a raw (T, Z, Y, X) / (T, H, W) array. The whole volume is fitted in one native parallel call — the AIF SVD is built once and the Z slices fan across cores in C++ (:func:pydcm.dsc.fit is the single-slice form). Shares the :mod:pydcm.perfusion feeder with DCE for the DICOM assembly + timing.

Returns {"cbf", "cbv", "mtt", "tmax", "ttp"} — each a (Z, H, W) float32 volume — plus "fitted" and "times_s".

aif (length T, ΔR2*) is REQUIRED (DSC has no population AIF) — measure it from an arterial ROI with :func:measure_aif. times_s and te_s are read from the DICOM tags when omitted; mask may be (H, W) or (Z, H, W).

write_param_maps

write_param_maps(
    reference,
    result,
    params=("cbf", "cbv", "mtt", "tmax", "ttp"),
    *,
    dtype=None,
    output_dir=None
)

Emit DSC perfusion VOLUMES as DICOM Parametric Maps (multi-frame when 3-D).

Thin convenience over :func:pydcm.write_paramap supplying the per-parameter UCUM units (CBF mL/100mL/min, CBV mL/100mL, MTT/Tmax s): each requested map in result (from :func:fit) is written against reference (the source DSC slices, one per Z plane). Returns {param: Part-10 bytes}, or {param: path} when output_dir is given.

NB: the units labels are NOMINAL. :func:fit returns CALIBRATION-FREE relative CBF/CBV (MTT/Tmax are already absolute seconds); to store true mL/100mL[/min] you must pre-scale the maps by the absolute calibration (k·ρ·(1−Hct) and the s→min / fraction→mL/100mL factors) before writing.

pydcm.perfusion

Shared dynamic-perfusion DICOM feeder — the DICOM-side glue common to DCE & DSC.

Both modalities take the SAME shape of input — a 4-D dynamic acquisition (a directory / file-list / enhanced-multiframe file / loaded :class:pydcm.Volume4D / raw array) — and need the SAME two things the native compute core cannot do:

  1. assemble the temporal stack into a contiguous (T, Z, H, W) cube, and
  2. recover the per-frame acquisition times off the DICOM tags.

This module is that one feeder (so :mod:pydcm.dce and :mod:pydcm.dsc do not each re-roll the tag parsing). Times are returned in seconds — DSC uses them verbatim, DCE divides by 60. The vendor-aware 4-D assembly itself lives lower still (native load_4d / Volume4D); this layer only adds the perfusion timing + sequence-parameter extraction on top.

TICMetrics

Readouts off one time–intensity curve.

Attributes (values in the caller's own units; times carry through unchanged): baseline: mean of the first n_baseline samples. peak / peak_time, trough / trough_time: extremes of the RAW curve, not a smoothed one. enhancement: (peak - baseline) / abs(baseline). nan when the baseline is 0 — a relative change from nothing has no value, and 0 there would read as "no enhancement". washin: mean slope from the last baseline sample to the peak, per time unit. nan when the peak is at or before the baseline window. washout: mean slope from the peak to the LAST sample (negative for a curve that comes back down). nan when the peak IS the last sample. auc: trapezoidal integral of value - baseline, signed — a curve that dips below baseline subtracts. rise_time: time to the first crossing of baseline + rise_frac * (peak - baseline), linearly interpolated. nan when the curve never reaches it. n_baseline: the window actually used, after clamping into range. ok: the readouts are meaningful.

A nan above is a term that is undefined for this curve, not a failed computation and not a zero.

roi_signal

roi_signal(series, mask)

Mean signal per time frame over a non-zero spatial ROI mask.

series is (T, H, W) or (T, Z, Y, X); mask is broadcast over the spatial dims. Returns the ROI mean curve (T,) as float64.

auto_n_baseline

auto_n_baseline(roi, *, rising)

Pre-bolus frame count = first frame departing the leading plateau by >10%.

rising=True for DCE (T1 enhancement rises), rising=False for DSC (T2* susceptibility drops). Used as the S0 averaging window for signal→conc.

series_times_s

series_times_s(paths, frames=None)

Per-volume acquisition times (seconds, t[0]=0) from DICOM tags, or None.

Enhanced multiframe (one file repeated across volumes, distinct frames) is read from the per-frame functional groups; classic multi-instance series use the per-file top-level tags (acquisition-clock preference order).

seq_tr_fa

seq_tr_fa(path0)

(RepetitionTime[s], FlipAngle[deg]) off one instance, or (None, None).

seq_te

seq_te(path0)

EchoTime[s] off one instance, or None.

assemble_4d

assemble_4d(source)

Resolve a perfusion source to (pixels[T,Z,H,W] f32, paths, vol4d).

source may be a DICOM directory / file-list / enhanced-multiframe file (assembled via native :func:pydcm.load_4d), a loaded :class:pydcm.Volume4D, or a raw (T,Z,Y,X) / (T,H,W) array. The 4th DICOM dimension is checked to be TEMPORAL (load_4d also stacks b-value / echo / cardiac-phase series the same way — fitting one of those as a time course would be silently wrong).

Returns the contiguous cube plus paths (per-volume file list, or None for a raw array) and the vol4d (or None) so callers can pull timing / geometry.

frames_of

frames_of(vol4d)

The per-volume enhanced-MF frame indices (or None) for vol4d.

broadcast_zhw

broadcast_zhw(arr, Z, dtype)

Normalise an optional per-voxel array (mask / T1 map) to (Z, H, W).

Accepts None (→ None), a 2-D (H, W) plane (→ repeated across Z, the common "same ROI on every slice" case), or an already-3-D (Z, H, W) volume.

tic

tic(t, v, *, n_baseline=None, rise_frac=0.5)

Readouts off one time–intensity curve v sampled at times t.

t must be strictly increasing and both arrays finite — the integral and the slopes are measured along t, so an unsorted axis or a nan sample would produce a number that looks fine and is not; both raise.

n_baseline is how many leading samples are averaged for the baseline. None takes the engine's documented fallback — a tenth of the curve, at least one sample — which is this library's choice, not a field convention. A caller that knows when contrast arrived should say so instead; :func:tic_roi will detect it for you with rising=.

rise_frac is the fraction used for rise_time (0.5 = the conventional half-rise). Out-of-range values yield a nan rise_time rather than being clamped to a different question.

tic_roi

tic_roi(
    series,
    mask,
    times_s,
    *,
    n_baseline=None,
    rising=None,
    rise_frac=0.5
)

Readouts off the mean curve of a spatial ROI in a dynamic series.

series is (T, H, W) or (T, Z, Y, X) and mask is broadcast over the spatial dims — the same pair :func:roi_signal takes, and the same one dce.measure_aif / dsc.measure_aif take, so an arterial ROI can be read both ways without being re-extracted.

times_s are the per-frame times (see :func:series_times_s).

Baseline window, in order of precedence:

  • n_baseline — an explicit count.
  • rising — detect it with :func:auto_n_baseline: the first frame departing the leading plateau by >10%. True for a curve that rises (T1 enhancement), False for one that drops (T2* susceptibility). This is the "say when contrast arrived" path, and it is what to use on a contrast run.
  • neither — the engine's arbitrary fallback.

pydcm.dti

pydcm DTI — diffusion tensor estimation, scalar maps and deterministic tracking.

A thin NumPy surface over the native dcm_dti engine. Every number is computed in the C core; this module marshals arrays, orchestrates a DICOM series, and refuses the inputs the engine would otherwise accept and quietly mis-fit.

From a DICOM series::

import pydcm.dti as dti
res = dti.fit_series("ep2d_diff/")      # b0 split, fit, maps
res["FA"], res["MD"]                    # (Z, Y, X) float32
res.affine                              # voxel -> patient, row-major 4x4

tracks = dti.track_series("ep2d_diff/") # streamlines in PATIENT mm
dti.write_tracts("ep2d_diff/", tracks, "tracts.dcm")

From arrays already in hand::

res = dti.fit(volumes, bvals, bvecs)    # volumes (V, Z, Y, X)
maps = dti.fit_maps(b0, dwi, bvals, bvecs, maps=("FA", "MD"))   # flat voxel axis

Units: b-values in s/mm², so diffusivities (MD/AD/RD) are mm²/s and FA is dimensionless. Streamline coordinates are mm — grid mm from :func:track, patient mm from :func:track_series and anything that goes to DICOM.

Two things this module refuses rather than passing through, because the engine cannot tell and the result looks plausible either way:

  • b-vectors must be unit length. The design matrix carries |g|², so a half-length vector scales every diffusivity by 4 while leaving FA untouched.
  • baselines are not bvals == 0. UIH writes 1.25 for its b=0 volume and Siemens reports 50 for volumes that are baselines in every other respect, so the split goes through the native per-manufacturer threshold.

And one it cannot: fit_maps takes dwi direction-major, and when n_dirs equals n_voxels a transposed array is shape-valid under both readings, so no check distinguishes them. :func:fit and :func:fit_series build that array themselves and are the reason to prefer them — the raw tier is there for callers who already have a flat voxel axis and know which way round it is.

DtiResult

Bases: dict

The maps, plus the geometry needed to write or track them.

A dict of {map name: array} so it reads like the DCE result, with the grid carried alongside — a map without its affine cannot be written to anything.

map_units

map_units(name)

UCUM (code, scheme, meaning) for a map name, or None if it has no units (DEC, which is a colour).

baseline_mask

baseline_mask(bvals, *, manufacturer=None, threshold=None)

Which volumes are baselines rather than diffusion directions.

bvals: b-value per volume. manufacturer: DICOM Manufacturer (0008,0070). Selects the threshold — Siemens needs a higher one because it reports b=50 for volumes that are baselines in every other respect. threshold: override the per-manufacturer value entirely.

Returns a boolean array, True where the volume is a baseline.

Not bvals == 0: UIH writes 1.25 for its b=0 volume, which that test would hand to the tensor fit as a diffusion direction with an arbitrary gradient.

head_mask

head_mask(b0, *, cleanup=False)

Head mask from a 3-D b=0 baseline. Returns (mask uint8 [Z,Y,X], kept).

Median filter, Otsu's threshold, dilate. cleanup additionally removes islands and fills pockets: better for seeding, but a departure from the reference implementation this was matched against.

fit_maps

fit_maps(
    b0, dwi, bvals, bvecs, maps=_DEFAULT_MAPS, *, wls=False
)

Tensor fit and scalar maps over a FLAT voxel axis — the thinnest tier.

b0: [n_voxels] mean baseline signal. dwi: [n_dirs, n_voxels] DW signal, direction-major (the DW axis FIRST). bvals: [n_dirs] b-values, baselines already excluded. bvecs: [n_dirs, 3] unit gradient directions. maps: which maps to compute; see :data:MAPS. wls: weight by signal² instead of ordinary least squares.

Returns {name: array} — float32 [n_voxels], except DEC which is uint8 [n_voxels, 4] RGBA.

The shape checks below catch a transposed dwi only when n_dirs and n_voxels differ; when they are equal both readings are valid and the fit silently returns garbage. Use :func:fit if you have a volume stack.

fit

fit(
    volumes,
    bvals,
    bvecs,
    *,
    maps=_DEFAULT_MAPS,
    wls=False,
    manufacturer=None,
    threshold=None,
    affine=None,
    mask=False,
    cleanup=False
)

Tensor fit over a 4-D volume stack, with the baseline split handled.

volumes: (V, Z, Y, X) — the layout :func:pydcm.load_dwi returns. bvals: [V]; bvecs: [V, 3] or (3, V), INCLUDING the baselines. maps, wls: as :func:fit_maps. manufacturer / threshold: how to recognise a baseline; see :func:baseline_mask. affine: voxel -> patient 4x4, carried into the result for writing and tracking. Identity if omitted. mask: zero the maps outside the head. False by default, so what comes back is the fit as measured; True builds one with :func:head_mask, or pass your own array.

Returns a :class:DtiResult of (Z, Y, X) arrays (DEC is (Z, Y, X, 4)).

Read the background before trusting a histogram of this: outside the head the signal is noise, the log-linear fit is degenerate, and the eigen decomposition returns an arbitrary direction with FA near 1. An unmasked FA map therefore has its maximum in air, and summary statistics over the whole volume describe mostly background. mask=True is the fix; :func:track applies one by default for the same reason.

fit_series

fit_series(
    series,
    *,
    maps=_DEFAULT_MAPS,
    wls=False,
    recursive=True,
    order="gradient",
    threshold=None,
    mask=False,
    cleanup=False
)

A DICOM DWI series -> a :class:DtiResult.

Reads the series through :func:pydcm.load_dwi, whose gradients are already in the voxel convention that pairs with its pixel data, splits the baseline using the series' own Manufacturer, and fits. See :func:fit for mask and for why an unmasked FA map peaks in air.

track

track(
    volumes,
    bvals,
    bvecs,
    *,
    affine=None,
    manufacturer=None,
    threshold=None,
    wls=False,
    mask=True,
    cleanup=False,
    fa_threshold=0.15,
    angle_deg=45.0,
    step_size=0.5,
    max_steps=2000,
    seed_fa_min=0.3,
    max_tracks=100000,
    max_total_points=10000000,
    patient=True
)

Deterministic RK4 tractography from a 4-D DWI stack.

volumes, bvals, bvecs, affine, manufacturer, threshold, wls: as :func:fit. mask: apply a head mask before seeding. True by default and it matters: outside the head the fit is degenerate and FA approaches 1, so an unmasked run spends most of its budget on background. Pass an array to supply your own, or False to seed everywhere. cleanup: passed to :func:head_mask when building the mask. angle_deg: maximum turn per step, in DEGREES. The engine takes a cosine; this converts, so a caller cannot pass 45 and get cos(45)=0.7071's meaning by accident. step_size: RK4 step in VOXEL units, not mm. patient: return streamlines in patient coordinates. False returns the grid mm the renderer uses. Anything written to DICOM needs patient.

Returns a list of (P, 3) arrays — float64 patient mm, or float32 grid mm when patient=False.

track_series

track_series(
    series, *, recursive=True, order="gradient", **kwargs
)

A DICOM DWI series -> streamlines in patient coordinates.

write_tracts

write_tracts(
    reference,
    tracks,
    output=None,
    *,
    label="DTI",
    description="Deterministic tensor tractography",
    rgb=None
)

Write streamlines as a DICOM Tractography Results object.

tracks must be in PATIENT coordinates — what :func:track_series and track(..., patient=True) return. Grid mm would write a well-formed object whose tracts are rotated and displaced out of the reference's Frame of Reference, which no reader can detect.

reference supplies the demographics and the Frame of Reference UID those coordinates are expressed in, so it should be the series the tracks were computed from — a directory, as :func:fit_series and :func:track_series take, or a list of instances. The anatomy and diffusion-model codes default to White Matter and Single Tensor, which is what this engine produces.

Returns the output path when output is given, matching :func:save_dwi, and the Part-10 bytes when it is not.

Derived objects — authoring & reading

Author and read the structured DICOM objects (see the how-to recipes).

Segmentations (SEG)

pydcm.seg

pydcm — DICOM Segmentation authoring + reading (pydcm.seg).

Author coded binary or fractional Segmentations from a labelmap / probability maps, and read a Segmentation back to a labelmap / per-segment masks, over the shared native SEG write/decode engines — a native interop path for the common cases.

SegmentReader

Read a DICOM Segmentation into per-segment masks.

MultiClassReader

Read a (non-overlapping) Segmentation into one label-map volume.

AlgorithmIdentificationSequence

Identifies the algorithm that produced a segment.

SegmentDescription

Description of one segment.

write_seg

write_seg(
    reference,
    labelmap,
    segments,
    output=None,
    sop_instance_uid="",
    series_instance_uid="",
    dimension_organization_uid="",
    content_date="",
    content_time="",
)

Author a coded BINARY DICOM Segmentation from a labelmap + segment terminology.

a source-image path, or a list of the source series' instance paths

— geometry, demographics and source references are taken from it.

labelmap: a uint16 array (H, W) or (slices, H, W); value k marks the segment whose labelID is k. For a series the slices must be ordered by ascending position. segments: list of dicts, each with label, labelID, rgb = (r, g, b), category / type / anatomic = (CodeValue, CodingScheme, CodeMeaning), algorithm_type, algorithm_name. output: write the SEG there and return None; if omitted, return Part-10 bytes.

sop_instance_uid / series_instance_uid / dimension_organization_uid / content_date (YYYYMMDD) / content_time (HHMMSS): this segmentation's own identity. Left empty, every one is derived from the study, so two runs over one study produce SEGs claiming to be the SAME instance and a second producer collides with the first. Supply :func:pydcm.generate_uid values — a fresh SOP per run and a per-producer series — where that matters; the timestamps let a consumer order the runs.

write_seg_fractional

write_seg_fractional(
    reference,
    maps,
    segments,
    *,
    type="probability",
    max_value=255,
    output=None,
    sop_instance_uid="",
    series_instance_uid="",
    dimension_organization_uid="",
    content_date="",
    content_time=""
)

Author a FRACTIONAL DICOM Segmentation from per-segment probability/occupancy maps.

The natural output of a soft-prediction model — each segment keeps its 8-bit value map instead of a hard 1-bit mask.

reference / segments / output: as in :func:write_seg. maps: array [nseg, (slices,) H, W] (segment-major; maps[i] is segment i's map). Float input is treated as 0..1 and scaled to 0..max_value; integer input is used as-is. type: 'probability' or 'occupancy' (Segmentation Fractional Type).

sop_instance_uid / series_instance_uid / dimension_organization_uid / content_date (YYYYMMDD) / content_time (HHMMSS): this segmentation's own identity. Left empty, every one is derived from the study, so two runs over one study produce SEGs claiming to be the SAME instance and a second producer collides with the first. Supply :func:pydcm.generate_uid values — a fresh SOP per run and a per-producer series — where that matters; the timestamps let a consumer order the runs.

write_seg_overlapping

write_seg_overlapping(
    reference,
    masks,
    segments,
    output=None,
    sop_instance_uid="",
    series_instance_uid="",
    dimension_organization_uid="",
    content_date="",
    content_time="",
)

Author a BINARY DICOM Segmentation whose segments may OVERLAP.

:func:write_seg takes a label map, so a pixel there has exactly one segment. DICOM has no such restriction — every frame names its own segment through Per-frame Segment Identification (0062,000B), so two frames may claim the same pixel of the same source image. A model with hierarchical labels produces exactly that: "spine" contains "cervical spine" contains "C1", and one pixel is all three. Flattened to a label map, the containing structures are silently gone; this writer keeps them.

reference / segments / output: as in :func:write_seg. masks: array [nseg, (slices,) H, W] (segment-major, the shape :func:write_seg_fractional takes). Non-zero means the pixel belongs to that segment. Frames with no set pixel are skipped, so passing one frame per segment per slice is fine.

Bit-packed like :func:write_seg's frames — the same information as a fractional occupancy map at one eighth the bytes.

sop_instance_uid / series_instance_uid / dimension_organization_uid / content_date (YYYYMMDD) / content_time (HHMMSS): this segmentation's own identity. Left empty, every one is derived from the study, so two runs over one study produce SEGs claiming to be the SAME instance and a second producer collides with the first. Supply :func:pydcm.generate_uid values — a fresh SOP per run and a per-producer series — where that matters; the timestamps let a consumer order the runs.

write_seg_from_prediction

write_seg_from_prediction(
    prediction, reference, segments, output=None
)

Map a model prediction back onto the original DICOM grid and write a coded SEG.

Closes the inference loop: preprocess a series (resample / crop / reorient / transpose), run a model, then call this to put the label map back where it came from. prediction is resampled onto the reference series' grid by AFFINE (label-safe nearest), so whatever spatial preprocessing produced it is inverted geometrically — no recorded op-stack needed.

a label-map :class:~pydcm.volume.Volume — integer voxels, value k

marks the segment with labelID k — carrying its (processed-space) affine. For soft probabilities [C, Z, Y, X] take :func:pydcm.transforms.argmax first.

reference: the original series — a directory or list of instance paths. Its grid, demographics and per-slice references define the SEG. Precondition: a single coherent 3-D series (one orientation, one stack). The resample target comes from :func:load_series (which IOP-clusters + splits temporal/echo/b-value onto a 4th axis), while :func:write_seg keeps every dims-matching slice — so a 4-D / multi-echo / perfusion / multi-orientation reference makes the two grids disagree and raises a clear error; pass the coherent sub-series in that case. segments / output: as in :func:write_seg.

seg_from_nifti

seg_from_nifti(reference, mask, segments, output=None)

Author a coded DICOM Segmentation from a NIfTI label volume + reference series.

The NIfTI / FSL / ANTs → DICOM-SEG return path — the converter cannot produce. mask is a .nii/.nii.gz label volume co-framed with reference (the natural NIfTI → segment → mask-back round-trip); the native reader flips its Z axis to the reference's ascending-position order via the affine, so the labels land on the right slices.

a source-image path, a directory of the series' instances, or a list

of instance paths — geometry / demographics / source references come from it.

mask: path to the NIfTI label volume. segments: as in :func:write_seg (labelID selects which label maps to each). output: write the SEG there and return None; if omitted, return Part-10 bytes.

read_seg

read_seg(path, *, masks=False)

Reconstruct a DICOM Segmentation, over the shared native SEG decode engine (geometry-correct: frames are placed onto a slice grid built from the per-frame Image Position projected along the slice normal — unlike the simpler :class:MultiClassReader).

masks=False (default): (labelmap, meta)labelmap is (slices, rows, cols) uint16, voxel value = DICOM Segment Number (0 = background). meta["overlapping"] flags overlapping segments (combined labelmap is lossy there — use masks=True). masks=True: (masks, meta)(nseg, slices, rows, cols) float32 occupancy in [0, 1] (binary → 0/1, fractional → value/max); lossless for overlapping / fractional. Plane k is segment meta["segment_numbers"][k].

meta carries per-segment terminology (segments: number / label / category / type / anatomic codes / rgb), geometry (image_orientation_patient, pixel_spacing, slice_thickness, slice_origins) and a 4×4 affine (voxel→world LPS mm). Returns None when path is not a Segmentation.

Segmentation

Segmentation(
    source_images,
    pixel_array,
    segmentation_type,
    segment_descriptions,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name=None,
    software_versions=None,
    device_serial_number=None,
    *,
    fractional_type="PROBABILITY",
    max_fractional_value=255,
    content_description=None,
    content_label=None,
    content_creator_name=None,
    transfer_syntax_uid=None,
    **_kwargs
)

Constructor — returns a pydcm Dataset.

Built over the native write_seg / write_seg_fractional: source_images supply geometry/demographics, pixel_array is the labelmap (BINARY/LABELMAP) or per-segment maps (FRACTIONAL), segment_descriptions the coded terminology. extra kwargs are accepted for source compatibility.

Parametric maps

pydcm.paramap

pydcm — DICOM Parametric Map authoring + reading (pydcm.paramap).

Parametric Map conversion (image ↔ paramap), over the native engine:

  • :func:write_paramap — author a float Parametric Map (SOP 1.2.840.10008.5.1.4.1.1.30) from a real-valued array + the source series' geometry + a Real World Value Mapping (units / quantity / slope / intercept), via the native parametric-map engine (the float-pixel counterpart of the SEG writer). a native capability authoring; this is pydcm-native value-add.
  • :func:read_paramap — read a Parametric Map back to a real-valued float32 array
  • metadata (the Real World Value Mapping). Float / double-float pixel data decode directly; integer-stored maps have their RWVM slope/intercept applied. Reads third-party maps, not only pydcm's own output.

write_paramap

write_paramap(
    reference,
    values,
    *,
    units=None,
    quantity=None,
    slope=None,
    intercept=None,
    label=None,
    explanation=None,
    dtype=None,
    output=None,
    sop_instance_uid="",
    dimension_organization_uid="",
    content_date="",
    content_time=""
)

Author a DICOM Parametric Map from a real-valued array.

a source-image path, or the list of source-series instance paths —

geometry, demographics and Frame of Reference are taken from it (one slice per array plane, ordered by position).

values: a float array (H, W) or (slices, H, W) of real-world values (one plane per reference slice). units: the measurement units — (code, scheme, meaning) (UCUM by default), (code, meaning), a plain meaning string, or a dict. E.g. ("um2/s", "UCUM", "um2/s"). quantity: the measured quantity code (value, scheme, meaning) (DCM by default), e.g. ("113041", "DCM", "Apparent Diffusion Coefficient"). dtype: the stored pixel type. None (default) → 32-bit float (FloatingPointImagePixel; the values are stored verbatim). "uint16" / "int16" / "uint8" / "int8" → integer pixels quantized through the Real World Value Mapping (stored = round((value - intercept) / slope)), so a reader recovers value = stored * slope + intercept. slope / intercept: Real World Value Mapping slope / intercept. For float storage the default is identity (1 / 0 — values are already real-world). For an integer dtype left unset, they are auto-computed to span the value range across the integer range (lossy only by the quantization step); pass them to control the scaling explicitly. output: write the map there and return None; if omitted, return Part-10 bytes.

sop_instance_uid / content_date (YYYYMMDD) / content_time (HHMMSS) / dimension_organization_uid: this object's own identity. Left empty, the SOP Instance UID is derived deterministically from the study, so two built for one study carry the same one — right for a single self-contained export, a DICOM global-uniqueness violation for a producer that mints many.

read_paramap

read_paramap(path)

Read a DICOM Parametric Map to (values, meta).

a float32 array (frames, rows, cols) of real-world values. Float /

double-float pixel data is returned directly; an integer-stored map has its Real World Value Mapping (slope/intercept) applied.

meta: the geometry sidecar (as :func:pydcm.decode) plus is_parametric_map and, when present, real_world_value_mapping = {slope, intercept, units, label, first_value_mapped, last_value_mapped, has_lut}.

Structured reports (SR / TID 1500)

pydcm.sr

pydcm Structured Reporting (pydcm.sr) — compat surface + native engine.

  • compatCode (a coded-concept shape) and content_json, for from pydcm.sr import … imports.
  • pydcm-native (over the shared native SR engines): author any Comprehensive SR from a content-tree dict (write_sr); author / read a TID 1500 Measurement Report (write_report / read_report); validate an SR content tree (sr_validate); and look up the PS3.16 coded-concept table (sr_code_meaning / sr_validate_code / sr_cid_has).

TrackingIdentifier

A measurement group's tracking identity.

FindingSite

An anatomic location code.

Measurement

One numeric measurement (name/value/unit + qualifiers).

QualitativeEvaluation

A coded name/value evaluation.

SourceImageForRegion

The image a 2D region is drawn on.

ImageRegion

A 2D ROI (SCOORD) on a source image.

ImageRegion3D

A 3D ROI (SCOORD3D) in a Frame of Reference.

MeasurementsAndQualitativeEvaluations

Measurement group (no ROI).

PlanarROIMeasurementsAndQualitativeEvaluations

Bases: MeasurementsAndQualitativeEvaluations

Planar (2D) ROI measurement group.

VolumetricROIMeasurementsAndQualitativeEvaluations

Bases: PlanarROIMeasurementsAndQualitativeEvaluations

Volumetric (3D) ROI measurement group.

PersonObserverIdentifyingAttributes

Person observer identifying attributes.

DeviceObserverIdentifyingAttributes

Device observer identifying attributes.

ObserverContext

Wraps a person/device observer.

ObservationContext

Observer (+ subject) context.

MeasurementReport

The TID 1500 root (observation context + procedure + measurement groups).

ContentItem

Base for the SR content-item primitives — a typed SR tree node.

append

append(item)

Add a child content item.

ContentSequence

Bases: list

An ordered list of content items.

content_json

content_json(ds)

The semantic SR content tree (reuses the native content engine via :func:pydcm.content).

write_sr

write_sr(document, output=None)

Author a Comprehensive DICOM Structured Report from a content-tree dict.

The general SR writer (vs. a fixed template): build any tree of content items.

a dict with patient_name / patient_id / study_uid /

study_date / series_uid (+ optional sop_class_uid / completion_flag / verification_flag), a title code {value, scheme, meaning} (the root CONTAINER's Concept Name), and a content list. Each content item: relationship ("CONTAINS", …), value_type ("CODE"/"NUM"/"TEXT"/"CONTAINER"/"IMAGE"/"SCOORD"/…), concept code, and per-type fields — text; code ({value,scheme, meaning}); value + unit (NUM); datetime; ref_sop_class / ref_sop_instance (IMAGE); graphic_type + graphic_data (SCOORD) — plus a nested content list for children.

output: write the SR there and return None; if omitted, return Part-10 bytes.

write_report

write_report(
    measurements,
    *,
    reference=None,
    patient_name="",
    patient_id="",
    study_uid="",
    study_date="",
    series_uid="",
    output=None,
    sop_instance_uid=""
)

Author a TID 1500 Measurement Report SR from a list of measurements, over the same native SR-export engine.

a list of measurement dicts, each with concept_value /

concept_scheme / concept_meaning (the measured quantity's code, e.g. "103355008", "SCT", "Width"), value (float), unit_code / unit_meaning (UCUM, e.g. "mm"), and optionally ref_sop_class_uid / ref_sop_instance_uid (the measured image), graphic_type ("POINT" / "POLYLINE" / "CIRCLE" / "ELLIPSE") and scoord ([col0, row0, col1, row1, …] pixel coordinates). scoord is recorded only when ref_sop_instance_uid is also given — spatial coordinates are stored relative to their referenced image. May instead be a full document dict ({patient_…, study_…, series_uid, measurements: […]}, the shape :func:read_report returns) so write_report(read_report(x)) round-trips.

reference: a DICOM path to inherit patient + study identity from (the report attaches to that study); explicit keyword args take precedence. Study/Series UIDs are content-derived when neither given nor inherited. output: write the SR there and return None; if omitted, return Part-10 bytes.

read_report

read_report(path)

The measurements of a TID 1500 Measurement Report SR. Returns {patient_name, patient_id, study_uid, study_date, series_uid, measurements: [...]} round-tripping :func:write_report's input (measurements is empty when path carries no SR content).

sr_to_html

sr_to_html(path)

Render a DICOM Structured Report to clinical-readable HTML (a str).

Renders any SR to standalone, clinical-readable markup — not only the TID 1500 measurement-report shape.

write_measurement_report

write_measurement_report(document, output=None)

Author a TYPED TID 1500 Measurement Report — the standard TID 1500 measurement-report capability, over the native SR authoring engine (structure cross-validated against reference SR implementations). Unlike :func:write_report (a flat list of measurements) this builds the full TID 1500 structure: observation context, measurement groups (TID 1411/1501), each with tracking identity, finding + finding sites, an optional ROI region, NUM measurements (TID 300, with method / derivation / per-measurement finding sites) and qualitative evaluations.

a dict with patient_name / patient_id / study_uid /

study_date / series_uid; optional observer = {type: "device"|"person", name, uid}, procedure_reported (a code), language (default "en-US"); and groups — each {tracking_id, tracking_uid, finding?, finding_sites?[], roi?, measurements[], qualitative_evaluations?[]}. A code is {value, scheme, meaning}. A measurement is {name, value, unit, method?, derivation?, finding_sites?[]}. A roi is {graphic_type, scoord:[...], is_3d?, frame_of_reference_uid?, ref_sop_class_uid?, ref_sop_instance_uid?}. A qualitative evaluation is {name, value} (both codes).

output: write the SR there and return None; if omitted, return Part-10 bytes.

read_measurement_report

read_measurement_report(path)

The typed TID 1500 Measurement Report of an SR — {patient/study, observer, procedure_reported?, groups: [...]} round-tripping :func:write_measurement_report (empty groups when path is not a measurement report). Reads third-party SR reports, not just pydcm's own output.

sr_code_meaning

sr_code_meaning(scheme, value)

Code Meaning for a coded concept (scheme, value) from the DICOM PS3.16 Content Mapping Resource (the most complete public set), or None if the code is unknown. E.g. sr_code_meaning("DCM", "126000")"Imaging Measurement Report".

sr_validate_code

sr_validate_code(scheme, value, meaning=None)

True if (scheme, value) is a known coded concept and — when meaning is given — its Code Meaning matches it (a typo / wrong-meaning check).

sr_cid_has

sr_cid_has(cid, scheme, value)

True if the coded concept (scheme, value) is a member of Context Group cid (e.g. sr_cid_has(7469, "SCT", "103339001")).

sr_validate

sr_validate(path)

Validate an SR file's content tree — structural well-formedness (root is a CONTAINER, valid value types / relationships, NUM has units, CODE has a value, …), coded-concept conformance against the PS3.16 table, AND TID content-template conformance (measurement-group mandatory rows + cardinality, value-type per row, value-set-per-concept, container nesting, and conditional Observer / Subject / Algorithm-Identification rows) — returning a list of {severity, location, message} findings (empty = a conformant SR).

Comprehensive3DSR

Comprehensive3DSR(
    evidence,
    content,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer=None,
    **_kw
)

Constructor — a TID 1500 SR Dataset.

read_regions

read_regions(path)

Every spatial region an SR delineates, wherever the document put it.

The complement of :func:read_report, not a replacement for it. A report reader walks the template — root → Imaging Measurements → Measurement Group → region — and that is right for a report: it finds a measurement that carries no shape at all.

An importer cannot use that walk. Real producers put regions at five different depths and three of them carry no DCM 125007 anywhere in the chain, so a template-driven walk returns nothing for them. This enumerates every SCOORD / SCOORD3D regardless of ancestry — plus every image reference that names a segment, which is TID 1411's way of saying the region of interest is that segment — and gathers each one's context by looking around it.

It is the same enumeration the native ROI importer drives, so a reader and an importer cannot disagree about what a document contains.

Returns a list of dicts:

kind "REGION" (coordinates) or "SEGMENT". graphic_type the Defined Term — "POINT", "POLYLINE", "POLYGON", "ELLIPSOID"… — or None for a segment. points (n, 2) float32 column/row in the referenced image, or (n, 3) patient-LPS mm when is_3d. frame_of_reference_uid the space a 3-D region is in. ref_sop_class_uid / ref_sop_instance_uid / referenced_frame_number the image the region names (a volumetric group may name one too); the frame number is 1-based or None. segment_number / segment_sop_instance_uid for a SEGMENT region. tracking_uid / tracking_id (112040) / (112039) — the only stable name a region has across exports. has_measurement, concept, unit_code, unit_meaning, value the measurement associated with the region, when there was one. finding_meaning the finding the region sits under.

Key Object Selection

pydcm.ko

pydcm — DICOM Key Object Selection authoring + reading (pydcm.ko).

The ko capability: flag instances as "key objects" (a KOS document, PS3.3 KOS IOD / PS3.16 TID 2010) and read one back, over the native KOS engine — a native authoring capability.

KeyObjectSelection

Content: a document-title code + the objects flagged as key.

write_ko

write_ko(
    references,
    *,
    patient_name="",
    patient_id="",
    study_uid="",
    study_date="",
    study_time="",
    study_id="",
    accession_number="",
    title=None,
    output=None,
    sop_instance_uid="",
    series_instance_uid="",
    retrieve_ae_title="",
    retrieve_location_uid=""
)

Author a Key Object Selection document flagging references as key objects.

a list whose items are either reference dicts (``{study_uid, series_uid,

sop_class_uid, sop_instance_uid}) or DICOM paths /Dataset`` objects (their identifiers are extracted automatically — the common "flag these images" case).

title: the Key Object Document Title — a {value, scheme, meaning} dict or a (value, scheme, meaning) tuple; defaults to (113000, DCM, "Of Interest"). patient_ / study_: identity for the KOS; when omitted they are inherited from the first path/Dataset reference (the study the KOS is filed under). output: write the KOS there and return None; if omitted, return Part-10 bytes.

read_ko

read_ko(path)

Read a Key Object Selection document -> {patient_name, patient_id, study_uid, series_uid, title, references: [{sop_class_uid, sop_instance_uid}, …]} (the IMAGE content items), or None when path is not a KOS.

KeyObjectSelectionDocument

KeyObjectSelectionDocument(
    evidence,
    content,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer=None,
    institution_name=None,
    institutional_department_name=None,
    requested_procedures=None,
    transfer_syntax_uid=None,
    **_kwargs
)

Constructor — returns a pydcm Dataset over the native write_ko. content.referenced_objects are the flagged key objects, content.document_title the KOS title code.

Presentation State (GSPS)

pydcm.pr

pydcm — DICOM Grayscale Softcopy Presentation State authoring + reading (pydcm.pr).

The pr capability (GSPS): author a presentation state that records how to display referenced images — window/level, Presentation LUT shape, rotate/flip, displayed area, and graphic/text annotations on named layers — over the native presentation-state engine. Reading reuses the existing PS content reader (pydcm.content) — a native authoring capability.

write_pr

write_pr(
    references,
    *,
    kind="GSPS",
    patient_name="",
    patient_id="",
    study_uid="",
    study_date="",
    sop_instance_uid="",
    series_instance_uid="",
    content_label="PS",
    content_description="",
    content_creator="",
    window=None,
    voi_luts=None,
    presentation_lut_shape="IDENTITY",
    rotation=0,
    h_flip=False,
    displayed_areas=None,
    graphic_layers=None,
    graphic_annotations=None,
    palette=None,
    icc_profile=None,
    color_space="",
    mask=None,
    blending=None,
    blending_display=None,
    output=None
)

Author a Softcopy Presentation State for references.

"GSPS" (Grayscale, default), "COLOR" (Color SC PS, for RGB images — adds an

ICC profile, drops the grayscale VOI/Presentation-LUT pipeline), "PSEUDO_COLOR" (Pseudo-Color SC PS, for grayscale images — adds a Palette Color LUT mapping stored values to RGB), "XAXRF" (XA/XRF Grayscale SC PS, the grayscale pipeline + Mask Subtraction), or "ADVANCED_BLENDING" (Advanced Blending SC PS, 11.8 — blend N pseudo-color/color inputs into true color).

mask: XA/XRF Mask Subtraction — {operation:"AVG_SUB"|"TID"|"REV_TID", mask_frames?:[...], applicable_range?:[start,end], sub_pixel_shift?:[row,col], tid_offset?}. blending: ADVANCED_BLENDING inputs — [{input_number, study_uid, series_uid, references:[{sop_class_uid, sop_instance_uid}], palette:{red,green,blue, first_mapped?}}] (each input is pseudo-colored via its palette). blending_display: how the inputs combine — [{mode:"EQUAL"|"FOREGROUND", inputs:[input_number,...], relative_opacity?}]. references: a list of reference dicts ({series_uid, sop_class_uid, sop_instance_uid, frame_numbers?}) or DICOM paths / Dataset objects (identifiers extracted). window: convenience for one Softcopy VOI LUT — (center, width) or a dict {window_center, window_width, function?, explanation?}. Use voi_luts for several. function: "LINEAR" (default) / "LINEAR_EXACT" / "SIGMOID". presentation_lut_shape: "IDENTITY" (default) or "INVERSE" (GSPS only). rotation / h_flip: spatial transform (0/90/180/270; flip horizontally). displayed_areas: list of {tlhc:[x,y], brhc:[x,y], size_mode?, magnification?, pixel_spacing?:[x,y]}. If omitted, a SCALE-TO-FIT area covering the first path/Dataset reference's full extent is added automatically. graphic_layers: [{name, order?, description?, cielab?:[L,a,b]}]. graphic_annotations: [{layer, texts?:[...], graphics?:[...]}]. palette: PSEUDO_COLOR Palette Color LUT — {red:[...], green:[...], blue:[...], first_mapped?}; each channel an equal-length list of 16-bit values (the entry count and 16-bit depth are taken from the data — there is nothing else to set). icc_profile: COLOR ICC profile bytes (0028,2000); color_space: defined term (0028,2002), e.g. "SRGB". output: write the PS there and return None; if omitted, return Part-10 bytes.

read_pr

read_pr(path)

Read a Presentation State's semantic content (referenced images, presentation LUT shape, displayed areas, graphic layers, annotations, …) as a dict, or None when path is not a presentation state. Reuses the shared PS content reader.

GrayscaleSoftcopyPresentationState

GrayscaleSoftcopyPresentationState(
    referenced_images,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name,
    software_versions,
    device_serial_number,
    content_label,
    **kwargs
)

Constructor (GSPS).

ColorSoftcopyPresentationState

ColorSoftcopyPresentationState(
    referenced_images,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name,
    software_versions,
    device_serial_number,
    content_label,
    **kwargs
)

Constructor (Color SC PS).

PseudoColorSoftcopyPresentationState

PseudoColorSoftcopyPresentationState(
    referenced_images,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name,
    software_versions,
    device_serial_number,
    content_label,
    **kwargs
)

Constructor.

Bulk annotations (microscopy)

pydcm.ann

pydcm — Microscopy Bulk Simple Annotations reading + authoring (pydcm.ann).

The ann capability: read AND write a Microscopy Bulk Simple Annotations object (SOP 1.2.840.10008.5.1.4.1.1.91.1) — the compact format for huge numbers of whole-slide annotations (cells / nuclei / regions). Both directions live in the native engine (read + build over the shared Part-10 emit/parse primitives); these are the thin marshalling wrappers.

Measurements

Measured quantity over a group.

AnnotationGroup

Annotation group.

read_ann

read_ann(path)

Read a Microscopy Bulk Simple Annotations file.

Returns a dict {coordinate_type, groups:[...]} or None if path is not a Bulk Annotations object. Each group carries its identity (number/uid/label/ generation_type), coded property_category / property_type, graphic_type, num_annotations, and annotations: a list of (n_points, dim) float64 arrays decoded from the bulk coordinates (dim is 2 for a "2D" coordinate type, 3 for "3D"). Each group's measurements is a list of {name, unit, values, annotation_index}values a float64 array (one per annotation, or per annotation_index when sparse).

write_ann

write_ann(
    source,
    groups,
    *,
    coordinate_type="2D",
    series_instance_uid=None,
    series_number=1,
    sop_instance_uid=None,
    instance_number=1,
    manufacturer="pydcm",
    manufacturer_model_name=None,
    software_versions=None,
    device_serial_number=None,
    output=None
)

Author a Microscopy Bulk Simple Annotations object (native annotation engine).

a source-image path / Dataset (or a list) — identity, Frame of Reference and

referenced-image links are taken from it.

groups: list of dicts {number, label, generation_type, property_category, property_type, graphic_type, annotations, measurements?} where annotations is a list of (n_points, dim) arrays and the codes are (value, scheme, meaning) tuples or :class:~pydcm.sr.Code.

MicroscopyBulkSimpleAnnotations

MicroscopyBulkSimpleAnnotations(
    source_images,
    annotation_coordinate_type,
    annotation_groups,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name=None,
    software_versions=None,
    device_serial_number=None,
    **_kwargs
)

Constructor — returns a pydcm Dataset built over the native write_ann.

Secondary Capture

pydcm.sc

Secondary Capture images (pydcm.sc) — write an ndarray as an SC DICOM object.

Functional API write_sc plus an SCImage constructor (returns a :class:pydcm.Dataset built over the native set_pixel_data).

write_sc

write_sc(
    pixel_array,
    photometric_interpretation="MONOCHROME2",
    *,
    bits_stored=None,
    study_instance_uid=None,
    series_instance_uid=None,
    sop_instance_uid=None,
    series_number=1,
    instance_number=1,
    manufacturer="pydcm",
    patient_id="",
    patient_name="",
    patient_birth_date="",
    patient_sex="",
    accession_number="",
    study_id="",
    study_date="",
    study_time="",
    referring_physician_name="",
    conversion_type="WSD",
    pixel_spacing=None,
    output=None
)

Author a Secondary Capture image from pixel_array.

pixel_array is uint8/uint16 shaped (rows, cols), (frames, rows, cols), (rows, cols, 3) or (frames, rows, cols, 3). Returns a :class:Dataset (or writes to output and returns None).

SCImage

SCImage(
    pixel_array,
    photometric_interpretation,
    bits_allocated,
    coordinate_system,
    study_instance_uid,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    *,
    patient_id=None,
    patient_name=None,
    patient_birth_date=None,
    patient_sex=None,
    accession_number=None,
    study_id=None,
    study_date=None,
    study_time=None,
    referring_physician_name=None,
    pixel_spacing=None,
    **_kwargs
) -> Dataset

SCImage constructor — returns a :class:Dataset.

pydcm builds the object over the native set_pixel_data (a thin Dataset, not a bespoke class); coordinate_system and other extra kwargs are accepted for source compatibility.

Parametric Map classes

pydcm.pm

Parametric Maps (pydcm.pm) — class API over the native write_paramap writer. Re-exports the functional write_paramap / read_paramap.

RealWorldValueMapping

Real-world value mapping.

write_paramap

write_paramap(
    reference,
    values,
    *,
    units=None,
    quantity=None,
    slope=None,
    intercept=None,
    label=None,
    explanation=None,
    dtype=None,
    output=None,
    sop_instance_uid="",
    dimension_organization_uid="",
    content_date="",
    content_time=""
)

Author a DICOM Parametric Map from a real-valued array.

a source-image path, or the list of source-series instance paths —

geometry, demographics and Frame of Reference are taken from it (one slice per array plane, ordered by position).

values: a float array (H, W) or (slices, H, W) of real-world values (one plane per reference slice). units: the measurement units — (code, scheme, meaning) (UCUM by default), (code, meaning), a plain meaning string, or a dict. E.g. ("um2/s", "UCUM", "um2/s"). quantity: the measured quantity code (value, scheme, meaning) (DCM by default), e.g. ("113041", "DCM", "Apparent Diffusion Coefficient"). dtype: the stored pixel type. None (default) → 32-bit float (FloatingPointImagePixel; the values are stored verbatim). "uint16" / "int16" / "uint8" / "int8" → integer pixels quantized through the Real World Value Mapping (stored = round((value - intercept) / slope)), so a reader recovers value = stored * slope + intercept. slope / intercept: Real World Value Mapping slope / intercept. For float storage the default is identity (1 / 0 — values are already real-world). For an integer dtype left unset, they are auto-computed to span the value range across the integer range (lossy only by the quantization step); pass them to control the scaling explicitly. output: write the map there and return None; if omitted, return Part-10 bytes.

sop_instance_uid / content_date (YYYYMMDD) / content_time (HHMMSS) / dimension_organization_uid: this object's own identity. Left empty, the SOP Instance UID is derived deterministically from the study, so two built for one study carry the same one — right for a single self-contained export, a DICOM global-uniqueness violation for a producer that mints many.

read_paramap

read_paramap(path)

Read a DICOM Parametric Map to (values, meta).

a float32 array (frames, rows, cols) of real-world values. Float /

double-float pixel data is returned directly; an integer-stored map has its Real World Value Mapping (slope/intercept) applied.

meta: the geometry sidecar (as :func:pydcm.decode) plus is_parametric_map and, when present, real_world_value_mapping = {slope, intercept, units, label, first_value_mapped, last_value_mapped, has_lut}.

ParametricMap

ParametricMap(
    source_images,
    pixel_array,
    series_instance_uid,
    series_number,
    sop_instance_uid,
    instance_number,
    manufacturer,
    manufacturer_model_name,
    software_versions,
    device_serial_number,
    contains_recognizable_visual_features,
    real_world_value_mappings,
    window_center,
    window_width,
    *,
    content_description=None,
    content_label=None,
    content_creator_name=None,
    transfer_syntax_uid=None,
    **_kwargs
)

Constructor — returns a pydcm Dataset.

Built over the native write_paramap: source_images supply geometry/demographics, pixel_array the real-valued (or stored) planes, and the first real_world_value_mappings entry the units / quantity / slope / intercept. extra kwargs are accepted for source compatibility.

Legacy Converted Enhanced

pydcm.legacy_converted

pydcm — Legacy Converted Enhanced CT/MR/PET authoring (pydcm.legacy_converted).

The legacy capability: fold a set of classic single-frame CT/MR/PET instances (one series) into ONE enhanced multi-frame object (LegacyConvertedEnhanced{CT,MR,PET}Image) over the native legacy-conversion engine. A faithful, reversible re-encapsulation — identity is inherited from the source series, geometry / rescale / window / frame-type are mapped into the Shared / Per-Frame Functional Groups, every frame is linked back to its origin, and leftover source attributes are preserved verbatim. converter; pydcm-native.

write_legacy_converted

write_legacy_converted(
    series,
    *,
    series_instance_uid="",
    sop_instance_uid="",
    series_number=0,
    instance_number=1,
    manufacturer="",
    model_name="",
    device_serial="",
    software_versions="",
    output=None
)

Convert a classic single-frame CT/MR/PET series into one Legacy Converted Enhanced multi-frame object.

a list of DICOM file paths (or Datasets read from disk) for the

classic single-frame instances of ONE series, in any order — frames are sorted into geometric slice order. The target SOP Class (CT/MR/PET) is chosen from the shared source Modality.

series_instance_uid / sop_instance_uid: identity for the new object; minted deterministically when omitted. series_number / instance_number: new series / instance numbers. manufacturer / model_name / device_serial / software_versions: Enhanced General Equipment (Type 1). Inherited from the source when omitted, else a default. output: write the object there and return None; if omitted, return Part-10 bytes.

Encapsulated documents (PDF / CDA / STL / OBJ / MTL)

pydcm.encapdoc

pydcm — Encapsulated Documents (pydcm.encapdoc).

Wrap a PDF / CDA / STL / OBJ / MTL document into its Encapsulated Document Storage instance (PS3.3 A.45/A.85), or extract one — over the shared native encapsulation engine. The assembly, per-type module sets, detection and MIME-aware extraction all run in C++; this wrapper only shuttles bytes and copies identity from a reference dataset.

EncapsulatedDocument

An extracted Encapsulated Document.

Attributes:

Name Type Description
payload

the document bytes. Trailing OB pad NULs are stripped for pdf/text MIME types; model/* (binary STL legitimately ends in 0x00) is verbatim, possibly with the single even-length pad byte.

mime / title / sop_class_uid / sop_instance_uid

as recorded.

type

"pdf" | "cda" | "stl" | "obj" | "mtl" when the SOP class is one of the five Encapsulated …Storage classes, else None.

write_encapsulated

write_encapsulated(
    src,
    *,
    type="auto",
    output=None,
    title=None,
    mime=None,
    units=None,
    reference=None,
    **ids
)

Wrap a document into its Encapsulated Document DICOM instance.

src: a file path, or raw bytes (then type must be explicit unless content magic identifies it). type: auto (file extension, then content magic) or pdf|cda|stl|obj|mtl. title defaults to the file stem. units: 3D-model Measurement Units UCUM code (default um). reference: a DICOM file whose Patient/Study identity is copied (the document joins that study). Extra keyword ids: patient_name, patient_id, birth_date, sex, study_uid, study_date, study_time, study_id, accession, referring, series_uid, frame_of_reference_uid, charset.

Returns the Part-10 bytes, or writes output and returns its path.

read_encapsulated

read_encapsulated(path)

Extract an Encapsulated Document instance → :class:EncapsulatedDocument.

Surface segmentation meshes

pydcm.surface

pydcm — Surface Segmentation reading (pydcm.surface).

Read a Surface Segmentation object (SOP 1.2.840.10008.5.1.4.1.1.66.5) — surface meshes stored as native DICOM (points + mesh primitives) rather than an encapsulated STL blob. Every primitive type is decoded by the native engine and triangulated into one flat triangle list, so each surface comes back as a plain (points, triangles) pair ready for pydcm.mesh / rendering. This is the thin marshalling wrapper; the parse lives in the native engine.

write_surface is the inverse, over the same model: what :func:read_surface returns is close to what it takes.

read_surface

read_surface(path)

Read a Surface Segmentation file.

Returns a dict {surfaces:[...], segments:[...]} or None if path is not a Surface Segmentation object. Each surface carries its identity (number) and display hints (finite_volume / manifold / recommended_type / recommended_opacity / cielab) plus geometry:

  • points(N, 3) float64 vertex coordinates (mm, patient space)
  • normals(N, 3) float64 per-vertex normals, or None
  • triangles(M, 3) uint32 vertex indices (0-based; triangle / strip / fan / facet primitives are all expanded into this single list)
  • lines / vertices(L, 2) / (V,) uint32 indices for edge / line / vertex primitives (present only when the surface uses them)

Each segment carries number, label, algorithm_type, coded property_category / property_type, the surface-generation algorithm_family / algorithm_name / algorithm_version, and the referenced_surface_numbers linking it to its surface(s).

write_surface

write_surface(
    surfaces,
    segments,
    *,
    output=None,
    patient_name="",
    patient_id="",
    study_uid="",
    study_date="",
    series_uid="",
    frame_of_reference_uid="",
    sop_instance_uid="",
    content_date="",
    content_time=""
)

Author a Surface Segmentation file.

surfaces: list of dicts, one per mesh.

- ``points`` — ``(N, 3)`` float64 vertex coordinates in patient mm.
  Required. Written as Double Point Coordinates Data, so the
  coordinates come back at the precision they went in with; the
  32-bit form would round them to about seven digits.
- ``triangles`` — ``(M, 3)`` uint32 vertex indices, **0-based**, or
  omitted for a point cloud. The 1-based wire form is the engine's
  business. An index outside ``points`` is refused rather than
  dropped, which is what a reader would do with it.
- ``normals`` — ``(N, 3)`` float32 per-vertex normals, optional.
- ``comments``, ``processing``, ``opacity``, ``rgb``
- ``finite_volume`` / ``manifold`` — True / False / omitted. Omitted
  is UNKNOWN, which is the honest answer for an arbitrary mesh
  rather than a claim that it is neither.
- ``presentation_type`` — omitted follows the geometry: SURFACE with
  triangles, POINTS without. A cloud presented as a surface draws
  as nothing.
list of dicts, one per segment. label,

algorithm_type, algorithm_name, algorithm_version, property_category / property_type / algorithm_family as (value, scheme, meaning), and surfaces0-based indices into the surfaces list, not wire Surface Numbers.

output: path to write to. Omitted returns the bytes.

Both sequences are Type 1: a document needs at least one surface and at least one segment.

Tractography results

pydcm.tract

pydcm — DICOM Tractography Results (pydcm.tract): author and read.

  • :func:write_mktract — author a Tractography Results Storage (SOP 1.2.840.10008.5.1.4.1.1.66.6) from track sets of polylines (streamlines) in Frame-of-Reference world coordinates, via the native dcm_tract_export engine. The write counterpart of pydcm's tractography reader and the natural sink for dipy / MRtrix streamlines.

write_mktract

write_mktract(
    reference,
    track_sets,
    *,
    output=None,
    sop_instance_uid="",
    content_date="",
    content_time=""
)

Author a DICOM Tractography Results Storage from track sets.

source-series path / list of instance paths (demographics + Frame

of Reference UID), or None to mint fresh identifiers. Track point coordinates are in that Frame of Reference (patient world mm).

track_sets: a single track-set dict, or a list of them. Each::

{
  "label": str, "description": str, "algorithm_name": str,
  "anatomy": coded concept,      # default SCT 389080008 "White Matter"
  "diffusion": coded concept,    # default DCM 113231 "Single Tensor"
  "line_thickness": float, "rgb": (r, g, b),
  "tracks": [ (n_i, 3) array of xyz, ... ],  # streamlines, world mm
  # optional per-track measurements + statistics (e.g. FA / ADC):
  "measurements": [
    {"concept": code, "units": code,         # what is measured + units
     "values": [ arr_track0, arr_track1, … ]}  # one value array per track
  ],
  "track_statistics": [
    {"concept": code, "modifier": code, "units": code,
     "values": arr}                          # one scalar per track
  ],
  "set_statistics": [
    {"concept": code, "modifier": code, "units": code, "value": float}
  ],
}

Coded concepts accept ``(value, scheme, meaning)``, ``(value, meaning)``,
a plain meaning string, a dict, or ``None``. A measurement's ``concept``
is required; ``units`` defaults to unitless; ``modifier`` is optional.

output: write the file there and return None; if omitted, return the Part-10 bytes.

sop_instance_uid / content_date (YYYYMMDD) / content_time (HHMMSS): this object's own identity. Left empty, the SOP Instance UID is derived deterministically from the study, so two built for one study carry the same one — right for a single self-contained export, a DICOM global-uniqueness violation for a producer that mints many.

read_tract

read_tract(path)

Read a Tractography Results (66.6) file — the inverse of write_mktract.

Returns {"track_sets": [...]} or None when path is not a Tractography Results object. Each track set carries its identity (number, label, description), the coded anatomy, the diffusion_model and algorithm_name, display hints (rgb / cielab / line_thickness) and:

  • tracks — list of {points: (N, 3) float32, rgb?, point_colors?}. Coordinates stay float32 because that is the width the standard gives them (0066,0016 OF) and a streamline goes to a vertex buffer.
  • measurements — a quantity sampled ALONG the tracks (FA, ADC), one entry per track under tracks. indices is present only when the values do not apply to every point in order; the standard pairs them one to one, but this reports what the file says, so zip them only after checking the lengths agree.
  • track_statistics — one float64 per track, e.g. mean FA per track.
  • set_statistics — one value for the whole set.

Statistics come back as float64 and coordinates as float32 on purpose: a statistic is a measurement, a coordinate is geometry the viewer draws.

Semantic content (auto-detect by SOP class)

pydcm.content

pydcm — structured-object content reader (pydcm.content).

The interpreted (semantic) view of a derived DICOM object — Segmentation, RT Structure Set, RT Plan (photon + ion), RT Dose, Presentation State, Waveform, Ophthalmic Visual Field, or Structured Report (the full content tree) — over the shared native content engine. One unified reader that auto-detects the SOP class, organized by operation rather than object; the interpreted counterpart to the raw element model.

content

content(
    path, contours=False, control_points=False, meshes=False
)

Semantic content of a structured DICOM object — Segmentation, RT Structure Set, RT Plan, RT Dose, Presentation State, Waveform, Ophthalmic Visual Field, Surface Segmentation, or Structured Report (the full content tree) — as a dict (coded concepts resolved), or None if path is not one of those.

Raises RuntimeError when path is not decodable DICOM at all.

contours: RT Structure Set only — include each contour's xyz point list. control_points: RT Plan only — include every control point (angles, meterset, leaf/jaw positions) instead of the first-CP summary. meshes: Surface Segmentation only — include each surface's full points / normals / triangles arrays instead of just counts (use pydcm.read_surface for arrays as NumPy).

RT dosimetry & structures

pydcm.rt

pydcm — RT dosimetry reader (pydcm.rt).

The dose-side counterpart to the RT Structure Set support: read_rtdose returns the scaled dose grid (pixel × DoseGridScaling, computed in C++ by the shared native RT engine) as a NumPy volume plus its geometry and any stored DVH curves. The semantic metadata view of RT Plan / RT Dose lives in :func:pydcm.content.

StructureSet

An RT Structure Set, read.

Attributes:

Name Type Description
label

Structure Set Label.

rois

one :class:ROI per Structure Set ROI Sequence item, with the contour and observation items that resolved to it folded in. Two contour items bound to the same ROI describe ONE merged ROI — that is what DICOM means by it — so they merge here.

unbound_contours

contour items whose ROI reference was missing, malformed, unknown or ambiguous. They are kept rather than dropped, because a structure set that lost contours silently is worse than one that says it has some it cannot place.

findings

RT-domain defects, [{code, path, message}]. These describe usability, not general DICOM conformance — for that use :func:pydcm.validate.

\*_sequence_present

absent versus present-but-empty, which a reader deciding whether the object is usable has to tell apart.

ROI

One ROI of a structure set: its identity, its semantics, its contours.

interpreted_type and generation_algorithm are verbatim Defined Terms; rgb is the recommended display colour or None.

Contour

One contour of an ROI.

Attributes:

Name Type Description
points

(n, 3) float64 patient-LPS mm, full precision. Empty when the file's Contour Data was unusable — the structure set's findings says which, and half a contour is never exposed.

geometric_type

the verbatim Defined Term ("CLOSED_PLANAR", "CLOSEDPLANAR_XOR", "POINT", ...). PS3.3 Defined Terms are extensible, so a value this build does not recognise still arrives here rather than being dropped.

image_references

[{sop_class_uid, sop_instance_uid, frame_numbers}] — the source images this contour was drawn on.

usable property

usable

The file's Contour Data was readable, so :attr:points is the contour.

False means the value was present and unusable — the structure set's findings names it — which is not the same as the contour being absent, and is why this object still exists.

DVH

A DVH computed from RTSTRUCT + RTDOSE.

The histogram and its statistics are computed by the native RT engine; the D(V) / V(D) readings below are the engine's own bin search, not a second one written in Python — so a constraint reads the same here, in the CLI and in a viewer.

Attributes:

Name Type Description
counts

differential histogram, cm³ per 1-cGy bin (float64 ndarray, trailing zeros trimmed).

cumulative

suffix-sum of counts (the cumulative DVH).

bins

bin edges in Gy (len(counts) + 1 values, 0.01 Gy wide).

bincenters, dose_axis

bin centres in Gy.

volume

structure volume in cm³ — the total ROI when the DVH was computed with calculate_full_volume, else the covered volume.

total_volume, (covered_volume, uncovered_volume)

the volume split the dose grid actually produced. A per-cent is of covered_volume, because that is what the histogram is of.

min/max/mean (covered_volume, uncovered_volume)

dose statistics in Gy.

rx_dose

prescription in Gy, for the % dose forms (V100%). Optional — set it, or pass rx_dose= to :func:dvhcalc.

notes

dose-grid coverage notes ('' when the grid covers the structure).

relative_volume property

relative_volume

cumulative as a per-cent of the covered volume (ndarray).

relative_dose

relative_dose(rx_dose=None)

Bin centres as a per-cent of the prescription (ndarray).

dose_constraint

dose_constraint(volume, volume_units=None)

D(V): the dose that at least volume of the structure receives, in Gy.

volume_units is '%' (the default — a per-cent of the covered volume) or 'cc' / 'cm3' for an absolute volume. NaN when the structure is smaller than the volume asked about: that is a question with no answer, and 0 Gy would be a wrong one.

volume_constraint

volume_constraint(dose, dose_units=None)

V(D): the volume receiving at least dose, in cm³.

dose_units is 'Gy' (the default), 'cGy', or '%' of rx_dose.

statistic

statistic(name)

One constraint by name — 'D95', 'D2cc', 'V20Gy', 'V100%', 'Dmax', 'Dmin', 'Dmean'.

The grammar is the native engine's, the same one the DVH constraint grammar accepts, so a name means one thing across both surfaces.

describe

describe()

Print the constraints a plan review reads first. Returns the dict.

plot

plot(ax=None, **kw)

Plot the cumulative DVH. Needs matplotlib; returns the axes.

compare

compare(
    other, stats=("D100", "D98", "D95", "D50", "D2", "D2cc")
)

{stat: (self, other, other - self)} in Gy, for two plans or two rasterisation settings of the same structure.

DVHValue

One dosimetric readout: a number plus the units it is in.

Compares and formats as its number, so dvh.D95 > 60 and f"{dvh.D95:.1f}" read the way the constraint is written down, while repr keeps the units visible.

DoseGrid

A scaled RT Dose grid.

Attributes:

Name Type Description
dose

ndarray[depth, rows, cols] float32 — pixel × DoseGridScaling (Gy when dose_units == "GY").

dose_grid_scaling

the finite positive scale from the file that was applied. A missing or invalid required scale is rejected.

max_dose

grid maximum, computed in double precision — may differ from dose.max() in the last float32 ulp.

affine

4×4 voxel→world (LPS) matrix, column-major flat list — same convention as :func:pydcm.load_series.

frame_of_reference_uid, (origin_lps, column_step_lps, row_step_lps, frame_offsets_mm)

authoritative patient-LPS geometry in double precision. Frame offsets are canonical relative offsets even when the source used the absolute-z GridFrameOffsetVector option.

spacing

(z, y, x) mm compatibility view. For a one-frame or nonuniform grid, z is zero and the affine has no invented frame step.

grid_frame_offsets

float32 compatibility projection of frame_offsets_mm.

uniform_offsets, has_uniform_affine

geometry capability flags.

dvhs

list of stored DVH curves (dicts with bin_widths/volumes and full ROI contribution relationships). Optional statistics are absent when not recorded.

stored_dvh_error

optional diagnostic for a malformed stored DVH sequence; the independently usable dose grid remains available.

RTPlan

An RT Plan's prescription and fractionation.

Attributes:

Name Type Description
prescription_dose

the largest Target Prescription Dose among TARGET references, in Gy — or None. Largest rather than first because a plan with a boost carries more than one target and the reader means the plan's prescription. None is never substituted with the dose grid's maximum: that answers a different question.

fractions_planned

(300A,0078) of the first Fraction Group, or None.

fraction_groups

how many groups the plan carries. More than one is not summed — a reader that did would be inventing a prescription.

dose_references

the :class:DoseReference items.

incomplete

an item could not be recorded, so the plan is short by omission rather than by content.

dose_per_fraction property

dose_per_fraction

Prescription ÷ fractions, in Gy — None unless the plan states both and carries exactly one Fraction Group.

DoseReference

One Item of the plan's Dose Reference Sequence (300A,0010).

A dose that is None was absent; one that is absent and target_prescription_dose_invalid was present and unreadable. Those are different facts and neither of them is zero.

read_rtdose

read_rtdose(path)

Read an RT Dose file (SOP Class …481.2) into a :class:DoseGrid.

All computation (scaling in double precision, geometry, DVH decode) runs in the native RT engine; this wrapper only shapes the result.

dose_at

dose_at(path, points)

Dose at patient coordinates, in Gy.

The quantitative counterpart to :func:dvhcalc: what a prescription point or a measured location received, rather than what a structure received.

path: an RT Dose file. points: (N, 3) patient coordinates in the dose object's own Frame of Reference. Points in another frame are transformed first — with :meth:pydcm.registration.Registration.transform, say. Nothing here treats a mismatched frame as identity, because doing so would sample the right grid at the wrong place and report a plausible number.

Returns (values, inside)(N,) float64 Gy and (N,) bool. A point the grid does not cover has inside=False and a value of 0; a point where the dose really is zero has inside=True. They are the same number and mean opposite things, which is why the flag is separate rather than a sentinel dose.

Isodose lines are not here. They are a rendering question and live in the viewer; this is the readout.

write_rtdose

write_rtdose(
    dose,
    *,
    affine=None,
    origin=None,
    orientation=(1, 0, 0, 0, 1, 0),
    spacing=None,
    grid_frame_offsets=None,
    dose_units="GY",
    dose_type="PHYSICAL",
    dose_summation_type="PLAN",
    ref_plan_uid=None,
    reference=None,
    patient_name=None,
    patient_id=None,
    study_uid=None,
    study_date=None,
    series_uid=None,
    frame_of_reference_uid=None,
    scaling=None,
    bits=32,
    output=None,
    sop_instance_uid="",
    content_date="",
    content_time=""
)

Author an RT Dose file (SOP Class …481.2) from a dose grid.

The write side of :func:read_rtdose — the export (quantisation to unsigned integers with a self-consistent DoseGridScaling, Part-10 emit) runs in the native engine. Typical AI-workflow use: a predicted or accumulated grid → a file a TPS/viewer imports.

Geometry: pass affine (column-major 4×4 voxel→world, the :func:read_rtdose/:func:pydcm.load_series convention) OR origin+orientation+spacing (row, col) mm (+ optional grid_frame_offsets, default derived from the affine's frame step / uniform z spacing).

reference: a DICOM file (the RT Plan, planning CT, …) whose Patient/Study/FrameOfReference identity is copied; when it IS an RT Plan, ref_plan_uid defaults to its SOP Instance UID.

scaling: explicit DoseGridScaling; default = max dose / integer max (full dynamic range of bits, 32 or 16). Returns the Part-10 bytes, or writes output and returns its path.

Note: PS3.3 requires a ReferencedRTPlanSequence (Type 1C) for the PLAN/BEAM/… summation types — pass ref_plan_uid or an RT Plan as reference for fully conformant output; research/AI grids without a plan are written as-is.

sop_instance_uid / content_date (YYYYMMDD) / content_time (HHMMSS): this object's own identity. Left empty, the SOP Instance UID is derived deterministically from the series, so two built for one series carry the same one — right for a single self-contained export, a DICOM global-uniqueness violation for a producer that mints many.

write_rtstruct

write_rtstruct(
    reference,
    rois,
    *,
    label="",
    output=None,
    sop_instance_uid="",
    series_instance_uid=""
)

Author a DICOM RT Structure Set from ROI contours over a reference series.

the source-series instance paths (a path, a directory, or a list) — geometry,

demographics and per-contour source-image references are taken from it.

rois: a list of ROI dicts, each with name, optional rgb (r, g, b), optional interpreted_type (RT ROI Interpreted Type code: 0=UNKNOWN, 4=ORGAN, …), and contours: a list of (n, 3) float64 arrays of LPS patient-mm points — one planar contour each, matched to its source slice by position along the slice normal. label: Structure Set Label. output: write there and return None, else return Part-10 bytes.

sop_instance_uid / series_instance_uid: state this document's own identity. Left empty, the native engine mints them with a deterministic generator, so two documents authored in one process — or, for the study-derived writers, two authored for one study — carry the SAME UIDs. That is fine for a single self-contained export and is a DICOM global-uniqueness violation for a producer that mints many; supply :func:pydcm.generate_uid values there.

read_rtstruct

read_rtstruct(path, *, coordinates=True)

Read an RT Structure Set (SOP Class …481.3) into a :class:StructureSet.

The read side of :func:write_rtstruct. Contour points come back as (n, 3) float64 patient-LPS millimetres at full precision.

coordinates=False validates Contour Data without retaining it — for inspecting a large structure set's ROI table and findings without paying for the coordinate vectors.

RT-domain defects land in :attr:StructureSet.findings instead of raising: a structure set with one unusable contour is still worth reading, and which part failed is the useful answer. A structural decode failure does raise.

roi_mask

roi_mask(rtstruct, reference, roi, *, transform=None)

One ROI of an RT Structure Set as a boolean volume on reference's grid.

the image series the mask should land on — a directory, a file,

or a list of instance paths. Its slices are the target planes, in the order :func:write_seg and the other authoring writers use, so a mask and a Segmentation authored from the same series share one grid.

roi: the ROI Number — the identifier :func:dvhcalc also takes. transform: the (4, 4) RTSTRUCT-frame → reference-frame affine, e.g. pydcm.read_registration(reg).transform(struct_for, series_for). Required when the two Frames of Reference differ; omitting it there is refused rather than treated as identity, because identity where a registration was meant puts the mask in the wrong place with nothing to show that it did.

Returns (mask, meta). mask is (planes, rows, cols) bool. meta carries roi_name, set_voxels and findings.

One ROI per call, deliberately. Twenty ROIs over a 512×512×300 series is 1.5 GB of masks returned together, and a caller who wants a label volume has to decide what happens where two ROIs claim one voxel — a decision only that caller can make.

A pixel is in when its centre is, and a sample exactly on a boundary is in. There is no sampling knob: fractional coverage — how much of a pixel a contour covers — is a different question that :func:dvhcalc answers quantitatively, and turning it back into a yes/no would need a threshold that nothing states.

meta["findings"] is what the mask could not simply state:

contour_on_no_plane the contour is not within tolerance of any slice — ordinary when the ROI reaches past the series, and reported rather than silently dropped. contour_on_several_planes several slices are within tolerance of one contour, so it is drawn on the nearest; drawing it on each would be volume counted twice. same_plane_nested two CLOSED_PLANAR contours on one slice, one wholly inside the other. They are unioned, because the standard says nothing about composing CLOSED_PLANAR contours — a producer writing a hole that way predates CLOSEDPLANAR_XOR, which is the encoding that states one. The finding is how you learn it happened. same_plane_partial_overlap two contours on one slice cross without either containing the other; neither a union nor a hole is stated for that.

A structure set that mixes CLOSEDPLANAR_XOR with other closed types raises, naming the minority contours: the XOR rule is stated over the complete ROI, so no composition is defined for such a document.

dvhcalc

dvhcalc(
    structure,
    dose,
    roi,
    limit=None,
    calculate_full_volume=True,
    thickness=None,
    samples_per_axis=None,
    require_full_dose_coverage=False,
    rx_dose=None,
)

Compute the DVH of roi from an RT Structure Set + RT Dose file pair.

The rasterisation, patient-space dose interpolation, histogram and statistics all run in the native RT engine. Results follow the standard cumulative-DVH definition. limit truncates the returned histogram in cGy; full-volume integration and dose statistics remain untruncated.

samples_per_axis (1..32) sets the fractional-coverage sampling density; the default is the engine's. require_full_dose_coverage refuses a structure the dose grid does not fully reach rather than reporting it with a coverage note.

rx_dose is the prescription in Gy, or an :class:RTPlan / a path to the RT Plan to take it from — it is what the % dose forms (dvh.V100%) are relative to.

read_rtplan

read_rtplan(path)

Read an RT Plan (SOP Class …481.5) into an :class:RTPlan.

Beam geometry is deliberately not read — this is the prescription, which is the fact a dose display cannot get anywhere else.

Tumour response criteria

pydcm.recist

pydcm — tumour response criteria (pydcm.recist).

Six published criteria over the same shape of input: a list of lesion measurements at baseline, the same lesions at the current timepoint, and the nadir — the smallest sum seen so far, which is what progression is measured from, not the baseline.

>>> import pydcm.recist as recist
>>> baseline = [{"longest_mm": 32.0}, {"longest_mm": 18.0}]
>>> current  = [{"longest_mm": 20.0}, {"longest_mm": 12.0}]
>>> recist.evaluate(baseline, current)["response"]
'PR'

Which criterion applies is a clinical decision, not a technical one, so nothing here picks for you. What each one sums differs, and the field names say so:

=============== ============================== ========================== Criterion Field Summed as =============== ============================== ========================== RECIST 1.1 longest_mm sum of longest diameters iRECIST longest_mm as RECIST 1.1, + confirmation mRECIST viable_diam_mm arterially enhancing only Cheson/Lugano longest_mm, perpendicular_mm sum of the products RANO longest_mm, perpendicular_mm sum of the products PCWG3 new bone lesion counts, PSA the 2+2 rule =============== ============================== ==========================

Lymph nodes are not lesions for the complete-response check — a node is normal below 10 mm short axis rather than absent — so mark them is_lymph_node=True and give short_axis_mm.

Every threshold and tie-break lives in the native engine, validated against Eisenhauer (EJC 2009), Seymour (Lancet Oncol 2017), Lencioni & Llovet (Semin Liver Dis 2010), Cheson (JCO 2014), Wen (JCO 2010) and Scher (JCO 2016). The web runtime calls the same functions, so the two products cannot come to disagree about a category.

Measurement helpers turn a segmentation into the diameters the criteria want: :func:feret measures one mask, :func:feret_volume finds the slice a lesion is longest on, and :func:volume sums per-slice pixel counts. They take the masks read_seg and roi_mask already produce, so a lesion contoured once is not drawn again to be measured.

evaluate

evaluate(
    baseline,
    current,
    *,
    nadir_sld=0.0,
    new_lesion=False,
    nt_progression=False
)

RECIST 1.1 target-lesion response.

list of lesion dicts — longest_mm, optionally

short_axis_mm, is_lymph_node, status.

current: the SAME lesions in the SAME order at this timepoint. A length mismatch is an error, not something to broadcast away. nadir_sld: smallest sum of longest diameters recorded so far, in mm. Progression is measured from the nadir; leaving it 0 makes the baseline the reference, which is only right at the second timepoint. new_lesion: any new lesion at all is progression, whatever the sum did. nt_progression: unequivocal progression of a non-target lesion.

Returns {"response": "CR"|"PR"|"SD"|"PD"|"NE", "baseline_sld": float, "current_sld": float}, both sums in mm.

status is one of "measured" (the default), "too_small", "not_evaluable", "absent", "present", "unequivocal_pd".

evaluate_irecist

evaluate_irecist(
    baseline,
    current,
    *,
    nadir_sld=0.0,
    new_lesion=False,
    nt_progression=False,
    prev_response="NE",
    prev_was_iupd=False
)

iRECIST response, for immunotherapy.

iRECIST exists because a tumour can enlarge under immunotherapy before it responds, so progression is unconfirmed (iUPD) until a follow-up scan confirms it (iCPD). That makes the previous timepoint part of the input:

the RECIST 1.1 response at the previous timepoint —

"CR", "PR", "SD", "PD" or "NE". Required because this call has no access to those images to recompute it.

prev_was_iupd: whether the previous iRECIST read was iUPD, which is what turns this timepoint's progression into iCPD.

Returns {"response": "iCR"|"iPR"|"iSD"|"iUPD"|"iCPD"|"iNE", ...}.

evaluate_mrecist

evaluate_mrecist(
    baseline, current, *, nadir_sum=0.0, new_lesion=False
)

mRECIST for hepatocellular carcinoma.

Each measurement gives viable_diam_mm — the arterially enhancing diameter. That is the point of mRECIST: a treated lesion can keep its size while the viable part of it disappears, which RECIST 1.1 would read as stable disease and mRECIST reads as a complete response. Passing the whole lesion diameter here is a category error and is refused.

evaluate_cheson

evaluate_cheson(
    baseline, current, *, nadir_spd=0.0, new_lesion=False
)

Cheson/Lugano response for lymphoma.

Sums the products of perpendicular diameters (SPD), so each measurement needs both longest_mm and perpendicular_mm. Nodal disease is the usual case: mark nodes is_lymph_node=True.

evaluate_rano

evaluate_rano(
    baseline,
    current,
    *,
    nadir_spd=0.0,
    new_lesion=False,
    non_enhancing="unknown",
    on_steroids=False
)

RANO response for glioma.

"stable", "improved", "increased" or

"unknown". The non-enhancing (T2/FLAIR) component is part of the criterion, not context — a significant increase is progression even when the enhancing sum fell.

on_steroids: complete response requires the patient to be off steroids. An enhancing tumour that vanished under dexamethasone is not a CR.

evaluate_pcwg3

evaluate_pcwg3(
    *,
    new_lesions_scan1=0,
    new_lesions_scan2=None,
    prev_was_pending=False,
    psa_current=None,
    psa_nadir=None
)

PCWG3 bone and PSA progression for prostate cancer.

Bone progression follows the 2+2 rule: two or more new lesions on the first post-treatment scan, confirmed by two or more further new lesions on the next. Between the two the answer is PENDING, which is a real state and not a missing one — pass new_lesions_scan2=None while the confirmation scan has not happened.

PSA is optional; bone progression is assessable on imaging alone. When given, progression is measured against psa_nadir — the lowest value seen, not the baseline.

Returns {"bone": ...}, plus "psa" when a PSA pair was given.

feret

feret(mask, spacing)

Maximum Feret diameter of one 2-D binary mask.

2-D array. What read_seg(masks=True) and one plane of

:func:pydcm.rt.roi_mask already give you — including a FRACTIONAL segmentation's float occupancy, which is thresholded at half a voxel, the same threshold the dcmrecist CLI uses.

spacing: (row_mm, col_mm) pixel spacing — PixelSpacing order.

Returns {"longest_mm", "short_axis_mm", "longest_endpoints", "short_endpoints"}, endpoints as (x1, y1, x2, y2) in pixels, or None for an empty mask — no lesion, rather than one of length zero.

feret_volume

feret_volume(mask, spacing)

The slice a lesion is longest on, measured across a 3-D mask.

RECIST measures a lesion on the slice where it is largest, so a volumetric segmentation has to be reduced to that one slice before it becomes a diameter. This does that reduction and nothing else.

mask: 3-D array (planes, rows, cols), nonzero = lesion. spacing: (row_mm, col_mm) in-plane spacing.

Returns the :func:feret result for the winning slice, with "slice" added, or None when no slice holds any lesion.

volume

volume(
    mask=None,
    *,
    slice_pixel_counts=None,
    pixel_area_mm2=None,
    spacing=None,
    slice_spacing_mm=None
)

Lesion volume in mm³.

Give either a 3-D mask with spacing and slice_spacing_mm, or the per-slice slice_pixel_counts directly with pixel_area_mm2 — the second form is for a caller who already counted, e.g. while streaming.

pixel_to_world

pixel_to_world(origin, orientation, spacing, px, py)

One pixel coordinate to patient coordinates (mm).

A lesion recorded as a pixel on a slice cannot be found again in next month's study — the slice index and the pixel grid both move. Recorded in patient coordinates it can.

match_lesion

match_lesion(query, candidates, max_distance_mm=15.0)

Nearest prior lesion to a world coordinate.

Returns (index, distance_mm), or (None, None) when nothing lies within max_distance_mm — which is exactly what a genuinely new lesion looks like, so it is an answer rather than a failure.

nearest_slice

nearest_slice(world, slice_origins, normal)

Nearest slice to a world coordinate -> (index, signed_distance_mm).

Spatial registration

pydcm.registration

pydcm — Spatial Registration reader (pydcm.registration).

A registration object says how one Frame of Reference sits inside another: where the PET is relative to the CT, where today's MR is relative to last month's. Without one, a fusion or a contour propagation can only work between series that already share a Frame of Reference — which is not most of what a reader wants to compare.

Deformable Spatial Registration (66.3) carries a deformation grid rather than a matrix; it is a different IOD and is refused here rather than read as if it were this one.

RegistrationItem

One Item of the Registration Sequence.

Attributes:

Name Type Description
frame_of_reference_uid

the frame this item's matrix maps from.

matrix

(4, 4) float64 mapping that frame into the registration object's own frame, in the M @ [x, y, z, 1] convention. None when the file carried no usable matrix.

matrix_invalid

a matrix was present and cannot be used — a Decimal String that would not convert, the wrong number of values, or a bottom row that is not [0, 0, 0, 1]. Present-and-unusable is not the same as absent, and neither is identity.

matrix_type

the producer's claim — "RIGID", "RIGID_SCALE", "AFFINE" or "UNKNOWN". All three are affine; the distinction is what the producer says it did, which a consumer may want to trust differently.

matrix_count

how many matrices were composed into :attr:matrix. The standard applies them in the order encoded, so this is their composition, not the last one seen. 1 is the ordinary case.

Registration

A Spatial Registration object (SOP Class …66.1).

Attributes:

Name Type Description
frame_of_reference_uid

the object's OWN frame — the one every item's matrix maps into. An object conventionally includes an identity item for it, and that item is kept rather than dropped, because "these two frames are the same" is an answer.

items

the :class:RegistrationItem entries.

incomplete

an item could not be recorded, so the object is short by omission rather than by content.

frames property

frames

The Frames of Reference this object registers.

transform

transform(from_uid, to_uid=None)

The transform from from_uid to to_uid, as (4, 4) float64.

With to_uid omitted this maps into the registration object's own frame. With both given it is the transform a fusion actually asks for — neither series is necessarily the frame the object was authored in.

Identical frames answer with the identity, so a caller need not decide beforehand whether a registration is involved.

Returns None when this object does not state the transform: an unregistered frame, an unusable matrix, or a composition that would need a singular inverse. Never a silent identity — applying identity where a registration was intended puts the overlay in the wrong place with nothing to show that it did.

read_registration

read_registration(path)

Read a Spatial Registration (SOP Class …66.1) into a :class:Registration.

Raises for a Deformable Spatial Registration (66.3), which carries a deformation grid rather than a matrix.

write_registration

write_registration(
    items,
    frame_of_reference_uid,
    *,
    output=None,
    patient_name="",
    patient_id="",
    study_uid="",
    study_date="",
    series_uid="",
    label="",
    description="",
    creator="",
    sop_instance_uid="",
    content_date="",
    content_time=""
)

Author a Spatial Registration (66.1) — the inverse of :func:read_registration.

items: list of dicts, one per frame this object relates.

- ``frame_of_reference_uid`` — the frame this matrix maps **from**.
- ``matrix`` — ``(4, 4)`` float64, the ``M @ [x, y, z, 1]``
  convention :func:`read_registration` returns. It maps that frame
  **into** ``frame_of_reference_uid`` below; that direction is what
  the whole object means and reversing it is not detectable later.
- ``matrix_type`` — ``"RIGID"`` (default), ``"RIGID_SCALE"`` or
  ``"AFFINE"``. All three are affine; this is the producer's claim
  about its own pipeline and nothing checks it against the matrix.
this object's own frame — the one every

matrix maps into, and the frame of the fixed image in a fusion. An identity item for it is written automatically unless items already contains one; without that item the file parses and then refuses every query, because resolving A to B composes inverse(item_B) @ item_A and needs an item for B.

output: path to write to. Omitted returns the bytes.

A matrix whose bottom row is not [0, 0, 0, 1], one that is singular, a non-finite element, or two items naming one frame are all refused — each would produce a file that parses and then either refuses the question it exists for or answers it wrongly.

Rendering & overlays

Render a single frame to 8-bit RGB and burn presentation markup (GSPS graphics, SR SCOORD regions, SEG masks, RTSTRUCT contours) onto it — the frame an agent "sees".

pydcm.overlay

pydcm — render a DICOM frame and burn structured markup onto it (pydcm.overlay).

The headless, agent-vision rendering path: render one frame to an 8-bit display image (window/level), then burn a presentation state's graphic annotations (GSPS) and/or a structured report's SCOORD measurement regions (SR) onto the pixels — so a vision model SEES the radiologist's markup. Only markup that references the rendered image is drawn. All compute is native; this is a thin wrapper.

render_overlay

render_overlay(
    image,
    overlays=(),
    *,
    frame=1,
    window=None,
    max_dim=0,
    with_overlays=False
)

Render image (one frame) to 8-bit RGB and burn overlays' markup onto it.

image: the DICOM image — path | bytes | Dataset. overlays: a GSPS/SR object or an iterable of them (path | bytes | Dataset). Each is auto-detected; only markup referencing image's SOP Instance is drawn (GSPS annotations in their layer colour, SR SCOORDs in green). frame: 1-based frame number (default 1). window: (center, width) window/level, or None for the per-frame default. max_dim: aspect-preserving downscale so the largest side fits (0 = native). Markup is burned at native resolution and then downscaled with coverage preservation so thin contours remain visible. with_overlays: also return a structured description of the markup. The shapes (and the image's own 60xx overlay planes) are projected to pixel space with UTF-8 text / measurement values / labels — the "image + overlay JSON" form, so values/text need no in-pixel font.

The result is a numpy.ndarray [H, W, 3] uint8 (RGB) — or, with with_overlays=True, a tuple (ndarray, overlays) where overlays is a list of dicts (source, kind, points, color, and optional text / label / value / unit / number / filled).

Whole-slide imaging

pydcm.wsi

Whole-slide imaging (DICOM VL Whole Slide Microscopy) reader — an surface over pydcm's native pyramid engine.

from pydcm.wsi import open_slide
s = open_slide("/path/to/slide_dir")           # a dir of the slide's .dcm levels
s.level_count, s.level_dimensions, s.level_downsamples
rgba = s.read_region((x, y), level, (w, h))     # (x,y) in LEVEL-0 coords → numpy

read_region location is in level-0 reference coords, size in the requested level's coords; default returns RGBA, edge/sparse-missing pixels are transparent). rgba=False returns RGB. associated_images exposes DICOM label / overview / thumbnail / localizer images as a lazy mapping. The decode / tile assembly / pyramid all run in the shared native core — analysis (tiling for ML, stain normalisation) stays interop: feed the returned NumPy arrays to your pipeline.

Slide

A whole-slide pyramid (subset).

level_dimensions property

level_dimensions

((cols, rows), ...) per level, largest (level 0) first.

level_tile_dimensions property

level_tile_dimensions

((tile_cols, tile_rows), ...) per level.

level_tile_counts property

level_tile_counts

((tiles_x, tiles_y), ...) per level.

level_frame_counts property

level_frame_counts

Stored frame count per level in source instance frame order.

properties property

properties: dict[str, str]

and DICOM-derived slide metadata as string properties.

associated_image_names property

associated_image_names

Names of associated non-pyramid images, e.g. label or overview.

associated_images property

associated_images

Lazy mapping: name -> RGBA uint8 NumPy image.

icc_profile property

icc_profile: bytes | None

Raw DICOM ICC Profile bytes for the base pyramid level, if present.

icc_transform_available property

icc_transform_available: bool

Whether this build can apply WSI ICC profiles to sRGB via LCMS2.

dimensions property

dimensions

Level-0 (cols, rows).

tile_cache_capacity property

tile_cache_capacity: int

Decoded tile cache capacity in bytes. 0 disables retaining decoded tiles.

level_descriptor

level_descriptor(level: int) -> dict

Viewer-oriented metadata for one level without decoding any tile.

viewer_level

viewer_level(level: int, *, include_ranges=False) -> dict

Return the level descriptor plus source paths and optional range table.

The level entry a tiled WSI viewer consumes directly: metadata is cheap, and the dense encoded tile range grid can be handed to a range loader / tile scheduler without decoding pixels in this call.

viewer_levels

viewer_levels(*, include_ranges=False) -> tuple[dict, ...]

Return viewer-oriented descriptors for all pyramid levels.

level_concatenation

level_concatenation(level: int) -> dict | None

DICOM Concatenation metadata for a pyramid level, if present.

associated_image_dimensions

associated_image_dimensions(name: str)

(cols, rows) for an associated image, or (0, 0) if absent.

read_associated_image

read_associated_image(
    name: str, *, rgba=True, apply_icc_profile=False
)

Return an associated image as (rows, cols, 4) RGBA or (rows, cols, 3) RGB.

level_icc_profile

level_icc_profile(level: int) -> bytes | None

Raw DICOM ICC Profile bytes for one pyramid level, if present.

associated_image_icc_profile

associated_image_icc_profile(name: str) -> bytes | None

Raw DICOM ICC Profile bytes for an associated image, if present.

get_best_level_for_downsample

get_best_level_for_downsample(downsample: float) -> int

Return the best pyramid level for downsample.

set_tile_cache_capacity

set_tile_cache_capacity(capacity: int) -> None

Set decoded tile cache capacity in bytes for this slide.

read_region

read_region(location, level, size, *, rgba=True)

location = (x, y) top-left in LEVEL-0 coords; size = (w, h) in level coords. Returns a (h, w, 4) RGBA (or (h, w, 3) RGB) uint8 NumPy array.

read_tile

read_tile(level, tile, *, rgba=True, fill_missing=False)

Return one stored tile by zero-based tile = (tile_x, tile_y).

Sparse-missing tiles return an empty array by default. With fill_missing=True, an in-grid sparse-missing tile returns an all-zero tile instead (transparent in RGBA).

read_tiles

read_tiles(level, tiles, *, rgba=True, fill_missing=False)

Return multiple stored tiles in input order.

This is equivalent to repeated read_tile() calls, but crosses the Python/native boundary once for the whole batch.

read_tile_stack

read_tile_stack(
    level, tiles, *, rgba=True, fill_missing=False
)

Return multiple full-size tiles as (n, tile_h, tile_w, channels).

Unlike read_tiles(), this is strict: every requested tile must produce a full tile. Sparse-missing tiles require fill_missing=True.

read_tile_grid

read_tile_grid(
    level,
    origin,
    shape,
    *,
    rgba=True,
    require_existing=False
)

Read a rectangular tile grid as (tile_rows, tile_cols, tile_h, tile_w, channels).

This uses one native read_region() call and returns a NumPy view over the region buffer. Sparse-missing tiles are transparent by default; set require_existing=True to reject grids containing missing sparse tiles.

level_frame_tile

level_frame_tile(level, frame_number, *, as_index=False)

Return (tile_x, tile_y) for a stored frame in source frame order.

frame_number is DICOM 1-based by default. Set as_index=True for Python 0-based indexing.

level_source_paths

level_source_paths(level: int) -> tuple[str, ...]

File-backed source path(s) for a level; memory-backed slides return ().

level_tile_ranges

level_tile_ranges(level: int)

Return encoded tile byte ranges for a level as uint64[n, 6].

Columns are source_index, frame_index, tile_x, tile_y, offset, length. The ranges point into the source Part-10 file and are intended for viewer-style range loading; this method does not decode pixels.

level_tile_range_grid

level_tile_range_grid(level: int)

Return dense row-major encoded tile ranges as uint64[n, 6].

The row-major index is tile_y * tile_count_x + tile_x. Sparse-missing tiles have length == 0 and frame_index == MISSING_FRAME_INDEX. Columns are source_index, frame_index, tile_x, tile_y, offset, length.

tile_range

tile_range(
    level, tile
) -> tuple[int, int, int, int, int, int] | None

Return one encoded tile range or None for an absent/out-of-grid tile.

level_frame_range

level_frame_range(
    level, frame_number, *, as_index=False
) -> tuple[int, int, int, int, int, int]

Return encoded byte range for a stored frame in source frame order.

get_stored_frame

get_stored_frame(
    frame_number,
    *,
    level=0,
    as_index=False,
    dtype=None,
    rgba=False,
    fill_missing=False,
    apply_icc_profile=None
)

stored frame access for one WSI level.

The frame number follows DICOM 1-based numbering unless as_index=True. The returned array is a full stored tile in source frame order.

get_stored_frames

get_stored_frames(
    frame_numbers=None,
    *,
    level=0,
    as_indices=False,
    dtype=None,
    rgba=False,
    fill_missing=False,
    apply_icc_profile=None
)

stored frame batch access.

frame_numbers are DICOM 1-based by default. Set as_indices=True for Python 0-based indexing. None reads all stored frames for the level.

get_frame

get_frame(
    frame_number,
    *,
    level=0,
    as_index=False,
    dtype=None,
    rgba=False,
    fill_missing=False,
    apply_real_world_transform=None,
    apply_modality_transform=None,
    apply_voi_transform=False,
    apply_palette_color_lut=None,
    apply_icc_profile=None,
    **_kwargs
)

frame access.

For WSI this currently aliases stored-frame access. Pixel-transform keyword arguments are accepted for source compatibility; unsupported requested transforms raise NotImplementedError.

get_frames

get_frames(
    frame_numbers=None,
    *,
    level=0,
    as_indices=False,
    dtype=None,
    rgba=False,
    fill_missing=False,
    apply_real_world_transform=None,
    apply_modality_transform=None,
    apply_voi_transform=False,
    apply_palette_color_lut=None,
    apply_icc_profile=None,
    **_kwargs
)

batch frame access for WSI stored frames.

get_total_pixel_matrix

get_total_pixel_matrix(
    *,
    row_start=None,
    row_end=None,
    column_start=None,
    column_end=None,
    level=0,
    as_indices=False,
    dtype=None,
    rgba=False,
    apply_real_world_transform=None,
    apply_modality_transform=None,
    apply_voi_transform=False,
    apply_palette_color_lut=None,
    apply_icc_profile=None,
    **_kwargs
)

total pixel matrix access.

Row/column positions are DICOM 1-based by default, with row_end and column_end denoting the first position beyond the returned matrix. Set as_indices=True for Python 0-based intervals.

tile_exists

tile_exists(level, tile) -> bool

Return true when zero-based tile = (tile_x, tile_y) has stored pixel data.

get_thumbnail

get_thumbnail(size)

A downscaled RGB overview of the whole slide fitting within size = (w, h).

open_slide

open_slide(path) -> Slide

Open a slide from a directory of its .dcm instances, a list of paths, or a single multi-level file.

open_slides

open_slides(path) -> dict[str, Slide]

Open every WSI slide found under a directory or path list.

Returns {slide_key: Slide}, where slide_key is usually the FrameOfReferenceUID. Non-WSI files and WSI groups without a VOLUME instance are skipped.

Waveforms (ECG / EEG)

pydcm.waveforms

DICOM waveform I/O for ECG / EEG / hemodynamic / audio (pydcm.waveforms).

Three layers, all over the one DICOM model (no signal-analysis reimplemented — that is neurokit2 / MNE territory; feed them the arrays this module returns):

  • multiplex_array / generate_multiplex.
  • read_waveform — rich read: every multiplex group as physical-unit signals plus per-channel metadata (lead/electrode source, units, sensitivity, filters) and the waveform annotations — as NumPy + dicts.
  • write_waveform — author a Waveform SOP instance (12-lead/General ECG, scalp/sleep EEG, EMG/EOG, hemodynamic, respiratory, audio …) from per-channel arrays + metadata.
  • to_mne — hand a group straight to MNE-Python (EEG/MEG) as a RawArray.

multiplex_array

multiplex_array(ds, index: int = 0, as_raw: bool = True)

The (samples × channels) array of multiplex group index (PS3.3 C.10.9).

With as_raw=False the per-channel Sensitivity / baseline correction from ChannelDefinitionSequence is applied (real-world units).

generate_multiplex

generate_multiplex(ds, as_raw: bool = True)

Yield each multiplex group's array.

read_waveform

read_waveform(src) -> dict

Read every multiplex group of a waveform SOP instance into physical-unit signals plus metadata. src is a path or a parsed dataset. Returns::

{modality, sop_class_uid, groups: [{sampling_frequency, num_samples,
  duration_s, channels: [{label, source, units, sensitivity, baseline,
  sensitivity_correction, filter_low, filter_high, notch}], signals (n×ch
  physical units), raw (n×ch), annotations: [...]}], annotations: [...]}

Hand signals (and a channel's source/units) straight to neurokit2 (ECG) or MNE (EEG) — see to_mne.

write_waveform

write_waveform(
    out,
    signals,
    *,
    sampling_frequency,
    kind="ecg12",
    labels=None,
    sources=None,
    units="mV",
    sensitivity=None,
    sample_bits=16,
    patient_id="",
    patient_name="Anonymous^",
    patient_birth_date="",
    patient_sex="",
    study_uid=None,
    series_uid=None,
    sop_uid=None,
    series_number=1,
    instance_number=1
)

Author a DICOM Waveform SOP instance from per-channel signals.

signals: an (n_samples, n_channels) array (or a list of 1-D channel arrays) in physical units. Each channel is quantised to a sample_bits-bit integer via its sensitivity (physical units per LSB); ChannelSensitivity is stored so :func:read_waveform reconstructs the physical values. sensitivity may be a scalar, a per-channel list, or None (auto: max-fit per channel).

kind selects the IOD: ecg12 / ecg / ecg32 / ambulatory_ecg / hemodynamic / eps / eeg / sleep_eeg / emg / eog / arterial_pulse / respiratory / audio. labels / sources give the per-channel ChannelLabel and source meaning (e.g. "Lead I" / "Fp1"). Returns the SOP Instance UID.

to_mne

to_mne(group, ch_types='eeg')

A multiplex group (from :func:read_waveform) → an mne.io.RawArray.

MNE expects volts; channels in µV/mV are scaled accordingly from their units. Requires MNE (pip install mne). For ECG, prefer neurokit2 on group['signals'].

Networking

DICOMweb

pydcm.dicomweb

DICOMweb client (QIDO-RS) — native request builders + native HTTP transport.

Query a DICOMweb server for studies/series/instances and get DICOM-JSON back::

studies = pydcm.dicomweb.search_studies("http://pacs:8042", base_path="/dicom-web",
                                        matches={"00100020": "PAT001"}, limit=10)

Self-contained (no Python HTTP dependency): the request is built by the native zero-alloc DICOMweb builders — and executed through the native async HTTP transport driven synchronously, so this is the conformance-tested path.

Covers the three core transactions: QIDO-RS search (search_studies/_series/ _instances), WADO-RS retrieve (retrieve_study/_series/_instance → Part-10 bytes), and STOW-RS store (store_instances). Requires the optional _dicomweb extension.

search_studies

search_studies(
    server,
    *,
    base_path="",
    matches=None,
    includefields=None,
    limit=0,
    offset=0,
    auth=""
) -> list[dict]

QIDO-RS study search → list of DICOM-JSON study records.

matches is {tag_or_keyword: value} (e.g. {"00100020": "PAT001"}); includefields is a list of tags/keywords (or ["all"]); auth is an Authorization header value (e.g. "Bearer …"/"Basic …"). Returns [] on 204.

search_series

search_series(
    server,
    study_uid="",
    *,
    base_path="",
    matches=None,
    includefields=None,
    limit=0,
    offset=0,
    auth=""
) -> list[dict]

QIDO-RS series search (all series, or within study_uid).

search_instances

search_instances(
    server,
    study_uid="",
    series_uid="",
    *,
    base_path="",
    matches=None,
    includefields=None,
    limit=0,
    offset=0,
    auth=""
) -> list[dict]

QIDO-RS instance search (optionally scoped to a study/series).

resolve_auth

resolve_auth(config) -> tuple[str, str]

Resolve a unified outbound-auth config to the (header_name, header_value) it produces, via the shared native engine. config is a dict::

{"scheme": "none"|"basic"|"bearer"|"api_key"|"oauth2", ...}
  basic   : username, password
  bearer  : token
  api_key : token, header_name (default "Authorization")
  oauth2  : token_url, client_id, client_secret, scope, refresh_token,
            use_cache (bool, default True), cache_dir

OAuth2 acquires/refreshes + caches a Bearer token (cache → refresh_token → client_credentials). Returns ("", "") for scheme "none". Raises RuntimeError on failure. For Basic/Bearer/OAuth2 the header name is "Authorization", so value is what every auth= parameter here expects.

retrieve_study

retrieve_study(
    server,
    study_uid,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
) -> list[bytes]

WADO-RS: retrieve every Part-10 instance of a study → list of bytes blobs.

transfer_syntax optionally negotiates the wire encoding (a TS UID, e.g. "1.2.840.10008.1.2.4.50" for JPEG baseline, or "*" for any) — the server falls back to the default if it cannot honour it.

start_retrieve

start_retrieve(
    server,
    study_uid,
    series_uid="",
    *,
    store_dir,
    base_path="",
    transfer_syntax="",
    auth=""
)

Start a BACKGROUND WADO-RS retrieve on an engine-owned C++ thread that streams each instance of the study (or one series_uid) to store_dir as a Part-10 file. No Python thread drives it, so it progresses even while the host's main thread holds the GIL — the basis for a truly non-blocking background retrieve (the WADO twin of pydcm.dimse.AE.start_get_to_dir). Returns a handle: poll handle.done (bool), then read handle.result() (dict: done/ok/count/error). Drop the handle only once done (its destructor joins the worker).

transfer_syntax sets the WADO-RS transfer-syntax Accept parameter: empty = the server's default (PS3.18: Explicit VR Little Endian, possibly transcoded), "*" = the stored encoding verbatim (no transcoding), or a TS UID for a specific encoding.

retrieve_series

retrieve_series(
    server,
    study_uid,
    series_uid,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
) -> list[bytes]

WADO-RS: retrieve every Part-10 instance of a series → list of bytes blobs.

retrieve_instance

retrieve_instance(
    server,
    study_uid,
    series_uid,
    instance_uid,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
) -> bytes

WADO-RS: retrieve one Part-10 instance → bytes (raises if the server returns none).

retrieve_instance_wado_uri

retrieve_instance_wado_uri(
    server,
    study_uid,
    series_uid,
    instance_uid,
    *,
    base_path="/wado",
    content_type="application/dicom",
    transfer_syntax="",
    frame_number=0,
    rows=0,
    columns=0,
    image_quality=0,
    anonymize=False,
    auth=""
) -> bytes

WADO-URI: retrieve one object via the classic query-parameter GET (PS3.18 §9) → bytes.

WADO-URI predates WADO-RS and is the only retrieval protocol on many older PACS; for modern servers prefer :func:retrieve_instance (WADO-RS), which this mirrors. Returns the object bytes — a Part-10 instance when content_type="application/dicom" (the default), or the server-rendered image bytes for an image MIME (e.g. "image/jpeg"), in which case rows/columns/image_quality apply.

base_path is the WADO-URI endpoint (default "/wado" — a standalone endpoint, not under the DICOMweb /dicom-web root); frame_number is 1-based (0 = unset); transfer_syntax optionally negotiates the wire encoding; anonymize=True asks the server for a de-identified object.

retrieve_study_metadata

retrieve_study_metadata(
    server, study_uid, *, base_path="", auth=""
) -> list[dict]

WADO-RS: study metadata → list of per-instance DICOM-JSON records (no pixel data).

retrieve_series_metadata

retrieve_series_metadata(
    server, study_uid, series_uid, *, base_path="", auth=""
) -> list[dict]

WADO-RS: series metadata → list of per-instance DICOM-JSON records.

retrieve_instance_metadata

retrieve_instance_metadata(
    server,
    study_uid,
    series_uid,
    instance_uid,
    *,
    base_path="",
    auth=""
) -> dict

WADO-RS: one instance's metadata → a single DICOM-JSON record.

retrieve_frames

retrieve_frames(
    server,
    study_uid,
    series_uid,
    instance_uid,
    frames,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
) -> list[bytes]

WADO-RS: retrieve specific frames of an instance → list of raw frame bytes.

frames is a 1-based frame number, an iterable of them, or a spec string ("1", "1,3-5,7"). transfer_syntax optionally negotiates the frame pixel encoding.

iter_study

iter_study(
    server,
    study_uid,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
)

WADO-RS: yield each Part-10 instance of a study as bytes (memory-efficient stream).

iter_series

iter_series(
    server,
    study_uid,
    series_uid,
    *,
    base_path="",
    transfer_syntax="",
    auth=""
)

WADO-RS: yield each Part-10 instance of a series as bytes (memory-efficient stream).

retrieve_bulkdata

retrieve_bulkdata(uri, *, auth='') -> list[bytes]

WADO-RS: follow a server-issued BulkDataURI → list of bytes.

uri is the absolute URL the server handed out (e.g. a metadata record's BulkDataURI for PixelData); it is fetched verbatim, since its path layout is server-specific.

retrieve_rendered

retrieve_rendered(
    server,
    study_uid,
    series_uid="",
    instance_uid="",
    *,
    base_path="",
    level=None,
    quality=0,
    window=None,
    viewport=None,
    auth=""
) -> bytes

WADO-RS: a server-rendered image (default image/jpeg) → bytes.

level defaults to the deepest UID supplied (instance > series > study). window is an optional (center, width) tuple; quality an optional JPEG quality (1–100); viewport an optional (width, height) output size.

delete_study

delete_study(
    server, study_uid, *, base_path="", auth=""
) -> int

DICOMweb DELETE a whole study → HTTP status (200/204). Server must support the (non-core) delete transaction.

delete_series

delete_series(
    server, study_uid, series_uid, *, base_path="", auth=""
) -> int

DICOMweb DELETE a series → HTTP status.

delete_instance

delete_instance(
    server,
    study_uid,
    series_uid,
    instance_uid,
    *,
    base_path="",
    auth=""
) -> int

DICOMweb DELETE a single instance → HTTP status.

store_instances

store_instances(
    server,
    instances,
    *,
    study_uid="",
    base_path="",
    auth=""
) -> dict

STOW-RS: store Part-10 instances on the server.

instances is an iterable of bytes (path-like / file objects are NOT accepted — pass raw DICOM bytes, e.g. open(p, "rb").read() or ds.to_bytes()). If study_uid is given, the POST is bound to that study (server rejects mismatched StudyInstanceUID).

Returns {"status", "stored", "failed"} — status 200 (all stored) / 202 (partial) / 409 (none); stored and failed are lists of {sop_class_uid, sop_instance_uid, ...} parsed natively from the ReferencedSOPSequence (00081199) / FailedSOPSequence (00081198) by the native store-response parser, so a 202 partial-success is directly inspectable (failed[i]["failure_reason"] holds the PS3.4 code).

create_workitem

create_workitem(
    server,
    workitem,
    *,
    workitem_uid="",
    base_path="",
    auth=""
)

UPS-RS create (POST /workitems). workitem is a Dataset/dict of attributes; pass workitem_uid to propose the SOP Instance UID, else the server assigns one (returned in the result's location). Returns {status, location, body}.

search_workitems

search_workitems(
    server,
    *,
    base_path="",
    matches=None,
    includefields=None,
    limit=None,
    offset=0,
    auth=""
)

UPS-RS search (GET /workitems) → list of DICOM-JSON workitem records.

limit=None omits the parameter; limit=0 explicitly requests zero matches. Returns [] for an empty response.

retrieve_workitem

retrieve_workitem(
    server, workitem_uid, *, base_path="", auth=""
)

UPS-RS retrieve (GET /workitems/{w}) → the workitem as DICOM JSON (dict).

update_workitem

update_workitem(
    server,
    workitem_uid,
    changes,
    transaction_uid="",
    *,
    base_path="",
    auth=""
)

UPS-RS update (POST /workitems/{w}[?Transaction-uid=...]). changes is a Dataset/dict of attributes to merge. Omit transaction_uid for a SCHEDULED workitem; supply the current UID for an IN PROGRESS workitem.

change_workitem_state

change_workitem_state(
    server,
    workitem_uid,
    state,
    transaction_uid,
    *,
    requester="",
    base_path="",
    auth=""
)

UPS-RS change state (PUT /workitems/{w}/state). state is the CS value — e.g. "IN PROGRESS" / "COMPLETED" / "CANCELED" (use pydcm.dimse.UPS.IN_PROGRESS etc.); the body is built natively from (state, transaction_uid). requester is the optional requester AE Title query parameter.

request_cancel_workitem

request_cancel_workitem(
    server,
    workitem_uid,
    *,
    reason=None,
    requester="",
    base_path="",
    auth=""
)

UPS-RS cancel request (POST /workitems/{w}/cancelrequest). reason = Dataset/dict/None.

subscribe_workitem

subscribe_workitem(
    server,
    workitem_uid,
    ae_title,
    *,
    deletion_lock=False,
    filters=None,
    base_path="",
    auth=""
)

UPS-RS subscribe (POST /workitems/{w}/subscribers/{ae}). Pass the global subscription instance UID as workitem_uid to watch all workitems. filters is an attribute-to-value mapping for the filtered global subscription resource.

unsubscribe_workitem

unsubscribe_workitem(
    server, workitem_uid, ae_title, *, base_path="", auth=""
)

UPS-RS unsubscribe (DELETE /workitems/{w}/subscribers/{ae}).

suspend_global_subscription

suspend_global_subscription(
    server,
    global_resource_uid,
    ae_title,
    *,
    base_path="",
    auth=""
)

UPS-RS suspend a global or filtered-global subscription.

DIMSE

pydcm.dimse

DIMSE networking (pydcm.dimse).

A thin, familiar DIMSE API over pydcm's native C++ DIMSE engine, via the pydcm._dimse binding. Common SCU/SCP workflows work after one alias::

import pydcm.dimse as dimse
from pydcm.dimse import AE, evt

ae = AE(ae_title="MY_SCU")
ae.add_requested_context(Verification)
assoc = ae.associate("127.0.0.1", 11112)
if assoc.is_established:
    status = assoc.send_c_echo()          # Dataset with .Status
    assoc.release()

# SCP:
ae.add_supported_context(CTImageStorage)
server = ae.start_server(("0.0.0.0", 11112), block=False,
                         evt_handlers=[(evt.EVT_C_STORE, handle_store)])

ae.associate() follows the common SCU call signature (addr, port, contexts=None, ae_title=..., max_pdu=..., bind_address=...) and opens ONE association (a persistent native client) that send_c_echo / send_c_store / send_c_find / send_c_move reuse, exactly like One negotiate, many ops, one release(). The contexts negotiated are the ones you add_requested_context'd (plus a broad default set so basic flows work out of the box). send_c_get also reuses the persistent association when you add_supported_context the storage classes to receive — they are negotiated with the PS3.7 §D.3.3.4 role flip (scp_role) at associate() so the matched instances arrive as inbound C-STORE on the same connection. Without any supported context it falls back to a one-shot association (CT/MR defaults). TLS is opt-in via ae.tls = {...} (ca_file/cert_file/key_file/verify_peer/server_name/ciphers/check_hostname). ciphers takes "bcp195" for the curated RFC 9325 AEAD+PFS allowlist, "" for the provider default, or a raw OpenSSL cipher string; the server side also wins cipher selection. SCU TLS verifies the certificate chain and the dialled DNS/IP SAN when verify_peer is on. server_name overrides that reference identity and DNS SNI; check_hostname=False explicitly selects chain-only checking. verify_peer=False drops all verification (dev only). For multihomed or VPN clients, AE.associate(..., bind_address=(ip, 0)) can force the outbound TCP source address; otherwise the OS routing table chooses it. AE.start_server honours the host in (host, port) so C-MOVE destination listeners can bind a VPN address or "0.0.0.0" explicitly.

AE

An Application Entity over the native DIMSE engine.

active_associations property

active_associations

The associations opened by this AE that are still established.

remove_requested_context

remove_requested_context(
    abstract_syntax, transfer_syntax=None
)

Remove a requested presentation context.

remove_supported_context

remove_supported_context(
    abstract_syntax, transfer_syntax=None
)

Remove a supported presentation context.

make_server

make_server(
    address,
    ae_title=None,
    contexts=None,
    ssl_context=None,
    evt_handlers=None,
    server_class=None,
    **kw
)

Return a non-blocking SCP server handle; call .serve_forever() / .shutdown() on it. == start_server(block=False).

shutdown

shutdown()

Stop all servers started by this AE.

add_requested_contexts_for_files

add_requested_contexts_for_files(paths)

Add exact Storage contexts needed to stream these Part-10 files.

File streaming does not transcode. Call this before associate() when using Association.send_c_store_file(s) for compressed or otherwise non-default transfer syntaxes.

get_to_dir

get_to_dir(
    addr,
    port,
    dataset,
    query_model,
    store_dir,
    ae_title="ANY-SCP",
    tls_args=None,
    accept_storage_classes=None,
    bind_address=None,
)

Native C-GET that writes each matched instance straight to store_dir as a Part-10 file IN C++ (via the shared store_sink) — no per-instance Python callback, so the GIL is released for the WHOLE transfer. Safe to call from a background/worker thread without starving the host's event loop (unlike send_c_get, whose EVT_C_STORE handler re-acquires the GIL per instance). One-shot helper: it opens a persistent association negotiating the storage SCP role (so the SCP can push instances back), retrieves, then releases. Returns the result dict (status / completed / failed / received / remaining).

start_get_to_dir

start_get_to_dir(
    addr,
    port,
    dataset,
    query_model,
    store_dir,
    ae_title="ANY-SCP",
    tls_args=None,
    accept_storage_classes=None,
    bind_address=None,
)

Start a NATIVE C-GET on an engine-owned C++ thread (std::jthread) that writes each matched instance to store_dir via the shared store_sink. No Python thread drives it, so it keeps running even while the host's main thread holds the GIL (idle in a Qt/event loop) — the basis for a truly non-blocking background retrieve. Returns a handle: poll handle.done (a bool property), then read handle.result() (a dict with status / completed / failed / received / error). Drop the handle only once done (its destructor joins the worker).

start_server

start_server(
    address,
    block=True,
    ssl_context=None,
    evt_handlers=None,
    ae_title=None,
    contexts=None,
    store_dir=None,
    **_kw
)

store_dir ⇒ native store-to-dir listener: received instances are streamed to the native Part-10 store sink entirely in C++ (no per-instance Python C-STORE callback, no GIL, no full-data-set buffer). When set, any EVT_C_STORE handler is bypassed (the native sink wins).

Association

A pydcm DIMSE association, persistent: ae.associate() opens it, every send_c_* reuses it, release() tears it down. C-GET is the lone exception (one-shot, for SCU-role storage negotiation — see the module docstring).

local property

local

The local (requestor) AE — {ae_title, address, port}.

remote property

remote

The remote (acceptor) AE — {ae_title, address, port}.

name property writable

name

Thread-compatible association name property.

daemon property writable

daemon

Thread-compatible daemon flag. pydcm has no Python association thread.

accepted_contexts property

accepted_contexts

Presentation contexts the peer accepted — each carries the negotiated transfer_syntax. Built from the proposals + the engine's per-context negotiation result.

rejected_contexts property

rejected_contexts

Presentation contexts the peer did not accept.

kill

kill()

Forcibly terminate the association — same as abort().

bind

bind(event, handler, args=None)

Bind a handler to a lifecycle event (EVT_ESTABLISHED/RELEASED/ABORTED) on this association.

unbind

unbind(event, handler)

Unbind a lifecycle event handler.

send_c_cancel

send_c_cancel(msg_id, context_id=None, query_model=None)

Accepted for source compatibility. The native engine runs each C-FIND/GET/ MOVE to completion (no per-operation C-CANCEL), so this is a no-op; use abort() to tear the association down.

send_c_store_file

send_c_store_file(
    path,
    msg_id=1,
    originator_aet=None,
    originator_id=None,
    context_id=None,
)

C-STORE one Part-10 file without materialising it as a Python Dataset.

This is the file-path fast path for large studies: native code reads only the Part-10 file meta up front, then streams the bare data set bytes from disk through the native streaming C-STORE API.

send_c_store_files

send_c_store_files(paths)

C-STORE multiple Part-10 files via the native streaming file path.

Returns the native summary dict: sent/succeeded/failed/last_status/aborted.

send_c_get

send_c_get(
    dataset,
    query_model,
    msg_id=1,
    priority=2,
    store_dir=None,
)

C-GET. Matched instances are delivered to the EVT_C_STORE handler registered on this AE. When the AE has add_supported_context'd the storage classes (negotiated with SCP role at associate()), it runs on the PERSISTENT association; otherwise it falls back to a one-shot association that negotiates a CT/MR default set to receive the inbound C-STORE-RQs.

store_dir ⇒ NATIVE retrieve-to-dir: the engine writes each matched instance to that directory in C++ (shared store_sink), with NO per-instance Python callback, so the GIL is released for the whole transfer — safe from a background thread. Requires the persistent path (call add_supported_context() for the storage classes first).

ups_push

ups_push(workitem, instance_uid=None)

UPS Push: create a workitem (N-CREATE). The SCU mints the required SOP Instance UID when instance_uid is omitted.

ups_get

ups_get(workitem_uid, attributes=None)

UPS Pull: read a workitem (N-GET); attributes empty => whole workitem.

ups_set

ups_set(workitem_uid, modifications, transaction_uid)

UPS Pull: merge a modification list into a claimed workitem (N-SET). Stamps the claiming Transaction UID.

ups_change_state

ups_change_state(workitem_uid, state, transaction_uid)

UPS Pull: change Procedure Step State (N-ACTION type 1).

ups_claim

ups_claim(workitem_uid, transaction_uid=None)

Claim a SCHEDULED workitem (-> IN PROGRESS). Mints a Transaction UID if not given; returns (status, transaction_uid).

ups_complete

ups_complete(workitem_uid, transaction_uid)

Transition a claimed workitem to COMPLETED.

ups_cancel

ups_cancel(workitem_uid, transaction_uid)

Transition a claimed workitem to CANCELED (owner-side; use :meth:ups_request_cancel to ask another performer to cancel).

ups_request_cancel

ups_request_cancel(workitem_uid, reason=None)

UPS Push: request cancellation of a workitem you don't own (N-ACTION type 2); optional (0074,1238) Reason For Cancellation.

ups_subscribe

ups_subscribe(target=None, receiving_ae=None)

UPS Watch: subscribe to event reports (N-ACTION type 3). target defaults to the global subscription instance (watch all workitems).

ups_unsubscribe

ups_unsubscribe(target=None, receiving_ae=None)

UPS Watch: cancel a subscription (N-ACTION type 4).

mpps_create

mpps_create(attributes, instance_uid=None)

MPPS N-CREATE — start a Performed Procedure Step (the dataset should carry PerformedProcedureStepStatus == MPPS.IN_PROGRESS plus the Scheduled/Performed step attributes). Mints a SOP Instance UID when not given; returns (status, sop_instance_uid).

mpps_set

mpps_set(instance_uid, modifications)

MPPS N-SET — update a step (typically transition PerformedProcedureStepStatus to COMPLETED / DISCONTINUED plus the Performed Series Sequence, end date/time).

mpps_complete

mpps_complete(instance_uid, modifications=None)

MPPS N-SET transitioning the step to COMPLETED (stamps the status; merge any final Performed* attributes via modifications).

mpps_discontinue

mpps_discontinue(instance_uid, modifications=None)

MPPS N-SET transitioning the step to DISCONTINUED (operator cancelled the exam mid-procedure).

request_storage_commitment

request_storage_commitment(
    referenced_sop_instances, *, transaction_uid=None
)

Storage Commitment N-ACTION (Request Storage). referenced_sop_instances is an iterable of (sop_class_uid, sop_instance_uid) pairs to commit. transaction_uid (0008,1195) is PS3.4 Table J.3-1 Type 1 — minted here if not given. Returns (status, transaction_uid): keep the Transaction UID to correlate the commitment RESULT, which arrives asynchronously as an N-EVENT-REPORT (EVENT_SUCCESS / EVENT_FAILURES).

notify_instances_available

notify_instances_available(
    study_uid,
    instances,
    *,
    retrieve_ae=None,
    availability=None,
    instance_uid=None
)

IAN N-CREATE (PS3.4 §R): announce that instances of study_uid are available. instances is an iterable of (series_uid, sop_class_uid, sop_instance_uid) tuples (grouped by series here). availability is an IAN.* value (default IAN.ONLINE); retrieve_ae is the (0008,0054) Retrieve AE Title applied to every instance. Mints the IAN SOP Instance UID if not given. Returns (status, sop_instance_uid). For per-instance availability/retrieve, build the Attribute List Dataset yourself and call :meth:send_n_create on IAN.SOP_CLASS.

alias_presentation_context

alias_presentation_context(sop_class_uid, negotiated_as)

Route N-services for sop_class_uid over the presentation context negotiated for negotiated_as (PS3.4 §H Meta SOP-class alias).

Call after :meth:associate when you negotiated the §H Grayscale or Color Print Management Meta SOP class, then alias each member class (Film Session, Film Box, Image Box) onto the Meta so the engine selects the right presentation context for every N-* command::

assoc.alias_presentation_context(Print.FILM_SESSION, Print.GRAYSCALE_META)
assoc.alias_presentation_context(Print.FILM_BOX,     Print.GRAYSCALE_META)
assoc.alias_presentation_context(Print.GRAYSCALE_IMAGE_BOX, Print.GRAYSCALE_META)

No-op if the association is not established.

print_film_session

print_film_session(image_display_format='STANDARD\\1,1')

§H Film Session + Film Box N-CREATE workflow (steps 1–2).

Opens a Film Session, creates a Film Box with the given image_display_format, and returns (status, film_session_uid, film_box_uid, image_box_uid) — the Image Box UID is what you pass to :meth:print_set_image_box. Returns (status, None, None, None) on first N-CREATE failure.

print_set_image_box

print_set_image_box(
    image_box_uid,
    pixel_bytes,
    rows,
    cols,
    samples_per_pixel=1,
    position=1,
)

§H Image Box N-SET (step 2b): load rendered pixel data into the Image Box the SCP assigned.

pixel_bytes must be raw 8-bit display pixels in row-major order: rows × cols × samples_per_pixel bytes, where samples_per_pixel is 1 for MONOCHROME2 (grayscale) or 3 for RGB (color, interleaved). Uses :func:build_image_box_mods to build the validated wire bytes so the caller doesn't need to know the DICOM attribute layout.

print_action

print_action(film_box_uid)

§H N-ACTION Print (step 3): ask the SCP to print the Film Box.

print_cleanup

print_cleanup(film_box_uid=None, film_session_uid=None)

§H N-DELETE cleanup (step 4): best-effort; ignores failures.

Ophthalmic visual field

pydcm.opv

Ophthalmic Visual Field static perimetry (PS3.3 Supplement 146, SOP Class 1.2.840.10008.5.1.4.1.1.80.1).

Manage OPV DICOM: parse a study, flatten it to pandas / JSON, and check standard conformance.

No DICOM logic lives here. The semantic extraction is the native content engine (exposed as :func:pydcm.content); compliance is the native IOD/module conformance judge (exposed as :func:pydcm.iod_validate). This module is only the pandas/JSON ergonomics on top.

Example

import pydcm vf = pydcm.opv.read_dicom("vf.dcm") vf.pointwise_to_pandas() # one row per stimulus location vf.to_pandas() # one row of study-level fields vf.check_dicom_compliance() # Supplement 146 IOD Type-1/2 findings opvset, errors = pydcm.opv.read_dicom_directory("study_dir/")

OPVDicom

A single Ophthalmic Visual Field DICOM file.

test_points property

test_points

The per-stimulus records (list of dicts).

to_dict

to_dict()

The full semantic content as a nested dict.

to_pandas

to_pandas()

Study-level fields as a single-row DataFrame (test points excluded).

pointwise_to_pandas

pointwise_to_pandas()

One row per test point, each tagged with the study identifiers.

pointwise_to_nested_json

pointwise_to_nested_json()

Study identifiers + the nested per-point records (JSON-ready dict).

check_dicom_compliance

check_dicom_compliance()

Sup-146 IOD / module conformance findings (list of dicts).

Reuses the native IOD conformance judge — every mandatory module's Type-1 / Type-2 attribute must be present (Type-1 also non-empty), descending into present sequences. An empty list means conformant at the IOD level. Stricter than a flat tag checklist: it is per-SOP-Class and nested-sequence aware.

OPVDicomSet

A batch of :class:OPVDicom files with set-level aggregation.

to_pandas

to_pandas()

Study-level rows for every file, concatenated.

pointwise_to_pandas

pointwise_to_pandas()

Pointwise rows for every file, concatenated.

pointwise_to_nested_json

pointwise_to_nested_json()

Nested per-file JSON records (list).

check_dicom_compliance

check_dicom_compliance()

{path: [findings, ...]} across the set.

read_dicom

read_dicom(path)

Load a single OPV DICOM file → :class:OPVDicom.

read_dicom_directory

read_dicom_directory(dicom_directory, file_extension='dcm')

Load every *.<file_extension> OPV file in a directory.

Returns (OPVDicomSet, errors) where errors is a list of (path, message) for files that were not OPV objects or failed to parse.

read_visual_field

read_visual_field(path)

The native semantic content (nested dict) of an OPV file, or None.

The low-level reader behind :class:OPVDicom; mirrors pydcm.read_seg / pydcm.read_report in returning the raw structured content.

EHR bridges

FHIR

pydcm.fhir

FHIR R4 bridge — DICOM → FHIR resources, over the native FHIR engine.

Turns a DICOM instance into a FHIR ImagingStudy resource (the imaging↔EHR seam), so an agent or app can hand a study to a FHIR consumer without a separate mapping layer::

study = pydcm.fhir.imaging_study("CT0001.dcm")   # -> dict (FHIR R4 ImagingStudy)

The field mapping (study/series/instance + a contained Patient) lives in the native engine; this module is the thin marshaller. Requires the optional _fhir extension (like _dimse for networking).

imaging_study

imaging_study(source) -> dict

Build a FHIR R4 ImagingStudy (as a dict) from one DICOM instance, or a whole study of many instances.

source may be:

  • raw Part-10 bytes — a single instance;
  • a path to a .dcm file — a single instance;
  • a path to a directory — every readable instance under it is aggregated into one ImagingStudy, grouped by SeriesInstanceUID (the multi-series / multi-instance study form a FHIR consumer actually expects);
  • an iterable of paths / bytes — likewise aggregated.

Study/series/instance identifiers, the modality set, and patient demographics are mapped into the FHIR ImagingStudy with a contained Patient (subject.reference = "#patient-…"). Raises if no readable instance is found or StudyInstanceUID is absent.

HL7 v2

pydcm.hl7

HL7 v2 — parse messages (MSH-12 2.3 – 2.8) + build ORU^R01 results, over the native HL7 engine.

The HL7 side of the imaging↔EHR seam: read an inbound order/result and emit a radiology result back to the HIS::

segs = pydcm.hl7.parse(open("oru.hl7").read())   # -> [{"id": "MSH", "fields": [...]}, ...]
msg  = pydcm.hl7.build_oru(config, context, observations)   # -> ER7 string

Parsing returns each segment as {id, fields} (fields split on the field separator — the universal ER7 shape). build_oru takes plain dicts. MLLP networking (send/listen) is not bound here yet — this is the message layer. Requires the optional _hl7 extension.

parse

parse(text: str) -> list[dict]

Parse an HL7 v2 message into a list of segments.

Each segment is {"id": "MSH"|"PID"|…, "fields": [str, …]} where fields is the segment split on its field separator (ER7). Raises on a malformed message.

build_oru

build_oru(
    config: dict, context: dict, observations: list[dict]
) -> str

Build an HL7 v2.5 ORU^R01 result message (ER7 string).

config → MSH (our_app/our_facility/their_app/their_facility/ control_id/timestamp/version/specific_character_set); context → PID + ORC/OBR order identifiers (patient_, order_number, accession_number, procedure*, modality, ordering_provider, observation_datetime); observations → OBX rows (observation_id, value, value_type def "TX", status def "F"). Absent keys keep the native defaults.

Agent / MCP server

pydcm.mcp

pydcm — agent-facing MCP server (pydcm.mcp).

The Model Context Protocol projection of pydcm, over the shared native agent engine. A self-contained agent surface: it dispatches in-process to Python and covers pydcm end to end — analysis (radiomics / DVH / validation), perfusion (DCE + DSC), volume / 4-D / DWI assembly, WSI, waveforms, the structured-content reader, authoring, de-identification, and DIMSE / DICOMweb networking.

Every tool reads on the same foundation: ALL image codecs / transfer syntaxes are compiled in (JPEG / JPEG-LS / JPEG 2000 / HTJ2K / JPEG-XL / RLE / deflated / video) — no external plugins — and ALL character sets are decoded by the native charset engine (strong on CJK / ISO-2022 multi-byte), which is fault-tolerant: an unknown or non-conformant vendor SpecificCharacterSet falls back gracefully instead of erroring. So the read/inspect/convert tools work on real-world, vendor-quirky files.

Run it as an MCP stdio server (what an agent / Claude Desktop / mcp client connects to)::

python -m pydcm.mcp

Each tool's handler returns a string: a JSON object (surfaced as structuredContent), plain text, or a data:image/*;base64,… data URI (surfaced as an image block). Register more tools with the :func:tool decorator. Requires the _agent extension (included in builds with agent support).

serve

serve(verbose: bool = False) -> int

Run the MCP stdio server until stdin EOF. Returns the engine exit code.

The rest of the DICOM API

The standard DICOM data model is all here, so existing code runs unchanged.

TypesDataset, FileDataset, FileMetaDataset, DataElement, Sequence, PersonName, Tag, BaseTag, MultiValue, DSfloat, IS, UID, InvalidDicomError.

Submodulescharset, config, datadict, dataelem, dataset, dicomio, encaps, encoders, env_info, errors, examples, filereader, filewriter, jsonrep, multival, overlays, pixel_data_handlers, sequence, tag, uid, valuerep, values.

See Behaviour notes for the deliberate behaviours worth knowing.