{"id":32139266,"url":"https://github.com/jcreinhold/intensity-normalization","last_synced_at":"2026-02-21T03:02:20.297Z","repository":{"id":41432205,"uuid":"136844850","full_name":"jcreinhold/intensity-normalization","owner":"jcreinhold","description":"Normalize MR image intensities in Python","archived":false,"fork":false,"pushed_at":"2025-07-21T23:57:09.000Z","size":1800,"stargazers_count":341,"open_issues_count":0,"forks_count":58,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-12-07T09:41:57.632Z","etag":null,"topics":["fcm","harmonization","intensity-normalization","mri","neuroimaging","normalization","standardization","whitestripe","zscore"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jcreinhold.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-06-10T21:05:58.000Z","updated_at":"2025-11-18T15:38:58.000Z","dependencies_parsed_at":"2024-06-19T05:29:05.052Z","dependency_job_id":"b6167e65-477a-4f62-be98-7977e3422b52","html_url":"https://github.com/jcreinhold/intensity-normalization","commit_stats":{"total_commits":253,"total_committers":7,"mean_commits":"36.142857142857146","dds":0.5454545454545454,"last_synced_commit":"1f8d8e085175f6bdb0cd5847e56307b9be16822a"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/jcreinhold/intensity-normalization","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcreinhold%2Fintensity-normalization","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcreinhold%2Fintensity-normalization/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcreinhold%2Fintensity-normalization/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcreinhold%2Fintensity-normalization/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcreinhold","download_url":"https://codeload.github.com/jcreinhold/intensity-normalization/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcreinhold%2Fintensity-normalization/sbom","scorecard":{"id":511862,"data":{"date":"2025-08-11","repo":{"name":"github.com/jcreinhold/intensity-normalization","commit":"6d2e48672dc9a89fac9a0221f5be065a8dcd0727"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.7,"checks":[{"name":"Code-Review","score":1,"reason":"Found 3/27 approved changesets -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"11 commit(s) and 3 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/publish.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/publish.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish.yml:47: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:42: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:52: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/jcreinhold/intensity-normalization/tests.yml/master?enable=pin","Info:   0 out of   5 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   5 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":10,"reason":"GitHub workflow tokens follow principle of least privilege","details":["Info: topLevel 'contents' permission set to 'read': .github/workflows/publish.yml:9","Info: topLevel 'contents' permission set to 'read': .github/workflows/tests.yml:10","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/publish.yml:30"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 6 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-20T00:45:17.895Z","repository_id":41432205,"created_at":"2025-08-20T00:45:17.895Z","updated_at":"2025-08-20T00:45:17.895Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29672258,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T00:11:43.526Z","status":"online","status_checked_at":"2026-02-21T02:00:07.432Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fcm","harmonization","intensity-normalization","mri","neuroimaging","normalization","standardization","whitestripe","zscore"],"created_at":"2025-10-21T05:33:40.864Z","updated_at":"2026-02-21T03:02:20.291Z","avatar_url":"https://github.com/jcreinhold.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Intensity Normalization\n\nA modern Python package for normalizing MR image intensities.\n\n## Features\n\n- **🔧 Multiple Image Format Support**: Works with numpy arrays and nibabel images (.nii, .nii.gz, .mgz, .mnc, etc.)\n- **📊 6 Normalization Methods**: FCM, KDE, WhiteStripe, Z-score, Nyúl, LSQ\n- **⚡ High Performance**: Optimized implementations\n\n## Installation\n\n```bash\npip install intensity-normalization\n```\nOr via conda:\n```sh\nconda install conda-forge::intensity-normalization\n```\n\n## Quick Start\n\n### High-Level API\n\n```python\nimport numpy as np\nimport nibabel as nib\nfrom intensity_normalization import normalize_image\n\n# Load MRI image and brain mask\nimg = nib.load(\"brain_t1.nii.gz\")\nmask = nib.load(\"brain_mask.nii.gz\")\n\n# Normalize using FCM (default, recommended for T1)\nnormalized = normalize_image(img, method=\"fcm\", mask=mask)\n\n# Different methods for different modalities\nt2_normalized = normalize_image(img, method=\"kde\", modality=\"t2\")\nzscore_normalized = normalize_image(img, method=\"zscore\")\n```\n\n### Object-Oriented API\n\n```python\nfrom intensity_normalization import FCMNormalizer, ZScoreNormalizer\nimport numpy as np\n\n# Create synthetic brain data\nbrain_data = np.random.normal(1000, 200, (64, 64, 32))  # WM ~1000\nbrain_data[20:40, 20:40, 10:20] = np.random.normal(600, 100, (20, 20, 10))  # GM ~600\n\n# FCM normalization (tissue-based)\nfcm = FCMNormalizer(tissue_type=\"wm\")  # white matter reference\nnormalized = fcm.fit_transform(brain_data)\n\n# Z-score normalization\nzscore = ZScoreNormalizer()\nstandardized = zscore.fit_transform(brain_data)\n\nprint(f\"Original mean: {brain_data[brain_data \u003e 0].mean():.1f}\")\nprint(f\"FCM normalized WM mean: {normalized[40:60, 40:60, 10:20].mean():.2f}\")\nprint(f\"Z-score mean: {standardized[brain_data \u003e 0].mean():.2f}\")\n```\n\n### Population-Based Methods\n\n```python\nfrom intensity_normalization import NyulNormalizer, LSQNormalizer\nfrom intensity_normalization.adapters import create_image\n\n# Load multiple subjects\nimage_paths = [\"subject1_t1.nii.gz\", \"subject2_t1.nii.gz\", \"subject3_t1.nii.gz\"]\nimages = [create_image(path) for path in image_paths]\n\n# Nyúl histogram matching\nnyul = NyulNormalizer(output_min_value=0, output_max_value=100)\nnyul.fit_population(images)\n\n# Normalize all images to the same scale\nnormalized_images = [nyul.transform(img) for img in images]\n\n# LSQ tissue mean harmonization\nlsq = LSQNormalizer()\nlsq.fit_population(images)\nharmonized_images = [lsq.transform(img) for img in images]\n```\n\n## Command Line Interface\n\n### Single Image Normalization\n\n```bash\n# Basic usage\nintensity-normalize fcm brain_t1.nii.gz\n\n# With brain mask\nintensity-normalize fcm brain_t1.nii.gz -m brain_mask.nii.gz\n\n# Specify output location\nintensity-normalize zscore brain_t1.nii.gz -o normalized_brain.nii.gz\n\n# Different modalities and tissue types\nintensity-normalize kde brain_t2.nii.gz --modality t2 --tissue-type gm\nintensity-normalize whitestripe brain_flair.nii.gz --modality flair --width 0.1\n```\n\n### Method-Specific Parameters\n\n```bash\n# FCM with different tissue types and clusters\nintensity-normalize fcm brain.nii.gz --tissue-type wm --n-clusters 3\n\n# WhiteStripe with custom width\nintensity-normalize whitestripe brain.nii.gz --width 0.05\n\n# Get help for specific methods\nintensity-normalize fcm --help\n```\n\n## Supported File Formats\n\nWorks with all neuroimaging formats supported by nibabel:\n\n| Format | Extensions | Description |\n|--------|------------|-------------|\n| **NIfTI** | `.nii`, `.nii.gz` | Most common neuroimaging format |\n| **FreeSurfer** | `.mgz`, `.mgh` | FreeSurfer volume format |\n| **ANALYZE** | `.hdr/.img` | Legacy format pair |\n| **MINC** | `.mnc` | Medical Imaging NetCDF |\n| **PAR/REC** | `.par/.rec` | Philips scanner format |\n| **Numpy** | `.npy` | Raw numpy arrays |\n\n## Normalization Methods\n\n### Individual Methods (Single Image)\n\n| Method | Best For | Description |\n|--------|----------|-------------|\n| **FCM** | T1-weighted | Fuzzy C-means tissue segmentation (recommended) |\n| **Z-Score** | Any modality | Standard score normalization |\n| **KDE** | T1/T2/FLAIR | Kernel density estimation of tissue modes |\n| **WhiteStripe** | T1-weighted | Normal-appearing white matter standardization |\n\n### Population Methods (Multiple Images)\n\n| Method | Best For | Description |\n|--------|----------|-------------|\n| **Nyúl** | Cross-scanner | Piecewise linear histogram matching |\n| **LSQ** | Multi-site studies | Least squares tissue mean harmonization |\n\n### Method Selection Guide\n\n```python\n# T1-weighted images (structural)\nnormalize_image(t1_image, method=\"fcm\", tissue_type=\"wm\")\n\n# T2-weighted or FLAIR\nnormalize_image(t2_image, method=\"kde\", modality=\"t2\")\n\n# Quick standardization\nnormalize_image(image, method=\"zscore\")\n\n# Multi-site harmonization (requires multiple subjects)\nfrom intensity_normalization.services.normalization import NormalizationService\nconfig = NormalizationConfig(method=\"nyul\")\nharmonized = NormalizationService.normalize_images(all_images, config)\n```\n\n## Architecture Overview\n\nThe package is structured as follows:\n\n```\nintensity_normalization/\n├── domain/          # Core logic\n│   ├── protocols.py # Image and normalizer interfaces\n│   ├── models.py    # Configuration and value objects\n│   └── exceptions.py# Domain-specific exceptions\n├── adapters/        # External interfaces\n│   ├── images.py    # Universal image adapter (numpy/nibabel)\n│   └── io.py        # File I/O operations\n├── normalizers/     # Normalization implementations\n│   ├── individual/  # Single-image methods (FCM, Z-score, etc.)\n│   └── population/  # Multi-image methods (Nyúl, LSQ)\n├── services/        # Application services\n│   ├── normalization.py # Orchestration logic\n│   └── validation.py    # Input validation\n└── cli.py          # Command-line interface\n```\n\n## Advanced Usage\n\n### Custom Normalizers\n\n```python\nfrom intensity_normalization.domain.protocols import BaseNormalizer\nfrom intensity_normalization.domain.protocols import ImageProtocol\n\nclass CustomNormalizer(BaseNormalizer):\n    def fit(self, image: ImageProtocol, mask=None) -\u003e 'CustomNormalizer':\n        # Implement fitting logic\n        self.is_fitted = True\n        return self\n\n    def transform(self, image: ImageProtocol, mask=None) -\u003e ImageProtocol:\n        # Implement normalization logic\n        data = image.get_data()\n        normalized_data = your_normalization_function(data)\n        return image.with_data(normalized_data)\n```\n\n### Configuration-Based Workflow\n\n```python\nfrom intensity_normalization.domain.models import NormalizationConfig, Modality, TissueType\nfrom intensity_normalization.services.normalization import NormalizationService\n\n# Create configuration\nconfig = NormalizationConfig(\n    method=\"fcm\",\n    modality=Modality.T1,\n    tissue_type=TissueType.WM\n)\n\n# Validate configuration\nfrom intensity_normalization.services import ValidationService\nValidationService.validate_normalization_config(config)\n\n# Apply normalization\nresult = NormalizationService.normalize_image(image, config, mask)\n```\n\n### Batch Processing\n\n```python\nfrom pathlib import Path\nfrom intensity_normalization.adapters.images import create_image, save_image\nfrom intensity_normalization.services.normalization import NormalizationService\n\ndef process_directory(input_dir: Path, output_dir: Path, method: str = \"fcm\"):\n    \"\"\"Process all NIfTI files in a directory.\"\"\"\n    output_dir.mkdir(exist_ok=True)\n\n    for img_file in input_dir.glob(\"*.nii.gz\"):\n        # Load image\n        image = create_image(img_file)\n\n        # Create configuration\n        config = NormalizationConfig(method=method)\n\n        # Normalize\n        normalized = NormalizationService.normalize_image(image, config)\n\n        # Save result\n        output_file = output_dir / f\"{img_file.stem}_normalized.nii.gz\"\n        save_image(normalized, output_file)\n        print(f\"Processed: {img_file.name}\")\n\n# Usage\nprocess_directory(Path(\"raw_images/\"), Path(\"normalized/\"))\n```\n\n## Development\n\n### Setup Development Environment\n\n```bash\n# Clone and setup\ngit clone https://github.com/jcreinhold/intensity-normalization.git\ncd intensity-normalization\n\n# Install uv (modern Python package manager)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Create virtual environment and install dependencies\nuv sync --dev\n```\n\n### Code Quality\n\n```bash\n# Format code\nuv run ruff format intensity_normalization/\n\n# Lint code\nuv run ruff check intensity_normalization/\n\n# Type checking\nuv run mypy intensity_normalization/\n\n# Run tests\nuv run pytest\n\n# Run tests with coverage\nuv run pytest --cov=intensity_normalization --cov-report=html\n```\n\n### Contributing\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Make changes and add tests\n4. Ensure code quality (`uv run ruff format \u0026\u0026 uv run ruff check --fix \u0026\u0026 uv run mypy`)\n5. Run tests (`uv run pytest`)\n6. Commit changes (`git commit -m 'Add amazing feature'`)\n7. Push to branch (`git push origin feature/amazing-feature`)\n8. Open a Pull Request\n\n## Citation\n\nIf you use this package in your research, please cite:\n\n```bibtex\n@inproceedings{reinhold2019evaluating,\n  title={Evaluating the impact of intensity normalization on {MR} image synthesis},\n  author={Reinhold, Jacob C and Dewey, Blake E and Carass, Aaron and Prince, Jerry L},\n  booktitle={Medical Imaging 2019: Image Processing},\n  volume={10949},\n  pages={109493H},\n  year={2019},\n  organization={International Society for Optics and Photonics}}\n```\n\n## Related Papers\n\n- **FCM**: Udupa, J.K., et al. \"A framework for evaluating image segmentation algorithms.\" Computerized medical imaging and graphics 30.2 (2006): 75-87.\n- **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.\n- **WhiteStripe**: Shinohara, R.T., et al. \"Statistical normalization techniques for magnetic resonance imaging.\" NeuroImage 132 (2016): 174-184.\n\n## Support\n\n- **Issues**: [GitHub Issues](https://github.com/jcreinhold/intensity-normalization/issues)\n- **Discussions**: [GitHub Discussions](https://github.com/jcreinhold/intensity-normalization/discussions)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcreinhold%2Fintensity-normalization","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcreinhold%2Fintensity-normalization","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcreinhold%2Fintensity-normalization/lists"}