Medical image I/O for Python — read and write NIfTI, DICOM, MetaImage, and more in one consistent API.
- One function reads any format; format auto-detected from path
- Returns a NumPy array + rich
MetaData(affine, orientation, spacing) — no format-specific objects to unwrap - Transparent coordinate system normalization between ITK and NiBabel conventions
- Metadata-only reads (
read_meta) for large files when you only need spatial info
pip install medioimport medio
arr, meta = medio.read_img('scan.nii.gz')
print(meta.ornt, meta.spacing) # e.g. 'LPI', [0.5, 0.5, 1.0]
medio.save_img('out.mhd', arr, meta)meta = medio.read_meta('large_scan.nii.gz')
print(meta.spatial_shape) # (256, 256, 128)
print(meta.affine.spacing) # [0.98, 0.98, 1.5]arr, meta = medio.read_img('scan.nii.gz', desired_ornt='RAS')
# arr axes are reordered; meta.affine updated to matcharr, meta = medio.read_img('scan.mhd')
medio.save_dir('dicom_out/', arr, meta)A DICOM directory is only a valid 3D volume if its slices agree geometrically. Series Instance UID
does not guarantee that: scanners emit a localizer/scout under the same Series Instance UID as
the acquisition, and reading it as part of the volume silently corrupts the derived slice spacing.
read_img and read_meta therefore refuse an inconsistent series instead of returning a wrong
image:
medio.read_img('dicom_dir/')
# InconsistentSeriesError: DICOM series mixes slice orientations: 1 of 325 slice(s) are not
# parallel to the first ... typically a localizer/scout slice sharing the Series Instance UID.
# read only the slices that share the dominant geometry (drops the localizer):
arr, meta = medio.read_img('dicom_dir/', keep_dominant_geometry=True)
# opt out entirely and get the previous, unchecked behaviour:
arr, meta = medio.read_img('dicom_dir/', validate_series=False)The checks are the invariants any single volume must satisfy — one orientation, one in-plane
geometry, uniformly spaced slice positions — and they catch missing or duplicated slices as well as
localizers. SliceThickness is deliberately not required to equal the slice spacing: gapped and
overlapping reconstructions are legitimate.
keep_dominant_geometry filters first and then still validates what remains, so it discards
off-geometry slices without hiding a genuine gap in the rest.
from medio.medimg import MedImg
mimg = MedImg(arr, meta)
cropped = mimg[2:8, 3:7, :] # origin updated
downsampled = mimg[::2, ::2, ::1] # spacing updated| Format | Extensions | Default backend |
|---|---|---|
| NIfTI | .nii, .nii.gz |
ITK |
| DICOM | directory or .dcm |
ITK |
| MetaImage | .mhd, .mha |
ITK |
| NIfTI (NiBabel) | .nii, .nii.gz |
backend='nib' |
| DICOM (pydicom) | .dcm |
backend='pdcm' |
| Other ITK formats | .png, .jpg, … |
ITK |
medio.read_img(input_path, desired_ornt=None, backend=None, dtype=None,
header=False, channels_axis=-1, coord_sys='itk', **kwargs)
→ tuple[np.ndarray, MetaData]| Parameter | Type | Default | Description |
|---|---|---|---|
input_path |
path-like | — | File or DICOM directory |
desired_ornt |
str | None | None |
Reorient to this axis code (e.g. 'RAS') |
backend |
str | None | None |
Force backend: 'itk', 'nib', 'pdcm' |
dtype |
dtype | None | None |
Cast array to this dtype |
header |
bool | False |
Include raw format header in MetaData.header |
channels_axis |
int | None | -1 |
Axis for multi-channel (e.g. RGB) images |
coord_sys |
'itk' | 'nib' | None |
'itk' |
Coordinate convention for orientation and metadata |
**kwargs are passed to the backend. DICOM directories (both backends): series, validate_series, keep_dominant_geometry. ITK-specific: pixel_type, fallback_only. pydicom-specific: globber, allow_default_affine.
| DICOM-directory parameter | Type | Default | Description |
|---|---|---|---|
validate_series |
bool | True |
Raise InconsistentSeriesError unless the slices form one consistent 3D volume. False restores the previous, unchecked behaviour |
keep_dominant_geometry |
bool | False |
Read only the slices sharing the dominant geometry, discarding e.g. a localizer that shares the Series Instance UID |
medio.read_meta(input_path, desired_ornt=None, backend=None,
header=False, coord_sys='itk', **kwargs)
→ MetaDataSee read_img for parameters. Reads only spatial metadata without loading pixel data.
medio.save_img(filename, np_image, metadata, use_original_ornt=True,
backend=None, dtype=None, channels_axis=None,
mkdir=False, parents=False, **kwargs)| Parameter | Type | Default | Description |
|---|---|---|---|
filename |
path-like | — | Output file path (format inferred from suffix) |
np_image |
ndarray | — | Image array |
metadata |
MetaData | — | Spatial metadata |
use_original_ornt |
bool | True |
Reorient to metadata.orig_ornt before saving |
backend |
str | None | None |
Force backend: 'itk' or 'nib' |
dtype |
dtype | None | None |
Cast before saving |
channels_axis |
int | None | None |
Axis of channel dimension in np_image |
mkdir |
bool | False |
Create the output directory if it doesn't exist |
parents |
bool | False |
Create all missing parent directories |
medio.save_dir(dirname, np_image, metadata, use_original_ornt=True,
dtype=None, channels_axis=None, parents=False,
exist_ok=False, allow_dcm_reorient=False, **kwargs)Saves a 3D array as a DICOM series of 2D slices.
| Parameter | Default | Description |
|---|---|---|
dirname |
— | Output directory |
exist_ok |
False |
Allow writing into an existing non-empty directory |
allow_dcm_reorient |
False |
Reorient to nearest right-handed orientation if needed |
pattern |
'IM{}.dcm' |
Filename pattern; {} is replaced with the slice number |
metadata_dict |
None |
Override or add DICOM tags, e.g. {'0008|0060': 'US'} |
medio.MetaData(affine, coord_sys='itk', orig_ornt=None, header=None, spatial_shape=None)| Property | Type | Description |
|---|---|---|
affine |
Affine |
4×4 spatial transform (index space → physical space) |
coord_sys |
str | 'itk' or 'nib' |
ornt |
str | Current orientation code (e.g. 'LPI'), derived from affine |
orig_ornt |
str | Orientation before any reorientation |
spacing |
ndarray | Voxel spacing — alias for affine.spacing |
header |
dict | None | Raw format header (populated when header=True in read_img) |
spatial_shape |
tuple | None | Image dimensions (populated by read_meta) |
Methods: .convert(dest_coord_sys) — in-place convention switch; .clone() — deep copy.
A 4×4 NumPy array subclass with named spatial accessors.
from medio import Affine
import numpy as np
aff = Affine(np.eye(4))
aff = Affine(direction=np.eye(3), spacing=[0.5, 0.5, 1.0], origin=[0., 0., 0.])
coord = aff.index2coord([4, 0, 9]) # map voxel index → physical coordinateProperties: .spacing, .origin, .direction (all gettable and settable). Method: .clone().
For a mathematical background see NiBabel's affine documentation.
Container for an image array + metadata with spatially-aware indexing.
from medio.medimg.medimg import MedImg
mimg = MedImg(arr, meta) # from array + metadata
mimg = MedImg.from_file('scan.mhd') # load from fileIndexing crops or downsamples the array and updates the affine automatically:
| Indexing | Effect on metadata |
|---|---|
mimg[2:8, 3:7, :] |
origin updated to new start voxel |
mimg[::2, ::2, ::1] |
spacing scaled by step sizes |
mimg[..., 5:15] |
ellipsis supported |
Properties: .np_image, .metadata. Method: .save(filename).
medio uses ITK convention by default (coord_sys='itk').
Pass coord_sys='nib' to read_img / read_meta to work in NiBabel convention throughout.
Apache 2.0 — see LICENSE.
Issues and contributions: github.com/RSIP-Vision/medio/issues