Normalize intensities of MR image modalities
Project description
Intensity Normalization
A modern Python package for normalizing MR image intensities.
Features
- 🔧 Multiple Image Format Support: Works with numpy arrays and nibabel images (.nii, .nii.gz, .mgz, .mnc, etc.)
- 📊 6 Normalization Methods: FCM, KDE, WhiteStripe, Z-score, Nyúl, LSQ
- ⚡ High Performance: Optimized implementations
Installation
pip install intensity-normalization
Or via conda:
conda install conda-forge::intensity-normalization
Quick Start
High-Level API
import numpy as np
import nibabel as nib
from intensity_normalization import normalize_image
# Load MRI image and brain mask
img = nib.load("brain_t1.nii.gz")
mask = nib.load("brain_mask.nii.gz")
# Normalize using FCM (default, recommended for T1)
normalized = normalize_image(img, method="fcm", mask=mask)
# Different methods for different modalities
t2_normalized = normalize_image(img, method="kde", modality="t2")
zscore_normalized = normalize_image(img, method="zscore")
Object-Oriented API
from intensity_normalization import FCMNormalizer, ZScoreNormalizer
import numpy as np
# Create synthetic brain data
brain_data = np.random.normal(1000, 200, (64, 64, 32)) # WM ~1000
brain_data[20:40, 20:40, 10:20] = np.random.normal(600, 100, (20, 20, 10)) # GM ~600
# FCM normalization (tissue-based)
fcm = FCMNormalizer(tissue_type="wm") # white matter reference
normalized = fcm.fit_transform(brain_data)
# Z-score normalization
zscore = ZScoreNormalizer()
standardized = zscore.fit_transform(brain_data)
print(f"Original mean: {brain_data[brain_data > 0].mean():.1f}")
print(f"FCM normalized WM mean: {normalized[40:60, 40:60, 10:20].mean():.2f}")
print(f"Z-score mean: {standardized[brain_data > 0].mean():.2f}")
Population-Based Methods
from intensity_normalization import NyulNormalizer, LSQNormalizer
from intensity_normalization.adapters import create_image
# Load multiple subjects
image_paths = ["subject1_t1.nii.gz", "subject2_t1.nii.gz", "subject3_t1.nii.gz"]
images = [create_image(path) for path in image_paths]
# Nyúl histogram matching
nyul = NyulNormalizer(output_min_value=0, output_max_value=100)
nyul.fit_population(images)
# Normalize all images to the same scale
normalized_images = [nyul.transform(img) for img in images]
# LSQ tissue mean harmonization
lsq = LSQNormalizer()
lsq.fit_population(images)
harmonized_images = [lsq.transform(img) for img in images]
Command Line Interface
Single Image Normalization
# Basic usage
intensity-normalize fcm brain_t1.nii.gz
# With brain mask
intensity-normalize fcm brain_t1.nii.gz -m brain_mask.nii.gz
# Specify output location
intensity-normalize zscore brain_t1.nii.gz -o normalized_brain.nii.gz
# Different modalities and tissue types
intensity-normalize kde brain_t2.nii.gz --modality t2 --tissue-type gm
intensity-normalize whitestripe brain_flair.nii.gz --modality flair --width 0.1
Method-Specific Parameters
# FCM with different tissue types and clusters
intensity-normalize fcm brain.nii.gz --tissue-type wm --n-clusters 3
# WhiteStripe with custom width
intensity-normalize whitestripe brain.nii.gz --width 0.05
# Get help for specific methods
intensity-normalize fcm --help
Supported File Formats
Works with all neuroimaging formats supported by nibabel:
| Format | Extensions | Description |
|---|---|---|
| NIfTI | .nii, .nii.gz |
Most common neuroimaging format |
| FreeSurfer | .mgz, .mgh |
FreeSurfer volume format |
| ANALYZE | .hdr/.img |
Legacy format pair |
| MINC | .mnc |
Medical Imaging NetCDF |
| PAR/REC | .par/.rec |
Philips scanner format |
| Numpy | .npy |
Raw numpy arrays |
Normalization Methods
Individual Methods (Single Image)
| Method | Best For | Description |
|---|---|---|
| FCM | T1-weighted | Fuzzy C-means tissue segmentation (recommended) |
| Z-Score | Any modality | Standard score normalization |
| KDE | T1/T2/FLAIR | Kernel density estimation of tissue modes |
| WhiteStripe | T1-weighted | Normal-appearing white matter standardization |
Population Methods (Multiple Images)
| Method | Best For | Description |
|---|---|---|
| Nyúl | Cross-scanner | Piecewise linear histogram matching |
| LSQ | Multi-site studies | Least squares tissue mean harmonization |
Method Selection Guide
# T1-weighted images (structural)
normalize_image(t1_image, method="fcm", tissue_type="wm")
# T2-weighted or FLAIR
normalize_image(t2_image, method="kde", modality="t2")
# Quick standardization
normalize_image(image, method="zscore")
# Multi-site harmonization (requires multiple subjects)
from intensity_normalization.services.normalization import NormalizationService
config = NormalizationConfig(method="nyul")
harmonized = NormalizationService.normalize_images(all_images, config)
Architecture Overview
The package is structured as follows:
intensity_normalization/
├── domain/ # Core logic
│ ├── protocols.py # Image and normalizer interfaces
│ ├── models.py # Configuration and value objects
│ └── exceptions.py# Domain-specific exceptions
├── adapters/ # External interfaces
│ ├── images.py # Universal image adapter (numpy/nibabel)
│ └── io.py # File I/O operations
├── normalizers/ # Normalization implementations
│ ├── individual/ # Single-image methods (FCM, Z-score, etc.)
│ └── population/ # Multi-image methods (Nyúl, LSQ)
├── services/ # Application services
│ ├── normalization.py # Orchestration logic
│ └── validation.py # Input validation
└── cli.py # Command-line interface
Advanced Usage
Custom Normalizers
from intensity_normalization.domain.protocols import BaseNormalizer
from intensity_normalization.domain.protocols import ImageProtocol
class CustomNormalizer(BaseNormalizer):
def fit(self, image: ImageProtocol, mask=None) -> 'CustomNormalizer':
# Implement fitting logic
self.is_fitted = True
return self
def transform(self, image: ImageProtocol, mask=None) -> ImageProtocol:
# Implement normalization logic
data = image.get_data()
normalized_data = your_normalization_function(data)
return image.with_data(normalized_data)
Configuration-Based Workflow
from intensity_normalization.domain.models import NormalizationConfig, Modality, TissueType
from intensity_normalization.services.normalization import NormalizationService
# Create configuration
config = NormalizationConfig(
method="fcm",
modality=Modality.T1,
tissue_type=TissueType.WM
)
# Validate configuration
from intensity_normalization.services import ValidationService
ValidationService.validate_normalization_config(config)
# Apply normalization
result = NormalizationService.normalize_image(image, config, mask)
Batch Processing
from pathlib import Path
from intensity_normalization.adapters.images import create_image, save_image
from intensity_normalization.services.normalization import NormalizationService
def process_directory(input_dir: Path, output_dir: Path, method: str = "fcm"):
"""Process all NIfTI files in a directory."""
output_dir.mkdir(exist_ok=True)
for img_file in input_dir.glob("*.nii.gz"):
# Load image
image = create_image(img_file)
# Create configuration
config = NormalizationConfig(method=method)
# Normalize
normalized = NormalizationService.normalize_image(image, config)
# Save result
output_file = output_dir / f"{img_file.stem}_normalized.nii.gz"
save_image(normalized, output_file)
print(f"Processed: {img_file.name}")
# Usage
process_directory(Path("raw_images/"), Path("normalized/"))
Development
Setup Development Environment
# Clone and setup
git clone https://github.com/jcreinhold/intensity-normalization.git
cd intensity-normalization
# Install uv (modern Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create virtual environment and install dependencies
uv sync --dev
Code Quality
# Format code
uv run ruff format intensity_normalization/
# Lint code
uv run ruff check intensity_normalization/
# Type checking
uv run mypy intensity_normalization/
# Run tests
uv run pytest
# Run tests with coverage
uv run pytest --cov=intensity_normalization --cov-report=html
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make changes and add tests
- Ensure code quality (
uv run ruff format && uv run ruff check --fix && uv run mypy) - Run tests (
uv run pytest) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
Citation
If you use this package in your research, please cite:
@inproceedings{reinhold2019evaluating,
title={Evaluating the impact of intensity normalization on {MR} image synthesis},
author={Reinhold, Jacob C and Dewey, Blake E and Carass, Aaron and Prince, Jerry L},
booktitle={Medical Imaging 2019: Image Processing},
volume={10949},
pages={109493H},
year={2019},
organization={International Society for Optics and Photonics}}
Related Papers
- FCM: Udupa, J.K., et al. "A framework for evaluating image segmentation algorithms." Computerized medical imaging and graphics 30.2 (2006): 75-87.
- Nyúl: Nyúl, L.G., Udupa, J.K. "On standardizing the MR image intensity scale." Magnetic Resonance in Medicine 42.6 (1999): 1072-1081.
- WhiteStripe: Shinohara, R.T., et al. "Statistical normalization techniques for magnetic resonance imaging." NeuroImage 132 (2016): 174-184.
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file intensity_normalization-3.0.1.tar.gz.
File metadata
- Download URL: intensity_normalization-3.0.1.tar.gz
- Upload date:
- Size: 50.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7797fe520fed6bd4508e4dc9c07995250afc83dde09737fb21c98bc4b780d6d4
|
|
| MD5 |
d707b4f4f42f97725375cbe97e2d2b94
|
|
| BLAKE2b-256 |
2004dbe39ab62c5c1f58cc870295e380a4f5f0f6e11f35b90bfdc03065442ceb
|
Provenance
The following attestation bundles were made for intensity_normalization-3.0.1.tar.gz:
Publisher:
publish.yml on jcreinhold/intensity-normalization
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
intensity_normalization-3.0.1.tar.gz -
Subject digest:
7797fe520fed6bd4508e4dc9c07995250afc83dde09737fb21c98bc4b780d6d4 - Sigstore transparency entry: 300225595
- Sigstore integration time:
-
Permalink:
jcreinhold/intensity-normalization@6d2e48672dc9a89fac9a0221f5be065a8dcd0727 -
Branch / Tag:
refs/tags/3.0.1 - Owner: https://github.com/jcreinhold
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6d2e48672dc9a89fac9a0221f5be065a8dcd0727 -
Trigger Event:
release
-
Statement type:
File details
Details for the file intensity_normalization-3.0.1-py3-none-any.whl.
File metadata
- Download URL: intensity_normalization-3.0.1-py3-none-any.whl
- Upload date:
- Size: 28.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
777594a247a15ec419d158fe939327db993aa7c63d7815820ad5a8804787cb46
|
|
| MD5 |
b8268a0500bbea4be612e3411541daef
|
|
| BLAKE2b-256 |
cf0dc500b783db899589433868b753e7839bdea616c0ac381d87bffa56b6e090
|
Provenance
The following attestation bundles were made for intensity_normalization-3.0.1-py3-none-any.whl:
Publisher:
publish.yml on jcreinhold/intensity-normalization
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
intensity_normalization-3.0.1-py3-none-any.whl -
Subject digest:
777594a247a15ec419d158fe939327db993aa7c63d7815820ad5a8804787cb46 - Sigstore transparency entry: 300225624
- Sigstore integration time:
-
Permalink:
jcreinhold/intensity-normalization@6d2e48672dc9a89fac9a0221f5be065a8dcd0727 -
Branch / Tag:
refs/tags/3.0.1 - Owner: https://github.com/jcreinhold
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6d2e48672dc9a89fac9a0221f5be065a8dcd0727 -
Trigger Event:
release
-
Statement type: