| name | simpleitk-image-registration |
| description | Register, segment, filter, resample 3D medical images (MRI, CT, microscopy) via SimpleITK Python; DICOM, NIfTI, multi-modal. Rigid/affine/deformable registration, threshold/region-growing segmentation, Gaussian/morph filtering, label stats, format conversion. Use to align volumes across timepoints/modalities, segment fluorescence, or convert DICOM→NIfTI. |
| license | Apache-2.0 |
SimpleITK Image Registration and Analysis
Overview
SimpleITK is a simplified, high-level interface to the Insight Toolkit (ITK) for medical image processing. It provides Python-native access to registration (rigid, affine, B-spline, Demons), segmentation (thresholding, region growing, watershed, level sets), filtering (smoothing, morphology, gradients), and resampling for 3D/4D images from MRI, CT, ultrasound, and fluorescence microscopy. SimpleITK images carry physical space metadata (spacing, origin, direction cosines) which is critical for correct anatomical interpretation and multi-modal alignment.
When to Use
- Registering MRI volumes across timepoints (longitudinal studies) or to a standard atlas for normalization
- Segmenting cells or nuclei from fluorescence microscopy using Otsu thresholding with morphological cleanup
- Converting DICOM series (CT, MRI scanner output) to NIfTI format for downstream analysis with FSL or ANTs
- Applying pre-computed transforms to resample images to a common resolution or field of view
- Computing region statistics (volume, mean intensity, surface area) from binary label masks
- Running multi-modal registration (e.g., aligning PET to MRI) using mutual information metrics
- Use ANTs (via
antspyx) instead when you need state-of-the-art diffeomorphic registration with multi-atlas label fusion for neuroimaging research; SimpleITK is better for Python-native scriptable pipelines without native dependencies
- Use scikit-image (
scikit-image-processing) instead for 2D bioimage analysis with regionprops, morphological operations, and watershed on non-volumetric fluorescence microscopy data
Prerequisites
- Python packages:
SimpleITK>=2.3, numpy, matplotlib
- Optional:
SimpleITK-SimpleElastix for additional registration algorithms (Elastix)
- Data requirements: DICOM series (CT/MRI), NIfTI files (.nii or .nii.gz), or any ITK-supported format (MetaImage, NRRD, PNG, TIFF stacks)
- Environment: Python 3.8+; no GPU required; 8 GB RAM recommended for typical 3D volumes
pip install SimpleITK numpy matplotlib
pip install SimpleITK-SimpleElastix
Quick Start
import SimpleITK as sitk
image = sitk.ReadImage("brain_t1.nii.gz")
print(f"Size: {image.GetSize()}, Spacing: {image.GetSpacing()}")
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.0)
mask = sitk.OtsuThreshold(smoothed, 0, 1, 200)
print(f"Voxels in mask: {sitk.GetArrayFromImage(mask).sum()}")
sitk.WriteImage(mask, "brain_mask.nii.gz")
print("Saved brain_mask.nii.gz")
Core API
Module 1: Image I/O
Reading and writing DICOM series, NIfTI, and other formats with full metadata preservation.
import SimpleITK as sitk
image = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
print(f"Size (x,y,z): {image.GetSize()}")
print(f"Spacing (mm): {image.GetSpacing()}")
print(f"Origin: {image.GetOrigin()}")
print(f"Direction: {image.GetDirection()}")
sitk.WriteImage(image, "output.nii.gz")
print("Saved output.nii.gz")
import SimpleITK as sitk
import os
dicom_dir = "DICOM/series_001/"
series_ids = sitk.ImageSeriesReader.GetGDCMSeriesIDs(dicom_dir)
print(f"Found {len(series_ids)} DICOM series")
reader = sitk.ImageSeriesReader()
reader.SetFileNames(sitk.ImageSeriesReader.GetGDCMSeriesFileNames(dicom_dir, series_ids[0]))
reader.MetaDataDictionaryArrayUpdateOn()
reader.LoadPrivateTagsOn()
volume = reader.Execute()
print(f"DICOM volume size: {volume.GetSize()}")
print(f"Pixel spacing: {volume.GetSpacing()}")
sitk.WriteImage(volume, "ct_volume.nii.gz")
print("DICOM series → ct_volume.nii.gz")
Module 2: Image Filtering
Gaussian smoothing, median filtering, gradient magnitude, and edge-preserving filters.
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("fluorescence_cells.nii.gz", sitk.sitkFloat32)
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.5)
median_filtered = sitk.Median(image, [3, 3, 3])
gradient = sitk.GradientMagnitude(smoothed)
arr = sitk.GetArrayFromImage(gradient)
print(f"Gradient range: {arr.min():.2f} – {arr.max():.2f}")
print(f"Mean gradient: {arr.mean():.4f}")
sitk.WriteImage(smoothed, "smoothed.nii.gz")
sitk.WriteImage(gradient, "gradient.nii.gz")
import SimpleITK as sitk
image = sitk.ReadImage("ct_volume.nii.gz", sitk.sitkFloat32)
rescaled = sitk.RescaleIntensity(image, outputMinimum=0.0, outputMaximum=1.0)
equalized = sitk.AdaptiveHistogramEqualization(rescaled)
image_f32 = sitk.Cast(image, sitk.sitkFloat32)
mask_otsu = sitk.OtsuThreshold(image_f32, 0, 1, 200)
corrected = sitk.N4BiasFieldCorrection(image_f32, mask_otsu)
print("Applied: rescaling, histogram equalization, N4 bias correction")
sitk.WriteImage(corrected, "bias_corrected.nii.gz")
Module 3: Image Registration
Rigid, affine, and deformable (B-spline, Demons) registration using ImageRegistrationMethod.
import SimpleITK as sitk
fixed = sitk.ReadImage("atlas_t1.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
registration_method = sitk.ImageRegistrationMethod()
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
registration_method.SetMetricSamplingStrategy(registration_method.RANDOM)
registration_method.SetMetricSamplingPercentage(0.01)
registration_method.SetOptimizerAsGradientDescent(
learningRate=1.0, numberOfIterations=100,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
registration_method.SetOptimizerScalesFromPhysicalShift()
registration_method.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2, 1])
registration_method.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1, 0])
registration_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
initial_transform = sitk.CenteredTransformInitializer(
fixed, moving,
sitk.Euler3DTransform(),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
registration_method.SetInitialTransform(initial_transform, inPlace=False)
registration_method.SetInterpolator(sitk.sitkLinear)
final_transform = registration_method.Execute(fixed, moving)
print(f"Optimizer stop: {registration_method.GetOptimizerStopConditionDescription()}")
print(f"Final metric: {registration_method.GetMetricValue():.4f}")
resampled = sitk.Resample(
moving, fixed, final_transform,
sitk.sitkLinear, , moving.GetPixelID()
)
sitk.WriteImage(resampled, )
sitk.WriteTransform(final_transform, )
()
import SimpleITK as sitk
fixed = sitk.ReadImage("atlas_t1.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
affine_method = sitk.ImageRegistrationMethod()
affine_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
affine_method.SetMetricSamplingStrategy(affine_method.RANDOM)
affine_method.SetMetricSamplingPercentage(0.01)
affine_method.SetOptimizerAsGradientDescent(
learningRate=1.0, numberOfIterations=100,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
affine_method.SetShrinkFactorsPerLevel([4, 2, 1])
affine_method.SetSmoothingSigmasPerLevel([2, 1, 0])
affine_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
affine_init = sitk.CenteredTransformInitializer(
fixed, moving, sitk.AffineTransform(3),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
affine_method.SetInitialTransform(affine_init, inPlace=False)
affine_method.SetInterpolator(sitk.sitkLinear)
affine_transform = affine_method.Execute(fixed, moving)
print(f"Affine complete: metric = {affine_method.GetMetricValue():.4f}")
bspline_method = sitk.ImageRegistrationMethod()
bspline_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
bspline_method.SetMetricSamplingStrategy(bspline_method.RANDOM)
bspline_method.SetMetricSamplingPercentage(0.01)
bspline_method.SetOptimizerAsGradientDescent(
learningRate=0.5, numberOfIterations=50,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
bspline_method.SetShrinkFactorsPerLevel([2, ])
bspline_method.SetSmoothingSigmasPerLevel([, ])
bspline_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
mesh_size = [] * fixed.GetDimension()
bspline_init = sitk.BSplineTransformInitializer(image1=fixed, transformDomainMeshSize=mesh_size, order=)
composite = sitk.CompositeTransform()
composite.AddTransform(affine_transform)
composite.AddTransform(bspline_init)
bspline_method.SetInitialTransform(composite, inPlace=)
bspline_method.SetInterpolator(sitk.sitkLinear)
deformable_transform = bspline_method.Execute(fixed, moving)
resampled = sitk.Resample(moving, fixed, deformable_transform, sitk.sitkLinear, )
sitk.WriteImage(resampled, )
()
Module 4: Segmentation
Otsu thresholding, region growing, watershed, and morphological post-processing.
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("cells_gfp.nii.gz", sitk.sitkFloat32)
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.0)
binary = sitk.OtsuThreshold(smoothed, insideValue=0, outsideValue=1, numberOfHistogramBins=200)
arr = sitk.GetArrayFromImage(binary)
n_foreground = arr.sum()
total = arr.size
print(f"Foreground: {n_foreground} voxels ({100*n_foreground/total:.1f}%)")
sitk.WriteImage(binary, "cells_binary_otsu.nii.gz")
print("Saved: cells_binary_otsu.nii.gz")
import SimpleITK as sitk
image = sitk.ReadImage("mri_brain.nii.gz", sitk.sitkFloat32)
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=0.5)
seed = (128, 128, 64)
lower_threshold = 50.0
upper_threshold = 200.0
region_grown = sitk.ConnectedThreshold(
smoothed,
seedList=[seed],
lower=lower_threshold,
upper=upper_threshold,
replaceValue=1
)
print(f"Region growing: {sitk.GetArrayFromImage(region_grown).sum()} voxels segmented")
confidence_seg = sitk.ConfidenceConnected(
smoothed,
seedList=[seed],
numberOfIterations=2,
multiplier=2.5,
initialNeighborhoodRadius=2,
replaceValue=1
)
print(f"Confidence connected: {sitk.GetArrayFromImage(confidence_seg).sum()} voxels")
sitk.WriteImage(region_grown, "region_grown.nii.gz")
sitk.WriteImage(confidence_seg, "confidence_seg.nii.gz")
import SimpleITK as sitk
binary = sitk.ReadImage("cells_binary_otsu.nii.gz")
filled = sitk.BinaryFillhole(binary)
eroded = sitk.BinaryErode(filled, kernelRadius=(2, 2, 1), kernelType=sitk.sitkBall)
dilated = sitk.BinaryDilate(eroded, kernelRadius=(2, 2, 1), kernelType=sitk.sitkBall)
opened = sitk.BinaryMorphologicalOpening(binary, kernelRadius=(1, 1, 1), kernelType=sitk.sitkBall)
labeled = sitk.ConnectedComponent(opened)
n_objects = sitk.GetArrayFromImage(labeled).max()
print(f"Connected components (objects): {n_objects}")
relabeled = sitk.RelabelComponent(labeled, minimumObjectSize=100)
n_kept = sitk.GetArrayFromImage(relabeled).max()
print(f"Objects remaining after size filter: {n_kept}")
sitk.WriteImage(relabeled, "cells_labeled.nii.gz")
Module 5: Resampling and Transform Application
Applying transforms, resampling to a reference grid, and converting between array and image representations.
import SimpleITK as sitk
import numpy as np
reference = sitk.ReadImage("reference_volume.nii.gz")
moving = sitk.ReadImage("subject_volume.nii.gz")
transform = sitk.ReadTransform("rigid_transform.tfm")
resampled = sitk.Resample(
moving,
reference,
transform,
sitk.sitkLinear,
0.0,
moving.GetPixelID()
)
print(f"Resampled size: {resampled.GetSize()} (matches reference: {reference.GetSize()})")
sitk.WriteImage(resampled, "resampled_to_reference.nii.gz")
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("ct_volume.nii.gz", sitk.sitkFloat32)
arr = sitk.GetArrayFromImage(image)
print(f"NumPy array shape (z,y,x): {arr.shape}")
print(f"Value range: {arr.min():.1f} – {arr.max():.1f}")
clipped = np.clip(arr, -1000, 3000)
normalized = (clipped - clipped.mean()) / clipped.std()
out_image = sitk.GetImageFromArray(normalized)
out_image.CopyInformation(image)
print(f"Output size: {out_image.GetSize()}, Spacing: {out_image.GetSpacing()}")
new_spacing = [1.0, 1.0, 1.0]
orig_spacing = image.GetSpacing()
orig_size = image.GetSize()
new_size = [
int(round(orig_size[i] * orig_spacing[i] / new_spacing[i]))
for i in range(3)
]
resampled_iso = sitk.Resample(
image,
new_size,
sitk.Transform(),
sitk.sitkLinear,
image.GetOrigin(),
new_spacing,
image.GetDirection(),
0.0,
image.GetPixelID()
)
print()
sitk.WriteImage(resampled_iso, )
Module 6: Statistics and Measurement
Label shape statistics (volume, surface area, centroid, bounding box) and intensity statistics per region.
import SimpleITK as sitk
import pandas as pd
intensity = sitk.ReadImage("fluorescence_cells.nii.gz", sitk.sitkFloat32)
labels = sitk.ReadImage("cells_labeled.nii.gz", sitk.sitkUInt32)
shape_filter = sitk.LabelShapeStatisticsImageFilter()
shape_filter.ComputeOrientedBoundingBoxOn()
shape_filter.Execute(labels)
label_ids = shape_filter.GetLabels()
print(f"Number of labeled objects: {len(label_ids)}")
rows = []
for lbl in label_ids:
rows.append({
"label": lbl,
"volume_voxels": shape_filter.GetNumberOfPixels(lbl),
"volume_mm3": shape_filter.GetPhysicalSize(lbl),
"centroid_x": shape_filter.GetCentroid(lbl)[0],
"centroid_y": shape_filter.GetCentroid(lbl)[1],
"centroid_z": shape_filter.GetCentroid(lbl)[2],
"elongation": shape_filter.GetElongation(lbl),
"roundness": shape_filter.GetRoundness(lbl),
})
shape_df = pd.DataFrame(rows)
print(shape_df.head())
print(f"\nMean volume: {shape_df['volume_mm3'].mean():.1f} mm³")
shape_df.to_csv("cell_shape_stats.csv", index=False)
print("Saved: cell_shape_stats.csv")
import SimpleITK as sitk
import pandas as pd
intensity = sitk.ReadImage("fluorescence_cells.nii.gz", sitk.sitkFloat32)
labels = sitk.ReadImage("cells_labeled.nii.gz", sitk.sitkUInt32)
intensity_filter = sitk.LabelIntensityStatisticsImageFilter()
intensity_filter.Execute(labels, intensity)
label_ids = intensity_filter.GetLabels()
rows = []
for lbl in label_ids:
rows.append({
"label": lbl,
"mean_intensity": intensity_filter.GetMean(lbl),
"std_intensity": intensity_filter.GetSigma(lbl),
"min_intensity": intensity_filter.GetMinimum(lbl),
"max_intensity": intensity_filter.GetMaximum(lbl),
"median_intensity": intensity_filter.GetMedian(lbl),
"sum_intensity": intensity_filter.GetSum(lbl),
})
intensity_df = pd.DataFrame(rows)
print(intensity_df.describe().round(2))
intensity_df.to_csv("cell_intensity_stats.csv", index=False)
print(f"\nSaved: cell_intensity_stats.csv ({len(rows)} cells)")
Key Concepts
Physical Space vs. Pixel Space
SimpleITK images store spatial metadata (spacing in mm, origin, direction cosines) that defines how pixel coordinates map to physical (world) coordinates. Operations like Resample and registration work in physical space, ensuring anatomically correct alignment even when images have different voxel sizes or orientations. Always use CopyInformation() when creating output images from NumPy arrays to preserve physical metadata.
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("mri.nii.gz")
print(f"Pixel space size: {image.GetSize()}")
print(f"Physical spacing mm: {image.GetSpacing()}")
print(f"Physical origin mm: {image.GetOrigin()}")
print(f"Direction cosines: {image.GetDirection()}")
pixel_idx = (64, 64, 32)
physical_pt = image.TransformIndexToPhysicalPoint(pixel_idx)
print(f"Pixel {pixel_idx} → physical {physical_pt}")
arr = sitk.GetArrayFromImage(image)
print(f"NumPy shape: {arr.shape}")
Transform Composition
SimpleITK transforms can be composed using CompositeTransform to apply a sequence of transformations (e.g., affine pre-alignment followed by deformable B-spline refinement). Transforms are applied in the order they were added when calling Resample, enabling modular pipeline construction.
Common Workflows
Workflow 1: Fluorescence Microscopy Cell Segmentation
Goal: Segment individual cells from a 3D fluorescence microscopy volume, apply morphological cleanup, and extract per-cell measurements.
import SimpleITK as sitk
import pandas as pd
import numpy as np
image = sitk.ReadImage("cells_gfp.nii.gz", sitk.sitkFloat32)
print(f"Image size: {image.GetSize()}, Spacing: {image.GetSpacing()}")
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=0.8)
binary = sitk.OtsuThreshold(smoothed, insideValue=0, outsideValue=1, numberOfHistogramBins=200)
n_fg = sitk.GetArrayFromImage(binary).sum()
print(f"Foreground voxels after Otsu: {n_fg}")
filled = sitk.BinaryFillhole(binary)
opened = sitk.BinaryMorphologicalOpening(filled, kernelRadius=(1, 1, 1))
labeled = sitk.ConnectedComponent(opened)
relabeled = sitk.RelabelComponent(labeled, minimumObjectSize=200)
n_cells = int(sitk.GetArrayFromImage(relabeled).max())
print(f"Detected cells (>200 voxels): {n_cells}")
shape_filter = sitk.LabelShapeStatisticsImageFilter()
shape_filter.Execute(relabeled)
intensity_filter = sitk.LabelIntensityStatisticsImageFilter()
intensity_filter.Execute(relabeled, image)
rows = []
for lbl in shape_filter.GetLabels():
rows.append({
"cell_id": lbl,
"volume_mm3": shape_filter.GetPhysicalSize(lbl),
"roundness": shape_filter.GetRoundness(lbl),
: shape_filter.GetElongation(lbl),
: intensity_filter.GetMean(lbl),
: intensity_filter.GetSum(lbl),
})
df = pd.DataFrame(rows)
(df.describe().())
sitk.WriteImage(relabeled, )
df.to_csv(, index=)
()
Workflow 2: MRI Brain Atlas Registration
Goal: Register a subject's T1 MRI to a standard brain atlas using rigid + affine registration, then transfer atlas labels to subject space.
import SimpleITK as sitk
import numpy as np
atlas_t1 = sitk.ReadImage("mni152_t1.nii.gz", sitk.sitkFloat32)
subject_t1 = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
atlas_labels = sitk.ReadImage("mni152_labels.nii.gz", sitk.sitkUInt16)
print(f"Atlas size: {atlas_t1.GetSize()}, Subject size: {subject_t1.GetSize()}")
atlas_norm = sitk.RescaleIntensity(atlas_t1, 0.0, 1.0)
subject_norm = sitk.RescaleIntensity(subject_t1, 0.0, 1.0)
initial_transform = sitk.CenteredTransformInitializer(
atlas_norm, subject_norm,
sitk.AffineTransform(3),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
reg = sitk.ImageRegistrationMethod()
reg.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
reg.SetMetricSamplingStrategy(reg.RANDOM)
reg.SetMetricSamplingPercentage(0.01)
reg.SetOptimizerAsGradientDescent(
learningRate=1.0, numberOfIterations=200,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
reg.SetOptimizerScalesFromPhysicalShift()
reg.SetShrinkFactorsPerLevel([4, 2, 1])
reg.SetSmoothingSigmasPerLevel([2, 1, 0])
reg.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
reg.SetInitialTransform(initial_transform, inPlace=False)
reg.SetInterpolator(sitk.sitkLinear)
final_transform = reg.Execute(atlas_norm, subject_norm)
print(f"Registration metric: ")
()
subject_registered = sitk.Resample(
subject_t1, atlas_t1, final_transform,
sitk.sitkLinear, , subject_t1.GetPixelID()
)
inverse_transform = final_transform.GetInverse()
labels_in_subject_space = sitk.Resample(
atlas_labels, subject_t1, inverse_transform,
sitk.sitkNearestNeighbor, , atlas_labels.GetPixelID()
)
sitk.WriteImage(subject_registered, )
sitk.WriteImage(labels_in_subject_space, )
sitk.WriteTransform(final_transform, )
n_labels = (sitk.GetArrayFromImage(labels_in_subject_space).())
()
()
Workflow 3: DICOM Series to NIfTI Batch Conversion
Goal: Convert multiple DICOM series from a scanner directory to NIfTI with isotropic resampling.
import SimpleITK as sitk
from pathlib import Path
def convert_dicom_series(dicom_dir: str, output_path: str, target_spacing_mm: float = 1.0) -> dict:
"""Convert a DICOM series directory to NIfTI with optional isotropic resampling."""
series_ids = sitk.ImageSeriesReader.GetGDCMSeriesIDs(dicom_dir)
if not series_ids:
return {"error": f"No DICOM series in {dicom_dir}"}
reader = sitk.ImageSeriesReader()
reader.SetFileNames(sitk.ImageSeriesReader.GetGDCMSeriesFileNames(dicom_dir, series_ids[0]))
volume = reader.Execute()
orig_size = volume.GetSize()
orig_spacing = volume.GetSpacing()
new_spacing = [target_spacing_mm] * 3
new_size = [int(round(orig_size[i] * orig_spacing[i] / target_spacing_mm)) for i in range(3)]
resampled = sitk.Resample(
volume, new_size, sitk.Transform(),
sitk.sitkLinear, volume.GetOrigin(),
new_spacing, volume.GetDirection(),
0.0, volume.GetPixelID()
)
sitk.WriteImage(resampled, output_path)
return {
"input": dicom_dir, "output": output_path,
"orig_size": orig_size, "orig_spacing": orig_spacing,
"new_size": new_size, "n_series": len(series_ids)
}
input_root = Path("DICOM_data/")
output_root = Path()
output_root.mkdir(exist_ok=)
results = []
subject_dir (input_root.iterdir()):
subject_dir.is_dir():
out_file = output_root /
info = convert_dicom_series((subject_dir), (out_file), target_spacing_mm=)
results.append(info)
()
()
Key Parameters
| Parameter | Module | Default | Range / Options | Effect |
|---|
sigma | SmoothingRecursiveGaussian | — | 0.5–5.0 mm | Gaussian smoothing width; larger values blur more and reduce noise |
numberOfHistogramBins | OtsuThreshold, MutualInfo | 128 | 50–256 | Histogram resolution for threshold/metric calculation |
numberOfIterations | ImageRegistrationMethod | 100 | 50–500 | Max optimizer steps; increase for complex/fine registration |
learningRate | GradientDescent optimizer | 1.0 | 0.01–2.0 | Step size per optimizer iteration; too large causes divergence |
shrinkFactors | Multi-resolution pyramid | [4,2,1] | lists of [8,4,2,1] | Image downsampling per resolution level; larger = faster but coarser |
minimumObjectSize | RelabelComponent | 0 | 1–10000 voxels | Remove connected components smaller than this size in voxels |
kernelRadius | BinaryErode/Dilate | (1,1,1) | (1,1,1) – (5,5,5) | Morphological kernel radius per axis in voxels |
interpolator | Resample | sitk.sitkLinear | sitkLinear, sitkNearestNeighbor, sitkBSpline | Interpolation method; use NearestNeighbor for integer label images |
multiplier | ConfidenceConnected | 2.5 |
Best Practices
-
Always cast to float32 before registration or filtering: Many ITK filters expect float input. Integers from DICOM (typically sitk.sitkInt16) can cause silent errors or precision loss.
image = sitk.ReadImage("ct.nii.gz", sitk.sitkFloat32)
-
Use CopyInformation() when creating images from NumPy arrays: Without this, the output image loses physical spacing and origin, making it impossible to overlay with the source in a viewer.
arr = sitk.GetArrayFromImage(image)
modified = some_numpy_operation(arr)
out = sitk.GetImageFromArray(modified)
out.CopyInformation(image)
-
Use sitkNearestNeighbor for label/mask resampling: Linear or B-spline interpolation on integer label images creates fractional label values that corrupt the mask. Always use nearest-neighbor for binary masks and multi-label segmentations.
-
Run multi-resolution registration for speed and robustness: Set SetShrinkFactorsPerLevel([4,2,1]) and SetSmoothingSigmasPerLevel([2,1,0]) to prevent the optimizer from getting trapped in local minima on large 3D volumes.
-
Validate registration visually with checkerboard comparison: Before trusting a registration result, verify alignment using sitk.CheckerBoard between fixed and registered-moving.
checker = sitk.CheckerBoard(fixed, resampled_moving, [5, 5, 5])
sitk.WriteImage(checker, "registration_checker.nii.gz")
Common Recipes
Recipe: Multi-Modal Registration (MRI to CT)
When to use: Aligning different imaging modalities (e.g., PET to MRI, CT to MRI) where intensities are incompatible, requiring mutual information as metric.
import SimpleITK as sitk
ct = sitk.ReadImage("patient_ct.nii.gz", sitk.sitkFloat32)
mri = sitk.ReadImage("patient_mri.nii.gz", sitk.sitkFloat32)
reg = sitk.ImageRegistrationMethod()
reg.SetMetricAsMattesMutualInformation(numberOfHistogramBins=100)
reg.SetMetricSamplingStrategy(reg.RANDOM)
reg.SetMetricSamplingPercentage(0.05)
reg.SetOptimizerAsGradientDescent(
learningRate=1.0, numberOfIterations=150,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
reg.SetOptimizerScalesFromPhysicalShift()
reg.SetShrinkFactorsPerLevel([4, 2, 1])
reg.SetSmoothingSigmasPerLevel([2, 1, 0])
reg.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
initial = sitk.CenteredTransformInitializer(
ct, mri, sitk.AffineTransform(3),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
reg.SetInitialTransform(initial, inPlace=False)
reg.SetInterpolator(sitk.sitkLinear)
transform = reg.Execute(ct, mri)
mri_aligned = sitk.Resample(mri, ct, transform, sitk.sitkLinear, 0.0)
sitk.WriteImage(mri_aligned, "mri_aligned_to_ct.nii.gz")
print(f"Multi-modal registration complete. Metric: {reg.GetMetricValue():.4f}")
Recipe: Batch Apply Transform to Label Masks
When to use: After registering one image, apply the same transform to segmentation masks or atlas labels (using nearest-neighbor interpolation).
import SimpleITK as sitk
from pathlib import Path
transform = sitk.ReadTransform("subject_to_atlas.tfm")
reference = sitk.ReadImage("atlas_t1.nii.gz")
label_dir = Path("subject_labels/")
output_dir = Path("atlas_labels/")
output_dir.mkdir(exist_ok=True)
for label_file in sorted(label_dir.glob("*.nii.gz")):
label = sitk.ReadImage(str(label_file), sitk.sitkUInt16)
registered_label = sitk.Resample(
label, reference, transform,
sitk.sitkNearestNeighbor,
0, label.GetPixelID()
)
out_path = output_dir / label_file.name
sitk.WriteImage(registered_label, str(out_path))
n_labels = int(sitk.GetArrayFromImage(registered_label).max())
print(f" {label_file.name}: {n_labels} labels → {out_path.name}")
print(f"Batch transform applied to {len(list(label_dir.glob('*.nii.gz')))} files")
Recipe: Demons Deformable Registration
When to use: Fast deformable registration for images of the same modality with small deformations (e.g., longitudinal brain MRI with slight atrophy).
import SimpleITK as sitk
fixed = sitk.ReadImage("baseline_mri.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("followup_mri.nii.gz", sitk.sitkFloat32)
fixed_n = sitk.RescaleIntensity(fixed, 0.0, 1.0)
moving_n = sitk.RescaleIntensity(moving, 0.0, 1.0)
demons = sitk.DemonsRegistrationFilter()
demons.SetNumberOfIterations(50)
demons.SetStandardDeviations(1.0)
displacement_field = demons.Execute(fixed_n, moving_n)
print(f"Demons complete: RMS change = {demons.GetRMSChange():.6f}")
displacement_transform = sitk.DisplacementFieldTransform(displacement_field)
warped = sitk.Resample(moving, fixed, displacement_transform, sitk.sitkLinear, 0.0)
sitk.WriteImage(warped, "followup_registered_demons.nii.gz")
sitk.WriteImage(
sitk.Cast(sitk.VectorMagnitude(displacement_field), sitk.sitkFloat32),
"deformation_magnitude.nii.gz"
)
print("Saved: followup_registered_demons.nii.gz, deformation_magnitude.nii.gz")
Troubleshooting
| Problem | Cause | Solution |
|---|
TypeError: in method 'ImageRegistrationMethod_Execute' | Image pixel type is not float | Cast to float32 before registration: sitk.Cast(image, sitk.sitkFloat32) |
| Registration diverges (metric increases) | Learning rate too high or images not pre-aligned | Reduce learningRate to 0.1; use CenteredTransformInitializer for initial alignment; check that images overlap |
| Label image has fractional values after resample | Wrong interpolator used for mask | Set interpolator to sitk.sitkNearestNeighbor when resampling integer label images |
RuntimeError: Exception thrown in SimpleITK ReadImage | Unsupported format or corrupt DICOM | Verify file with dcmdump; for DICOM series use ImageSeriesReader.GetGDCMSeriesFileNames() |
| N4 bias correction very slow | Large image or too many iterations | Downsample first: resample to 2mm iso before correction, then apply transform to original-resolution image |
| Connected component labels too many or too few | Object size threshold wrong or insufficient preprocessing | Adjust minimumObjectSize in RelabelComponent; increase Gaussian sigma to merge touching objects |
| NumPy array shape does not match expected (z,y,x) | Axis ordering confusion | SimpleITK images are (x,y,z); GetArrayFromImage() returns (z,y,x) NumPy arrays; use arr.transpose(2,1,0) to convert |
| Otsu threshold segments too much background | Background is bright (e.g., confocal reflection artifacts) | Use sitk.OtsuMultipleThresholds with numberOfThresholds=2; or apply a foreground mask before thresholding |
Related Skills
- cellpose-cell-segmentation — deep learning cell segmentation from fluorescence microscopy; use when classical thresholding fails on heterogeneous staining or touching cells
- scikit-image-processing — 2D image analysis with
regionprops, watershed, and filters; prefer for non-volumetric 2D microscopy analysis
- napari-image-viewer — interactive visualization of 3D volumes and label masks; use alongside SimpleITK for visual inspection of registration and segmentation results
- pydicom-medical-imaging — direct DICOM tag access, anonymization, and metadata editing; use alongside SimpleITK which handles volume reconstruction but not PHI management
- nnunet-segmentation — automated deep learning segmentation for medical images; use when SimpleITK classical segmentation is insufficient for complex anatomical structures
References