Back to skills
SKILL.md
Simpleitk Image Registration
ASecurityRegister, 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.
- 368 stars
- 0 votes
- 0 copies
- 2 views
- Added May 29, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add jaechang-hits/SciAgent-Skills --skill simpleitk-image-registration --agent claude-codeAre you the author of Simpleitk Image Registration?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jaechang-hits-simpleitk-image-registration)---
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
```bash
pip install SimpleITK numpy matplotlib
# For additional Elastix-based registration algorithms:
pip install SimpleITK-SimpleElastix
```
## Quick Start
```python
import SimpleITK as sitk
# Read a NIfTI file, apply Gaussian smoothing, and save
image = sitk.ReadImage("brain_t1.nii.gz")
print(f"Size: {image.GetSize()}, Spacing: {image.GetSpacing()}")
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.0)
# Otsu threshold to create a brain mask
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.
```python
import SimpleITK as sitk
# Read a NIfTI file
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()}")
# Write as compressed NIfTI
sitk.WriteImage(image, "output.nii.gz")
print("Saved output.nii.gz")
```
```python
import SimpleITK as sitk
import os
# Read a DICOM series from a directory
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()}")
# Save the 3D volume as NIfTI
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.
```python
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("fluorescence_cells.nii.gz", sitk.sitkFloat32)
# Gaussian smoothing — reduces noise before segmentation
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.5)
# Median filter — removes salt-and-pepper noise (preserves edges better than Gaussian)
median_filtered = sitk.Median(image, [3, 3, 3])
# Gradient magnitude — highlights edges/boundaries
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")
```
```python
import SimpleITK as sitk
image = sitk.ReadImage("ct_volume.nii.gz", sitk.sitkFloat32)
# Normalize intensity to [0, 1] range using RescaleIntensity
rescaled = sitk.RescaleIntensity(image, outputMinimum=0.0, outputMaximum=1.0)
# Histogram equalization — improves contrast for registration
equalized = sitk.AdaptiveHistogramEqualization(rescaled)
# N4 bias field correction for MRI (removes B1 field inhomogeneity)
# Cast to float32 for bias correction
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`.
```python
import SimpleITK as sitk
# Load fixed (reference/atlas) and moving (subject to align) images
fixed = sitk.ReadImage("atlas_t1.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
# Set up rigid registration
registration_method = sitk.ImageRegistrationMethod()
# Similarity metric: Mattes mutual information (works for same-modality)
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
registration_method.SetMetricSamplingStrategy(registration_method.RANDOM)
registration_method.SetMetricSamplingPercentage(0.01)
# Optimizer: gradient descent with line search
registration_method.SetOptimizerAsGradientDescent(
learningRate=1.0, numberOfIterations=100,
convergenceMinimumValue=1e-6, convergenceWindowSize=10
)
registration_method.SetOptimizerScalesFromPhysicalShift()
# Multi-resolution pyramid: 3 levels → faster convergence
registration_method.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2, 1])
registration_method.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1, 0])
registration_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
# Initialize with center of geometry
initial_transform = sitk.CenteredTransformInitializer(
fixed, moving,
sitk.Euler3DTransform(),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
registration_method.SetInitialTransform(initial_transform, inPlace=False)
registration_method.SetInterpolator(sitk.sitkLinear)
# Execute registration
final_transform = registration_method.Execute(fixed, moving)
print(f"Optimizer stop: {registration_method.GetOptimizerStopConditionDescription()}")
print(f"Final metric: {registration_method.GetMetricValue():.4f}")
# Apply transform and save
resampled = sitk.Resample(
moving, fixed, final_transform,
sitk.sitkLinear, 0.0, moving.GetPixelID()
)
sitk.WriteImage(resampled, "subject_registered.nii.gz")
sitk.WriteTransform(final_transform, "rigid_transform.tfm")
print("Saved: subject_registered.nii.gz, rigid_transform.tfm")
```
```python
import SimpleITK as sitk
# Deformable B-spline registration for non-linear alignment
fixed = sitk.ReadImage("atlas_t1.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("subject_t1.nii.gz", sitk.sitkFloat32)
# Start with affine pre-registration
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}")
# B-spline deformable refinement
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, 1])
bspline_method.SetSmoothingSigmasPerLevel([1, 0])
bspline_method.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
mesh_size = [8] * fixed.GetDimension()
bspline_init = sitk.BSplineTransformInitializer(image1=fixed, transformDomainMeshSize=mesh_size, order=3)
composite = sitk.CompositeTransform(3)
composite.AddTransform(affine_transform)
composite.AddTransform(bspline_init)
bspline_method.SetInitialTransform(composite, inPlace=True)
bspline_method.SetInterpolator(sitk.sitkLinear)
deformable_transform = bspline_method.Execute(fixed, moving)
resampled = sitk.Resample(moving, fixed, deformable_transform, sitk.sitkLinear, 0.0)
sitk.WriteImage(resampled, "subject_deformable_registered.nii.gz")
print("Saved: subject_deformable_registered.nii.gz")
```
### Module 4: Segmentation
Otsu thresholding, region growing, watershed, and morphological post-processing.
```python
import SimpleITK as sitk
import numpy as np
# Read fluorescence microscopy image
image = sitk.ReadImage("cells_gfp.nii.gz", sitk.sitkFloat32)
# Gaussian smoothing before thresholding
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=1.0)
# Otsu thresholding: automatically finds optimal foreground/background threshold
# Returns binary label image: 1=foreground (cells), 0=background
binary = sitk.OtsuThreshold(smoothed, insideValue=0, outsideValue=1, numberOfHistogramBins=200)
# Count segmented voxels
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")
```
```python
import SimpleITK as sitk
image = sitk.ReadImage("mri_brain.nii.gz", sitk.sitkFloat32)
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=0.5)
# Region growing from a seed point: ConnectedThreshold
# Seeds must be inside the region of interest; lower/upper bound in image intensity units
seed = (128, 128, 64) # (x, y, z) in image coordinates
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")
# ConfidenceConnected: adaptive thresholding based on neighborhood statistics
confidence_seg = sitk.ConfidenceConnected(
smoothed,
seedList=[seed],
numberOfIterations=2,
multiplier=2.5, # number of standard deviations from mean
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")
```
```python
import SimpleITK as sitk
# Morphological operations for binary mask cleanup
binary = sitk.ReadImage("cells_binary_otsu.nii.gz")
# Fill small holes
filled = sitk.BinaryFillhole(binary)
# Erosion: remove thin protrusions and separate touching objects
eroded = sitk.BinaryErode(filled, kernelRadius=(2, 2, 1), kernelType=sitk.sitkBall)
# Dilation: restore true boundary after erosion
dilated = sitk.BinaryDilate(eroded, kernelRadius=(2, 2, 1), kernelType=sitk.sitkBall)
# Opening = erosion + dilation: removes small objects
opened = sitk.BinaryMorphologicalOpening(binary, kernelRadius=(1, 1, 1), kernelType=sitk.sitkBall)
# Connected component labeling: assign unique integer to each object
labeled = sitk.ConnectedComponent(opened)
n_objects = sitk.GetArrayFromImage(labeled).max()
print(f"Connected components (objects): {n_objects}")
# Remove objects smaller than 100 voxels
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.
```python
import SimpleITK as sitk
import numpy as np
# Resample moving image to match reference grid
reference = sitk.ReadImage("reference_volume.nii.gz")
moving = sitk.ReadImage("subject_volume.nii.gz")
# Load a pre-computed transform
transform = sitk.ReadTransform("rigid_transform.tfm")
# Resample with linear interpolation (use nearest neighbor for label images)
resampled = sitk.Resample(
moving,
reference,
transform,
sitk.sitkLinear, # interpolator; use sitk.sitkNearestNeighbor for labels
0.0, # default pixel value for regions outside the moving image
moving.GetPixelID()
)
print(f"Resampled size: {resampled.GetSize()} (matches reference: {reference.GetSize()})")
sitk.WriteImage(resampled, "resampled_to_reference.nii.gz")
```
```python
import SimpleITK as sitk
import numpy as np
# Convert between SimpleITK image and NumPy array
image = sitk.ReadImage("ct_volume.nii.gz", sitk.sitkFloat32)
# SimpleITK uses (x, y, z) ordering; NumPy gets (z, y, x) ordering
arr = sitk.GetArrayFromImage(image)
print(f"NumPy array shape (z,y,x): {arr.shape}")
print(f"Value range: {arr.min():.1f} – {arr.max():.1f}")
# Apply NumPy operations
clipped = np.clip(arr, -1000, 3000) # CT Hounsfield unit clipping
normalized = (clipped - clipped.mean()) / clipped.std()
# Convert back to SimpleITK image (preserving spatial metadata)
out_image = sitk.GetImageFromArray(normalized)
out_image.CopyInformation(image) # copy spacing, origin, direction from original
print(f"Output size: {out_image.GetSize()}, Spacing: {out_image.GetSpacing()}")
# Resample to isotropic 1mm spacing
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(), # identity transform
sitk.sitkLinear,
image.GetOrigin(),
new_spacing,
image.GetDirection(),
0.0,
image.GetPixelID()
)
print(f"Isotropic resampled size: {resampled_iso.GetSize()}")
sitk.WriteImage(resampled_iso, "isotropic_1mm.nii.gz")
```
### Module 6: Statistics and Measurement
Label shape statistics (volume, surface area, centroid, bounding box) and intensity statistics per region.
```python
import SimpleITK as sitk
import pandas as pd
# Load intensity image and binary label mask
intensity = sitk.ReadImage("fluorescence_cells.nii.gz", sitk.sitkFloat32)
labels = sitk.ReadImage("cells_labeled.nii.gz", sitk.sitkUInt32)
# Shape statistics: geometric measurements per label
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")
```
```python
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 statistics per label
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.
```python
import SimpleITK as sitk
import numpy as np
image = sitk.ReadImage("mri.nii.gz")
print(f"Pixel space size: {image.GetSize()}") # (x, y, z) in voxels
print(f"Physical spacing mm: {image.GetSpacing()}") # mm per voxel
print(f"Physical origin mm: {image.GetOrigin()}") # world coords of first voxel
print(f"Direction cosines: {image.GetDirection()}") # patient orientation matrix
# Convert pixel index to physical point
pixel_idx = (64, 64, 32)
physical_pt = image.TransformIndexToPhysicalPoint(pixel_idx)
print(f"Pixel {pixel_idx} → physical {physical_pt}")
# NumPy array has REVERSED axis order: (z, y, x)
arr = sitk.GetArrayFromImage(image)
print(f"NumPy shape: {arr.shape}") # (z, y, x)
```
### 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.
```python
import SimpleITK as sitk
import pandas as pd
import numpy as np
# 1. Load fluorescence volume
image = sitk.ReadImage("cells_gfp.nii.gz", sitk.sitkFloat32)
print(f"Image size: {image.GetSize()}, Spacing: {image.GetSpacing()}")
# 2. Denoise with Gaussian smoothing
smoothed = sitk.SmoothingRecursiveGaussian(image, sigma=0.8)
# 3. Otsu threshold to get binary foreground mask
binary = sitk.OtsuThreshold(smoothed, insideValue=0, outsideValue=1, numberOfHistogramBins=200)
n_fg = sitk.GetArrayFromImage(binary).sum()
print(f"Foreground voxels after Otsu: {n_fg}")
# 4. Morphological cleanup
filled = sitk.BinaryFillhole(binary)
opened = sitk.BinaryMorphologicalOpening(filled, kernelRadius=(1, 1, 1))
# 5. Label individual connected components (cells)
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}")
# 6. Compute shape statistics
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),
"elongation": shape_filter.GetElongation(lbl),
"mean_gfp_intensity": intensity_filter.GetMean(lbl),
"total_gfp_intensity": intensity_filter.GetSum(lbl),
})
df = pd.DataFrame(rows)
print(df.describe().round(3))
# 7. Save outputs
sitk.WriteImage(relabeled, "cells_labeled.nii.gz")
df.to_csv("cell_measurements.csv", index=False)
print(f"\nSaved: cells_labeled.nii.gz, cell_measurements.csv ({n_cells} cells)")
```
### 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.
```python
import SimpleITK as sitk
import numpy as np
# 1. Load atlas (fixed) and subject (moving) T1 MRI
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()}")
# 2. Normalize intensities for better registration convergence
atlas_norm = sitk.RescaleIntensity(atlas_t1, 0.0, 1.0)
subject_norm = sitk.RescaleIntensity(subject_t1, 0.0, 1.0)
# 3. Initialize with center-of-geometry alignment
initial_transform = sitk.CenteredTransformInitializer(
atlas_norm, subject_norm,
sitk.AffineTransform(3),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
# 4. Configure and run affine registration
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: {reg.GetMetricValue():.4f}")
print(f"Optimizer: {reg.GetOptimizerStopConditionDescription()}")
# 5. Apply transform to subject T1 (aligned to atlas space)
subject_registered = sitk.Resample(
subject_t1, atlas_t1, final_transform,
sitk.sitkLinear, 0.0, subject_t1.GetPixelID()
)
# 6. Invert transform to bring atlas labels to subject space
inverse_transform = final_transform.GetInverse()
labels_in_subject_space = sitk.Resample(
atlas_labels, subject_t1, inverse_transform,
sitk.sitkNearestNeighbor, 0, atlas_labels.GetPixelID()
)
# 7. Save results
sitk.WriteImage(subject_registered, "subject_in_atlas_space.nii.gz")
sitk.WriteImage(labels_in_subject_space, "atlas_labels_in_subject_space.nii.gz")
sitk.WriteTransform(final_transform, "subject_to_atlas.tfm")
n_labels = int(sitk.GetArrayFromImage(labels_in_subject_space).max())
print(f"Transferred {n_labels} atlas regions to subject space")
print("Saved: subject_in_atlas_space.nii.gz, atlas_labels_in_subject_space.nii.gz")
```
### Workflow 3: DICOM Series to NIfTI Batch Conversion
**Goal**: Convert multiple DICOM series from a scanner directory to NIfTI with isotropic resampling.
```python
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()
# Resample to isotropic voxels
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)
}
# Batch convert all subject directories
input_root = Path("DICOM_data/")
output_root = Path("NIfTI_converted/")
output_root.mkdir(exist_ok=True)
results = []
for subject_dir in sorted(input_root.iterdir()):
if subject_dir.is_dir():
out_file = output_root / f"{subject_dir.name}_t1.nii.gz"
info = convert_dicom_series(str(subject_dir), str(out_file), target_spacing_mm=1.0)
results.append(info)
print(f" {subject_dir.name}: {info.get('orig_size')} → {info.get('new_size')}")
print(f"\nConverted {len(results)} subjects to {output_root}/")
```
## 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` | `1.5`–`4.0` | Number of std devs from neighborhood mean to accept a voxel |
| `meshSize` | BSplineTransformInitializer | `[8]*ndim` | `[4]*ndim` – `[20]*ndim` | B-spline control point grid; larger = more deformation degrees of freedom |
## Best Practices
1. **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.
```python
image = sitk.ReadImage("ct.nii.gz", sitk.sitkFloat32) # explicit cast at load time
# Or: image_f32 = sitk.Cast(image, sitk.sitkFloat32)
```
2. **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.
```python
arr = sitk.GetArrayFromImage(image)
modified = some_numpy_operation(arr)
out = sitk.GetImageFromArray(modified)
out.CopyInformation(image) # preserve spacing, origin, direction
```
3. **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.
4. **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.
5. **Validate registration visually with checkerboard comparison**: Before trusting a registration result, verify alignment using `sitk.CheckerBoard` between fixed and registered-moving.
```python
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.
```python
import SimpleITK as sitk
# Load images from different modalities
ct = sitk.ReadImage("patient_ct.nii.gz", sitk.sitkFloat32)
mri = sitk.ReadImage("patient_mri.nii.gz", sitk.sitkFloat32)
# Mutual information metric handles multi-modal intensity relationships
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).
```python
import SimpleITK as sitk
from pathlib import Path
# Load the transform computed during registration
transform = sitk.ReadTransform("subject_to_atlas.tfm")
reference = sitk.ReadImage("atlas_t1.nii.gz")
# Apply to all label files in a directory
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, # critical: integer labels need nearest-neighbor
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).
```python
import SimpleITK as sitk
fixed = sitk.ReadImage("baseline_mri.nii.gz", sitk.sitkFloat32)
moving = sitk.ReadImage("followup_mri.nii.gz", sitk.sitkFloat32)
# Normalize intensities
fixed_n = sitk.RescaleIntensity(fixed, 0.0, 1.0)
moving_n = sitk.RescaleIntensity(moving, 0.0, 1.0)
# Demons registration filter
demons = sitk.DemonsRegistrationFilter()
demons.SetNumberOfIterations(50)
demons.SetStandardDeviations(1.0) # displacement field smoothing
displacement_field = demons.Execute(fixed_n, moving_n)
print(f"Demons complete: RMS change = {demons.GetRMSChange():.6f}")
# Apply displacement field transform
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
- [SimpleITK documentation](https://simpleitk.readthedocs.io/en/master/) — complete API reference and Jupyter notebook tutorials
- [SimpleITK GitHub](https://github.com/SimpleITK/SimpleITK) — source code, examples, and issue tracker
- [SimpleITK Notebooks](https://github.com/InsightSoftwareConsortium/SimpleITK-Notebooks) — 50+ Jupyter notebooks covering registration, segmentation, and I/O
- [Lowekamp BC et al. (2013) Front Neuroinform](https://doi.org/10.3389/fninf.2013.00045) — SimpleITK original paper describing design and capabilities
- [Yaniv Z et al. (2018) Frontiers in Neuroinformatics](https://doi.org/10.3389/fninf.2018.00008) — SimpleITK for the biomedical image processing community
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.