├── docs ├── requirements.txt ├── source │ ├── api.rst │ ├── index.rst │ └── conf.py ├── Makefile └── make.bat ├── test ├── requirements.txt ├── test_vtk.py ├── test_pyside.py ├── conftest.py ├── test_dask.py └── test_utilities.py ├── requirements.txt ├── NOTICE ├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ ├── docs.yml │ └── main.yml ├── .gitignore ├── CITATION.cff ├── .pre-commit-config.yaml ├── pyproject.toml ├── README.md ├── .zenodo.json ├── SimpleITK └── utilities │ ├── __init__.py │ ├── slice_by_slice.py │ ├── Logger.py │ ├── dask.py │ ├── make_isotropic.py │ ├── vtk.py │ ├── pyside.py │ ├── resize.py │ ├── fft.py │ └── overlay_bounding_boxes.py └── LICENSE /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | SimpleITK>=2.3.0 2 | numpy -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | SimpleITKUtilities 2 | 3 | Copyright 2023 NumFOCUS 4 | 5 | This software is distributed under the Apache 2.0 License. 6 | 7 | See LICENSE file for details. 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | day: "sunday" 11 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. automodule:: SimpleITK.utilities 5 | :members: 6 | 7 | .. automodule:: SimpleITK.utilities.dask 8 | :members: 9 | 10 | .. automodule:: SimpleITK.utilities.pyside 11 | :members 12 | 13 | .. automodule:: SimpleITK.utilities.vtk 14 | :members: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | coverage.xml 3 | htmlcov 4 | 5 | .idea 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # Environments 33 | .env 34 | .venv 35 | env/ 36 | venv/ -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. SimpleITK Utilities documentation master file, created by 2 | sphinx-quickstart on Fri Feb 10 11:26:36 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to SimpleITK Utilities' documentation! 7 | =============================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | api 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: "SimpleITK Utilities" 3 | message: "If you find this software useful in your work, support our efforts by citing it using the following information." 4 | type: software 5 | authors: 6 | - family-names: Lowekamp 7 | given-names: Bradley C. 8 | orcid: 'https://orcid.org/0000-0002-4579-5738' 9 | - family-names: Chen 10 | given-names: David T. 11 | orcid: 'https://orcid.org/0009-0002-8386-7840' 12 | - family-names: Yaniv 13 | given-names: Ziv 14 | orcid: 'https://orcid.org/0000-0003-0315-7727' 15 | url: 'https://github.com/SimpleITK/SimpleITKUtilities' 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # .pre-commit-config.yaml 2 | # for details see https://pre-commit.com 3 | # for list of available hooks see https://pre-commit.com/hooks.html 4 | # 5 | # Preclude commits that do not conform to various criteria. 6 | 7 | fail_fast: true 8 | 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.3.0 12 | hooks: # check for large files, aws credentials and private key files 13 | - id: check-added-large-files 14 | args: ['--maxkb=200'] 15 | - id: detect-private-key 16 | - repo: https://github.com/psf/black 17 | rev: 23.1.0 18 | hooks: # check conformance to black formatting 19 | - id: black 20 | args: ['--check'] # if run without arguments, will fail and will format the files 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "SimpleITKUtilities" 7 | authors = [ 8 | { name="Bradley Lowekamp", email="blowekamp@mail.nih.gov" }, 9 | { name="Ziv Yaniv", email="zivyaniv@nih.gov" }, 10 | ] 11 | description = "A collection of utilities and integration tools to enhance SimpleITK." 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dynamic = ["dependencies", "version"] 20 | 21 | 22 | [project.optional-dependencies] 23 | vtk=['vtk>=9.0'] 24 | dask=['dask'] 25 | pyside=['PySide6'] 26 | 27 | [tool.setuptools] 28 | packages = ["SimpleITK.utilities"] 29 | 30 | 31 | [tool.setuptools.dynamic] 32 | dependencies = {file = ["requirements.txt"]} 33 | 34 | [tool.setuptools_scm] 35 | write_to = "SimpleITK/utilities/_version.py" 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "SimpleITK Utilities" 10 | copyright = "2023, Bradley Lowekamp" 11 | author = "Bradley Lowekamp" 12 | release = "v0.1" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.githubpages", 20 | ] 21 | 22 | templates_path = ["_templates"] 23 | exclude_patterns = [] 24 | 25 | 26 | # -- Options for HTML output ------------------------------------------------- 27 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 28 | 29 | html_theme = "alabaster" 30 | html_static_path = ["_static"] 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleITKUtilities 2 | 3 | ![SimpleITK Utilities Testing](https://github.com/SimpleITK/SimpleITKUtilities/actions/workflows/main.yml/badge.svg)  [![SimpleITK Utilities Website](https://img.shields.io/website-up-down-brightgreen-red/http/shields.io.svg)](http://simpleitk.org/SimpleITKUtilities/) 4 | 5 | 6 | This Python package provides a collection of utilities and integration tools which enhance the basic SimpleITK Python package functionality. It enables data exchange between SimpleITK and other packages (e.g. [vtk](https://vtk.org/), [dask](https://www.dask.org/), [PySide](https://wiki.qt.io/Qt_for_Python)). Additional useful capabilities include handling [SimpleITK](https://github.com/SimpleITK/SimpleITK) messages using standard Python logging, making an image isotropic, a decorator which enables running a filter in a slice-by-slice manner and more. 7 | 8 | For a complete list of utilities, see the [package website](http://simpleitk.org/SimpleITKUtilities/). 9 | 10 | ## Installation 11 | 12 | The Python module is distributed on [PyPI - The Python Package Index](https://pypi.org/project/SimpleITKUtilities/). The package can be installed by running: 13 | ``` 14 | python -m pip install SimpleITKUtilities 15 | ``` 16 | -------------------------------------------------------------------------------- /test/test_vtk.py: -------------------------------------------------------------------------------- 1 | import SimpleITK as sitk 2 | from SimpleITK.utilities.vtk import sitk2vtk, vtk2sitk 3 | import vtk 4 | import gc 5 | 6 | 7 | def test_sitktovtk(): 8 | img = sitk.Image([10, 10, 5], sitk.sitkFloat32) 9 | img = img + 42.0 10 | vtk_img = sitk2vtk(img) 11 | 12 | # free the SimpleITK image's memory 13 | img = None 14 | gc.collect() 15 | 16 | assert vtk_img.GetScalarComponentAsFloat(0, 0, 0, 0) == 42.0 17 | 18 | 19 | def test_vtktositk(): 20 | source = vtk.vtkImageSinusoidSource() 21 | source.Update() 22 | img = source.GetOutput() 23 | 24 | sitkimg = vtk2sitk(img) 25 | source = None 26 | img = None 27 | gc.collect() 28 | 29 | assert sitkimg[0, 0, 0] == 255.0 30 | 31 | 32 | def test_multichannel(): 33 | img = sitk.Image([10, 10], sitk.sitkVectorUInt8, 3) 34 | img[0, 0] = (255, 127, 42) 35 | vtk_img = sitk2vtk(img) 36 | 37 | assert vtk_img.GetNumberOfScalarComponents() == 3 38 | 39 | r = int(vtk_img.GetScalarComponentAsFloat(0, 0, 0, 0)) 40 | g = int(vtk_img.GetScalarComponentAsFloat(0, 0, 0, 1)) 41 | b = int(vtk_img.GetScalarComponentAsFloat(0, 0, 0, 2)) 42 | 43 | assert (r, g, b) == (255, 127, 42) 44 | 45 | sitk_img = vtk2sitk(vtk_img) 46 | 47 | assert sitk_img[0, 0, 0] == (255, 127, 42) 48 | -------------------------------------------------------------------------------- /test/test_pyside.py: -------------------------------------------------------------------------------- 1 | import SimpleITK as sitk 2 | from PySide6 import QtWidgets 3 | from SimpleITK.utilities.pyside import sitk2qpixmap, qpixmap2sitk 4 | 5 | 6 | def test_pyside(): 7 | # QPixmap cannot be created without a QGuiApplication 8 | app = QtWidgets.QApplication() 9 | 10 | # Test grayscale 11 | scalar_image = sitk.Image([2, 3], sitk.sitkUInt8) 12 | scalar_image[0, 0] = 1 13 | scalar_image[0, 1] = 2 14 | scalar_image[0, 2] = 3 15 | scalar_image[1, 0] = 253 16 | scalar_image[1, 1] = 254 17 | scalar_image[1, 2] = 255 18 | 19 | qpixmap = sitk2qpixmap(scalar_image) 20 | sitk_image = qpixmap2sitk(qpixmap) 21 | # Compare on pixel values, metadata information ignored. 22 | assert sitk.Hash(sitk_image) == sitk.Hash(scalar_image) 23 | 24 | # Test color 25 | color_image = sitk.Image([2, 3], sitk.sitkVectorUInt8, 3) 26 | color_image[0, 0] = [0, 1, 2] 27 | color_image[0, 1] = [4, 8, 16] 28 | color_image[0, 2] = [32, 64, 128] 29 | color_image[1, 0] = [0, 10, 20] 30 | color_image[1, 1] = [30, 40, 50] 31 | color_image[1, 2] = [60, 70, 80] 32 | 33 | qpixmap = sitk2qpixmap(color_image) 34 | sitk_image = qpixmap2sitk(qpixmap) 35 | # Compare on pixel values, metadata information ignored. 36 | assert sitk.Hash(sitk_image) == sitk.Hash(color_image) 37 | -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "

This repository contains a collection of utilities and integration tools which enhance the basic SimpleITK Python package functionality. Among other things, it enables data exchange between SimpleITK and other packages (e.g. vtk, dask) and provides additional useful capabilities such as handling SimpleITK messages using standard Python logging.

", 3 | "keywords": [ 4 | "Open Science", 5 | "Open Source Software", 6 | "Image Analysis", 7 | "SimpleITK" 8 | ], 9 | "license": "Apache-2.0", 10 | "title": "SimpleITK Utilities", 11 | "upload_type": "software", 12 | "creators": [ 13 | { 14 | "affiliation": "National Institutes of Health", 15 | "name": "Lowekamp, Bradley", 16 | "orcid": "0000-0002-4579-5738" 17 | }, 18 | { 19 | "affiliation": "National Institutes of Health", 20 | "name": "Chen, David T.", 21 | "orcid": "0009-0002-8386-7840" 22 | }, 23 | { 24 | "affiliation": "National Institutes of Health", 25 | "name": "Yaniv, Ziv", 26 | "orcid": "0000-0003-0315-7727" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /SimpleITK/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | from .Logger import Logger 20 | from .slice_by_slice import slice_by_slice 21 | from .make_isotropic import make_isotropic 22 | from .fft import fft_based_translation_initialization 23 | from .overlay_bounding_boxes import overlay_bounding_boxes 24 | from .resize import resize 25 | 26 | try: 27 | from ._version import version as __version__ 28 | except ImportError: 29 | __version__ = "unknown version" 30 | 31 | 32 | __all__ = [ 33 | "Logger", 34 | "slice_by_slice", 35 | "make_isotropic", 36 | "fft_based_translation_initialization", 37 | "overlay_bounding_boxes", 38 | "resize", 39 | "__version__", 40 | ] 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | concurrency: 9 | group: publish 10 | 11 | 12 | jobs: 13 | publish: 14 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 15 | name: Upload release to Github Releases and PyPI 16 | runs-on: ubuntu-latest 17 | permissions: 18 | id-token: write 19 | contents: write 20 | environment: publish 21 | steps: 22 | - uses: actions/checkout@v6 23 | with: 24 | fetch-depth: 0 25 | - name: Set up Python 3.11 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: "3.11" 29 | - name: Build package 30 | run: | 31 | python -m pip install twine build 32 | python -m build --wheel --sdist 33 | python -m twine check dist/* 34 | ls -la dist 35 | - name: Create Release and Upload 36 | id: create_release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | run: | 40 | gh release create ${{ github.ref_name }} --repo ${{ github.repository }} --verify-tag --generate-notes --title "Release ${{ github.ref_name }}" 41 | gh release upload ${{ github.ref_name }} --repo ${{ github.repository }} dist/* 42 | 43 | 44 | - name: PyPI Publish package 45 | # hash for release/v1.8 46 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e 47 | with: 48 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Sphinx docs static content to GitHub Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | # Single deploy job since we're just deploying 24 | docs: 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v6 32 | - name: Set up Python 3.11 33 | uses: actions/setup-python@v6 34 | with: 35 | python-version: 3.11 36 | cache: 'pip' 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | python -m pip install -r docs/requirements.txt .[vtk,dask,pyside] 41 | - name: Build Sphinx Documentation 42 | run: | 43 | make -C docs html 44 | rm docs/build/html/.buildinfo 45 | 46 | - name: Upload documentation 47 | uses: actions/upload-artifact@v6 48 | with: 49 | name: sphinx-docs 50 | path: docs/build/html 51 | 52 | - name: Setup Pages 53 | uses: actions/configure-pages@v5 54 | - name: Upload artifact 55 | if: github.ref == 'refs/heads/main' 56 | uses: actions/upload-pages-artifact@v4 57 | with: 58 | # Upload build sphinx docs 59 | path: 'docs/build/html' 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import SimpleITK as sitk 3 | from pathlib import Path 4 | from itertools import product, chain 5 | 6 | _params = product( 7 | [sitk.sitkInt8, sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32], 8 | ["nrrd", "nii", "mha"], 9 | ) 10 | 11 | _params = chain( 12 | _params, 13 | product( 14 | [ 15 | sitk.sitkInt8, 16 | sitk.sitkInt16, 17 | sitk.sitkInt16, 18 | sitk.sitkInt32, 19 | sitk.sitkFloat32, 20 | ], 21 | ["mrc"], 22 | ), 23 | ) 24 | 25 | 26 | def to_ids(p): 27 | return f"{p[1]}_{sitk.GetPixelIDValueAsString([0])}" 28 | 29 | 30 | @pytest.fixture(scope="session", params=_params) 31 | def image_fixture(request, tmp_path_factory) -> Path: 32 | pixel_type, extension = request.param 33 | fn = f"image_{sitk.GetPixelIDValueAsString(pixel_type).replace(' ', '_')}.nrrd" 34 | img = sitk.Image([256, 128, 64], pixel_type) 35 | 36 | fn = Path(tmp_path_factory.mktemp("data")) / fn 37 | sitk.WriteImage(img, fn) 38 | return fn 39 | 40 | 41 | _params = product( 42 | [sitk.sitkInt8, sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32], 43 | ["nrrd", "nii", "mha"], 44 | ) 45 | _params = chain( 46 | _params, 47 | product( 48 | [ 49 | sitk.sitkInt8, 50 | sitk.sitkUInt8, 51 | sitk.sitkInt16, 52 | sitk.sitkInt16, 53 | sitk.sitkInt32, 54 | sitk.sitkUInt32, 55 | ], 56 | ["dcm"], 57 | ), 58 | ) 59 | 60 | 61 | @pytest.fixture(scope="session", params=_params) 62 | def image_fixture_2d(request, tmp_path_factory) -> Path: 63 | pixel_type, extension = request.param 64 | fn = f"image_2d_{sitk.GetPixelIDValueAsString(pixel_type).replace(' ', '_')}.nrrd" 65 | img = sitk.Image([512, 256], pixel_type) 66 | 67 | fn = Path(tmp_path_factory.mktemp("data")) / fn 68 | sitk.WriteImage(img, fn) 69 | return fn 70 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python Test and Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | 15 | linting: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | - name: Set up Python 3.11 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: 3.11 25 | cache: 'pip' 26 | - name: Linting with pre-commit 27 | run: | 28 | python -m pip install pre-commit 29 | pre-commit run --all-files 30 | 31 | test: 32 | needs: linting 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | python-version: [3.8,3.9,'3.10', 3.11] 37 | 38 | steps: 39 | - uses: actions/checkout@v6 40 | with: 41 | fetch-depth: 0 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v6 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | cache: 'pip' 47 | 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | python -m pip install -e .[vtk,dask,pyside] -r test/requirements.txt 52 | sudo apt-get update 53 | sudo apt install libegl1 54 | 55 | - name: Test with pytest 56 | env: 57 | QT_QPA_PLATFORM: offscreen 58 | run: | 59 | python -m pytest 60 | 61 | 62 | publish: 63 | needs: test 64 | name: Create Artifact 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v6 68 | with: 69 | fetch-depth: 0 70 | - name: Set up Python 3.11 71 | uses: actions/setup-python@v6 72 | with: 73 | python-version: "3.11" 74 | - name: Build package 75 | run: | 76 | python -m pip install twine build 77 | python -m build --wheel --sdist 78 | python -m twine check dist/* 79 | ls -la dist 80 | - name: Upload package 81 | if: github.event_name == 'push' 82 | uses: actions/upload-artifact@v6 83 | with: 84 | name: python-packages 85 | path: dist 86 | -------------------------------------------------------------------------------- /test/test_dask.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import SimpleITK as sitk 3 | from SimpleITK.utilities.dask import from_sitk 4 | import numpy as np 5 | 6 | 7 | def test_from_sitk(image_fixture): 8 | sitk_img = sitk.ReadImage(image_fixture) 9 | 10 | img = from_sitk(image_fixture) 11 | img.compute() 12 | assert img.shape == sitk_img.GetSize()[::-1] 13 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 14 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 15 | 16 | img = from_sitk(image_fixture, chunks=(1, -1, -1)) 17 | print(f"chunks: {[c[0] for c in img.chunks]}") 18 | img.compute() 19 | assert [c[0] for c in img.chunks] == [ 20 | 1, 21 | sitk_img.GetSize()[1], 22 | sitk_img.GetSize()[0], 23 | ] 24 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 25 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 26 | 27 | img = from_sitk(image_fixture, chunks=(1, 128, 128)) 28 | img.compute() 29 | assert [c[0] for c in img.chunks] == [1, 128, 128] 30 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 31 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 32 | 33 | img = from_sitk(image_fixture, chunks=(-1, 83, 89)) 34 | img.compute() 35 | assert [c[0] for c in img.chunks] == [sitk_img.GetSize()[2], 83, 89] 36 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 37 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 38 | 39 | 40 | def test_from_sitk_2d(image_fixture_2d): 41 | sitk_img = sitk.ReadImage(image_fixture_2d) 42 | 43 | img = from_sitk(image_fixture_2d) 44 | img.compute() 45 | assert img.shape == sitk_img.GetSize()[::-1] 46 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 47 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 48 | 49 | img = from_sitk(image_fixture_2d, chunks=(128, -1)) 50 | img.compute() 51 | assert [c[0] for c in img.chunks] == [128, sitk_img.GetSize()[0]] 52 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 53 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 54 | 55 | img = from_sitk(image_fixture_2d, chunks=(128, 128)) 56 | img.compute() 57 | assert [c[0] for c in img.chunks] == [128, 128] 58 | assert np.array_equal(np.asarray(img), sitk.GetArrayViewFromImage(sitk_img)) 59 | assert img.dtype == sitk.extra._get_numpy_dtype(sitk_img) 60 | -------------------------------------------------------------------------------- /SimpleITK/utilities/slice_by_slice.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | 21 | import itertools 22 | from functools import wraps 23 | 24 | 25 | def slice_by_slice(func): 26 | """A function decorator which executes func on each 3D sub-volume and 27 | *in-place* pastes the results into the input image. The input image type 28 | and the output image type are required to be the same type. 29 | 30 | :param func: A function which takes a SimpleITK Image as it's first 31 | argument and returns an Image as the result. 32 | 33 | :return: A decorated function. 34 | """ 35 | 36 | iter_dim = 2 37 | 38 | @wraps(func) 39 | def _slice_by_slice(image, *args, **kwargs): 40 | dim = image.GetDimension() 41 | 42 | if dim <= iter_dim: 43 | # 44 | image = func(image, *args, **kwargs) 45 | return image 46 | 47 | extract_size = list(image.GetSize()) 48 | extract_size[iter_dim:] = itertools.repeat(0, dim - iter_dim) 49 | 50 | extract_index = [0] * dim 51 | paste_idx = [slice(None, None)] * dim 52 | 53 | extractor = sitk.ExtractImageFilter() 54 | extractor.SetSize(extract_size) 55 | 56 | for high_idx in itertools.product( 57 | *[range(s) for s in image.GetSize()[iter_dim:]] 58 | ): 59 | # The lower 2 elements of extract_index are always 0. 60 | # The remaining indices are iterated through all indexes. 61 | extract_index[iter_dim:] = high_idx 62 | extractor.SetIndex(extract_index) 63 | 64 | # Sliced based indexing for setting image values internally uses 65 | # the PasteImageFilter executed "in place". The lower 2 elements 66 | # are equivalent to ":". For a less general case the assignment 67 | # could be written as image[:,:,z] = ... 68 | paste_idx[iter_dim:] = high_idx 69 | image[paste_idx] = func(extractor.Execute(image), *args, **kwargs) 70 | 71 | return image 72 | 73 | return _slice_by_slice 74 | -------------------------------------------------------------------------------- /SimpleITK/utilities/Logger.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | import logging 21 | 22 | 23 | class Logger(sitk.LoggerBase): 24 | """ 25 | Adapts SimpleITK messages to be handled by a Python Logger object. 26 | 27 | Allows using the logging module to control the handling of messages coming 28 | from ITK and SimpleTK. Messages such as debug and warnings are handled by 29 | objects derived from sitk.LoggerBase. 30 | 31 | The LoggerBase.SetAsGlobalITKLogger method must be called to enable 32 | SimpleITK messages to use the logger. 33 | 34 | The Python logger module adds a second layer of control for the logging 35 | level in addition to the controls already in SimpleITK. 36 | 37 | The "Debug" property of a SimpleITK object must be enabled (if 38 | available) and the support from the Python "logging flow" hierarchy 39 | to handle debug messages from a SimpleITK object. 40 | 41 | Warning messages from SimpleITK are globally disabled with 42 | ProcessObject:GlobalWarningDisplayOff. 43 | 44 | """ 45 | 46 | def __init__(self, logger: logging.Logger = logging.getLogger("SimpleITK")): 47 | """ 48 | Initializes with a Logger object to handle the messages emitted from 49 | SimpleITK/ITK. 50 | """ 51 | super(Logger, self).__init__() 52 | self._logger = logger 53 | 54 | @property 55 | def logger(self): 56 | return self._logger 57 | 58 | @logger.setter 59 | def logger(self, logger): 60 | self._logger = logger 61 | 62 | def __enter__(self): 63 | self._old_logger = self.SetAsGlobalITKLogger() 64 | return self 65 | 66 | def __exit__(self, exc_type, exc_val, exc_tb): 67 | self._old_logger.SetAsGlobalITKLogger() 68 | del self._old_logger 69 | 70 | def DisplayText(self, s): 71 | # Remove newline endings from SimpleITK/ITK messages since the Python 72 | # logger adds during output. 73 | self._logger.info(s.rstrip()) 74 | 75 | def DisplayErrorText(self, s): 76 | self._logger.error(s.rstrip()) 77 | 78 | def DisplayWarningText(self, s): 79 | self._logger.warning(s.rstrip()) 80 | 81 | def DisplayGenericOutputText(self, s): 82 | self._logger.info(s.rstrip()) 83 | 84 | def DisplayDebugText(self, s): 85 | self._logger.debug(s.rstrip()) 86 | -------------------------------------------------------------------------------- /SimpleITK/utilities/dask.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import dask.array as da 20 | import SimpleITK as sitk 21 | from pathlib import Path 22 | from typing import Union, Tuple 23 | 24 | PathType = Union[str, Path] 25 | ChunkType = Union[int, Tuple] 26 | 27 | 28 | def from_sitk(filename: PathType, chunks: ChunkType = None) -> da.Array: 29 | """Reads the filename into a dask array with map_block. 30 | 31 | SimpleITK is used to "stream read" chunks from the file if supported, otherwise the entire image will be read for 32 | each chunk request.ITK support full streaming includes MHA, MRC, NRRD and NIFTI file formats. 33 | 34 | :param filename: A path-like object to the location of an image file readable by SimpleITK. 35 | :param chunks: Please see dask documentation on chunks of dask arrays for supported formats. Chunk size can be tuned 36 | for performance based on continuously stored on disk, re-chunking, and downstream processes. 37 | :return: A Dask array of the image on file. 38 | """ 39 | reader = sitk.ImageFileReader() 40 | reader.SetFileName(str(filename)) 41 | reader.ReadImageInformation() 42 | img_shape = reader.GetSize()[::-1] 43 | # default to loading the whole image from file in case the ITK ImageIO does not support streaming 44 | if chunks is None: 45 | chunks = (-1,) * reader.GetDimension() 46 | 47 | is_multi_component = reader.GetNumberOfComponents() != 1 48 | 49 | if is_multi_component: 50 | img_shape = img_shape + (reader.GetNumberOfComponents(),) 51 | 52 | if len(chunks) < len(img_shape): 53 | chunks = chunks + (-1,) 54 | 55 | z = da.zeros( 56 | shape=img_shape, dtype=sitk.extra._get_numpy_dtype(reader), chunks=chunks 57 | ) 58 | 59 | def func(z, block_info=None): 60 | _reader = sitk.ImageFileReader() 61 | _reader.SetFileName(str(filename)) 62 | if block_info is not None: 63 | if is_multi_component: 64 | size = block_info[None]["chunk-shape"][-2::-1] 65 | index = [al[0] for al in block_info[None]["array-location"][-2::-1]] 66 | else: 67 | size = block_info[None]["chunk-shape"][::-1] 68 | index = [al[0] for al in block_info[None]["array-location"][::-1]] 69 | _reader.SetExtractIndex(index) 70 | _reader.SetExtractSize(size) 71 | sitk_img = _reader.Execute() 72 | return sitk.GetArrayFromImage(sitk_img) 73 | return z 74 | 75 | da_img = da.map_blocks(func, z, meta=z, name="from-sitk") 76 | return da_img 77 | -------------------------------------------------------------------------------- /SimpleITK/utilities/make_isotropic.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | import numpy as np 21 | import itertools 22 | 23 | 24 | def make_isotropic( 25 | image, 26 | interpolator=sitk.sitkLinear, 27 | spacing=None, 28 | default_value=0, 29 | standardize_axes=False, 30 | ): 31 | """ 32 | Many file formats (e.g. jpg, png,...) expect the pixels to be isotropic, same 33 | spacing for all axes. Saving non-isotropic data in these formats will result in 34 | distorted images. This function makes an image isotropic via resampling, if needed. 35 | 36 | :param image: (SimpleITK.Image): Input image. 37 | :type image: SimpleITK.Image 38 | :param interpolator: By default the function uses a linear interpolator. For 39 | label images one should use the sitkNearestNeighbor interpolator 40 | so as not to introduce non-existent labels. 41 | :param spacing: Desired spacing. If none given then use the smallest spacing from 42 | the original image. 43 | :type spacing: float 44 | 45 | :param default_value: Desired pixel value for resampled points that fall 46 | outside the original image (e.g. HU value for air, -1000, 47 | when image is CT). 48 | :param standardize_axes: If the original image axes were not the standard ones, i.e. non 49 | identity cosine matrix, we may want to resample it to have standard 50 | axes. To do that, set this parameter to True. 51 | :type standardize_axes: bool 52 | 53 | :return: An image with isotropic spacing which occupies the same region in space as 54 | the input image. 55 | :rtype: SimpleITK.Image 56 | """ 57 | original_spacing = image.GetSpacing() 58 | # Image is already isotropic, just return a copy. 59 | if all(spc == original_spacing[0] for spc in original_spacing): 60 | return sitk.Image(image) 61 | # Make image isotropic via resampling. 62 | original_size = image.GetSize() 63 | if spacing is None: 64 | spacing = min(original_spacing) 65 | new_spacing = [spacing] * image.GetDimension() 66 | new_size = [ 67 | int(round(osz * ospc / spacing)) 68 | for osz, ospc in zip(original_size, original_spacing) 69 | ] 70 | new_direction = image.GetDirection() 71 | new_origin = image.GetOrigin() 72 | # Only need to standardize axes if user requested and the original 73 | # axes were not standard. 74 | if standardize_axes and not np.array_equal( 75 | np.array(new_direction), np.identity(image.GetDimension()).ravel() 76 | ): 77 | new_direction = np.identity(image.GetDimension()).ravel() 78 | # Compute bounding box for the original, non standard axes image. 79 | boundary_points = [] 80 | for boundary_index in list( 81 | itertools.product(*zip([0] * image.GetDimension(), image.GetSize())) 82 | ): 83 | boundary_points.append(image.TransformIndexToPhysicalPoint(boundary_index)) 84 | max_coords = np.max(boundary_points, axis=0) 85 | min_coords = np.min(boundary_points, axis=0) 86 | new_origin = min_coords 87 | new_size = (((max_coords - min_coords) / spacing).round().astype(int)).tolist() 88 | return sitk.Resample( 89 | image, 90 | new_size, 91 | sitk.Transform(), 92 | interpolator, 93 | new_origin, 94 | new_spacing, 95 | new_direction, 96 | default_value, 97 | image.GetPixelID(), 98 | ) 99 | -------------------------------------------------------------------------------- /SimpleITK/utilities/vtk.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | import logging 21 | import vtk 22 | import vtk.util.numpy_support as vtknp 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def sitk2vtk(image: sitk.Image) -> vtk.vtkImageData: 28 | """Convert a 2D or 3D SimpleITK image to a VTK image. 29 | 30 | VTK versions prior to version 9 do not support a direction cosine 31 | matrix. If the installed version is lower than that, the direction 32 | cosine matrix is ignored and that information is lost. A warning 33 | is issued using the Python logging mechanism. 34 | 35 | VTK images are fundamentally 3D, so 2D images are made 3D with 36 | a Z dimension of 1. 37 | 38 | :param image: Image to convert. 39 | :return: A VTK image. 40 | """ 41 | 42 | size = list(image.GetSize()) 43 | if len(size) > 3: 44 | raise ValueError( 45 | "Conversion only supports 2D and 3D images, got {len(size)}D image" 46 | ) 47 | 48 | origin = image.GetOrigin() 49 | spacing = image.GetSpacing() 50 | direction = image.GetDirection() 51 | ncomp = image.GetNumberOfComponentsPerPixel() 52 | 53 | # VTK expects 3-dimensional image parameters 54 | if len(size) == 2: 55 | size.append(1) 56 | origin = origin + (0.0,) 57 | spacing = spacing + (1.0,) 58 | direction = [ 59 | direction[0], 60 | direction[1], 61 | 0.0, 62 | direction[2], 63 | direction[3], 64 | 0.0, 65 | 0.0, 66 | 0.0, 67 | 1.0, 68 | ] 69 | 70 | # Create VTK image and set its metadata 71 | vtk_image = vtk.vtkImageData() 72 | vtk_image.SetDimensions(size) 73 | vtk_image.SetSpacing(spacing) 74 | vtk_image.SetOrigin(origin) 75 | vtk_image.SetExtent(0, size[0] - 1, 0, size[1] - 1, 0, size[2] - 1) 76 | if vtk.vtkVersion.GetVTKMajorVersion() < 9: 77 | logger.warning( 78 | "VTK version <9 does not support direction matrix which is ignored" 79 | ) 80 | else: 81 | vtk_image.SetDirectionMatrix(direction) 82 | 83 | # Set pixel data 84 | depth_array = vtknp.numpy_to_vtk(sitk.GetArrayFromImage(image).ravel()) 85 | depth_array.SetNumberOfComponents(ncomp) 86 | vtk_image.GetPointData().SetScalars(depth_array) 87 | 88 | vtk_image.Modified() 89 | return vtk_image 90 | 91 | 92 | def vtk2sitk(image: vtk.vtkImageData) -> sitk.Image: 93 | """Convert a VTK image to a SimpleITK image. 94 | 95 | Note that VTK images are fundamentally 3D, even if the Z 96 | dimension is 1. 97 | 98 | :param image: Image to convert. 99 | :return: A SimpleITK image. 100 | """ 101 | sd = image.GetPointData().GetScalars() 102 | npdata = vtknp.vtk_to_numpy(sd) 103 | dims = list(image.GetDimensions()) 104 | dims.reverse() 105 | ncomp = image.GetNumberOfScalarComponents() 106 | if ncomp > 1: 107 | dims.append(ncomp) 108 | 109 | npdata.shape = tuple(dims) 110 | 111 | sitk_image = sitk.GetImageFromArray(npdata) 112 | sitk_image.SetSpacing(image.GetSpacing()) 113 | sitk_image.SetOrigin(image.GetOrigin()) 114 | # By default, direction is identity. 115 | 116 | if vtk.vtkVersion.GetVTKMajorVersion() >= 9: 117 | # Copy the direction matrix into a list 118 | dir_mat = image.GetDirectionMatrix() 119 | direction = [0] * 9 120 | dir_mat.DeepCopy(direction, dir_mat) 121 | sitk_image.SetDirection(direction) 122 | 123 | return sitk_image 124 | -------------------------------------------------------------------------------- /SimpleITK/utilities/pyside.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | import numpy as np 21 | from PySide6.QtGui import QPixmap, QImage 22 | 23 | 24 | def sitk2qimage(image: sitk.Image) -> QImage: 25 | """Convert a SimpleITK.Image to PySide QImage. 26 | 27 | Works with 2D images, grayscale or three channel, where it is assumed that 28 | the three channels represent values in the RGB color space. In SimpleITK there is no notion 29 | of color space, so if the three channels are in the HSV colorspace the display 30 | will look strange. If the SimpleITK pixel type represents a high dynamic range, 31 | the intensities are linearly scaled to [0,255]. 32 | 33 | :param image: Image to convert. 34 | :return: A QImage. 35 | """ 36 | number_components_per_pixel = image.GetNumberOfComponentsPerPixel() 37 | if number_components_per_pixel not in [1, 3]: 38 | raise ValueError( 39 | f"SimpleITK image has {number_components_per_pixel} channels, expected 1 or 3 channels" 40 | ) 41 | if number_components_per_pixel == 3 and image.GetPixelID() != sitk.sitkVectorUInt8: 42 | raise ValueError( 43 | f"SimpleITK three channel image has pixel type ({image.GetPixelIDTypeAsString()}), expected vector 8-bit unsigned integer" 44 | ) 45 | 46 | if number_components_per_pixel == 1 and image.GetPixelID() != sitk.sitkUInt8: 47 | image = sitk.Cast( 48 | sitk.RescaleIntensity(image, outputMinimum=0, outputMaximum=255), 49 | sitk.sitkUInt8, 50 | ) 51 | arr = sitk.GetArrayViewFromImage(image) 52 | return QImage( 53 | arr.data, 54 | image.GetWidth(), 55 | image.GetHeight(), 56 | arr.strides[0], # number of bytes per row 57 | QImage.Format_Grayscale8 58 | if number_components_per_pixel == 1 59 | else QImage.Format_RGB888, 60 | ) 61 | 62 | 63 | def sitk2qpixmap(image: sitk.Image) -> QPixmap: 64 | """Convert a SimpleITK.Image to PySide QPixmap. 65 | 66 | Works with 2D images, grayscale or three channel, where it is assumed that 67 | the three channels represent values in the RGB color space. In SimpleITK there is no notion 68 | of color space, so if the three channels are in the HSV colorspace the display 69 | will look strange. If the SimpleITK pixel type represents a high dynamic range, 70 | the intensities are linearly scaled to [0,255]. 71 | 72 | :param image: Image to convert. 73 | :return: A QPixmap. 74 | """ 75 | return QPixmap.fromImage(sitk2qimage(image)) 76 | 77 | 78 | def qimage2sitk(image: QImage) -> sitk.Image: 79 | """Convert a QImage to SimpleITK.Image. 80 | 81 | If the QImage contains a grayscale image will return a scalar 82 | SimpleITK image. Otherwise, returns a three channel RGB image. 83 | 84 | :param image: QImage to convert. 85 | :return: A SimpleITK image, single channel or three channel RGB. 86 | """ 87 | # Use constBits() to get the raw data without copying (bits() returns a deep copy). 88 | # Then reshape the array to the image shape. 89 | is_vector = True 90 | # Convert image to Format_RGB888 because it keeps the byte order 91 | # regardless of big/little endian (RGBA8888 doesn't). 92 | image = image.convertToFormat(QImage.Format_RGB888) 93 | arr = np.ndarray( 94 | (image.height(), image.width(), 3), 95 | buffer=image.constBits(), 96 | strides=[image.bytesPerLine(), 3, 1], 97 | dtype=np.uint8, 98 | ) 99 | if image.isGrayscale(): 100 | arr = arr[:, :, 0] 101 | is_vector = False 102 | return sitk.GetImageFromArray(arr, isVector=is_vector) 103 | 104 | 105 | def qpixmap2sitk(pixmap: QPixmap) -> sitk.Image: 106 | """Convert a QPixmap to SimpleITK.Image. 107 | 108 | If the QPixmap contains a grayscale image will return a scalar 109 | SimpleITK image. Otherwise, returns a four channel RGBA image. 110 | 111 | :param qpixmap: QPixmap to convert. 112 | :return: A SimpleITK image, single channel or four channel. 113 | """ 114 | return qimage2sitk(pixmap.toImage()) 115 | -------------------------------------------------------------------------------- /SimpleITK/utilities/resize.py: -------------------------------------------------------------------------------- 1 | import SimpleITK as sitk 2 | from typing import Sequence 3 | from typing import Union 4 | from math import ceil 5 | import collections 6 | 7 | 8 | def _issequence(obj): 9 | if isinstance(obj, (bytes, str)): 10 | return False 11 | return isinstance(obj, collections.abc.Sequence) 12 | 13 | 14 | def resize( 15 | image: sitk.Image, 16 | new_size: Sequence[int], 17 | isotropic: bool = True, 18 | fill: bool = True, 19 | interpolator=sitk.sitkLinear, 20 | fill_value: float = 0.0, 21 | use_nearest_extrapolator: bool = False, 22 | anti_aliasing_sigma: Union[None, float, Sequence[float]] = None, 23 | ) -> sitk.Image: 24 | """ 25 | Resize an image to an arbitrary size while retaining the original image's spatial location. 26 | 27 | Allows for specification of the target image size in pixels, and whether the image pixels spacing should be 28 | isotropic. The physical extent of the image's data is retained in the new image, with the new image's spacing 29 | adjusted to achieve the desired size. The image is centered in the new image. 30 | 31 | Anti-aliasing is enabled by default. 32 | 33 | Runtime performance can be increased by disabling anti-aliasing ( anti_aliasing_sigma=0 ), and by setting 34 | the interpolator to sitkNearestNeighbor at the cost of decreasing image quality. 35 | 36 | :param image: A SimpleITK image. 37 | :param new_size: The new image size in pixels. 38 | :param isotropic: If False, the original image is resized to fill the new image size by adjusting space. If True, 39 | the new image's spacing will be isotropic. 40 | :param fill: If True, the output image will be new_size, and the original image will be centered in the new image 41 | with constant or nearest values used to fill in the new image. If False and isotropic is True, the output image's 42 | new size will be calculated to fit the original image's extent such that at least one dimension is equal to 43 | new_size. 44 | :param fill_value: Value used for padding. 45 | :param interpolator: Interpolator used for resampling. 46 | :param use_nearest_extrapolator: If True, use a nearest neighbor for extrapolation when resampling, overridding the 47 | constant fill value. 48 | :param anti_aliasing_sigma: If zero no antialiasing is performed. If a scalar, it is used as the sigma value in 49 | physical units for all axes. If None or a sequence, the sigma value for each axis is calculated as 50 | $sigma = (new_spacing - old_spacing) / 2$ in physical units. Gaussian smoothing is performed prior to resampling 51 | for antialiasing. 52 | :return: A SimpleITK image with desired size. 53 | """ 54 | new_spacing = [ 55 | (osz * ospc) / nsz 56 | for ospc, osz, nsz in zip(image.GetSpacing(), image.GetSize(), new_size) 57 | ] 58 | 59 | if isotropic: 60 | new_spacing = [max(new_spacing)] * image.GetDimension() 61 | if not fill: 62 | new_size = [ 63 | ceil(osz * ospc / nspc) 64 | for ospc, osz, nspc in zip(image.GetSpacing(), image.GetSize(), new_spacing) 65 | ] 66 | 67 | center_cidx = [0.5 * (sz - 1) for sz in image.GetSize()] 68 | new_center_cidx = [0.5 * (sz - 1) for sz in new_size] 69 | 70 | new_origin_cidx = [0] * image.GetDimension() 71 | # The continuous index of the new center of the image, in the original image's continuous index space. 72 | for i in range(image.GetDimension()): 73 | new_origin_cidx[i] = center_cidx[i] - new_center_cidx[i] * ( 74 | new_spacing[i] / image.GetSpacing()[i] 75 | ) 76 | 77 | new_origin = image.TransformContinuousIndexToPhysicalPoint(new_origin_cidx) 78 | 79 | input_pixel_type = image.GetPixelID() 80 | 81 | if anti_aliasing_sigma is None: 82 | # (s-1)/2.0 is the standard deviation of the Gaussian kernel in index space, where s downsample factor defined 83 | # by nspc/ospc. 84 | anti_aliasing_sigma = [ 85 | max((nspc - ospc) / 2.0, 0.0) 86 | for ospc, nspc in zip(image.GetSpacing(), new_spacing) 87 | ] 88 | elif not _issequence(anti_aliasing_sigma): 89 | anti_aliasing_sigma = [anti_aliasing_sigma] * image.GetDimension() 90 | 91 | if any([s < 0.0 for s in anti_aliasing_sigma]): 92 | raise ValueError("anti_aliasing_sigma must be positive, or None.") 93 | if len(anti_aliasing_sigma) != image.GetDimension(): 94 | raise ValueError( 95 | "anti_aliasing_sigma must be a scalar or a sequence of length equal to the image dimension." 96 | ) 97 | 98 | if all([s > 0.0 for s in anti_aliasing_sigma]): 99 | image = sitk.SmoothingRecursiveGaussian(image, anti_aliasing_sigma) 100 | else: 101 | for d, s in enumerate(anti_aliasing_sigma): 102 | if s > 0.0: 103 | image = sitk.RecursiveGaussian(image, sigma=s, direction=d) 104 | 105 | return sitk.Resample( 106 | image, 107 | size=new_size, 108 | outputOrigin=new_origin, 109 | outputSpacing=new_spacing, 110 | outputDirection=image.GetDirection(), 111 | defaultPixelValue=fill_value, 112 | interpolator=interpolator, 113 | useNearestNeighborExtrapolator=use_nearest_extrapolator, 114 | outputPixelType=input_pixel_type, 115 | ) 116 | -------------------------------------------------------------------------------- /SimpleITK/utilities/fft.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | 21 | 22 | def fft_based_translation_initialization( 23 | fixed: sitk.Image, 24 | moving: sitk.Image, 25 | *, 26 | required_fraction_of_overlapping_pixels: float = 0.0, 27 | initial_transform: sitk.Transform = None, 28 | masked_pixel_value: float = None, 29 | ) -> sitk.TranslationTransform: 30 | """Perform fast Fourier transform based normalized correlation to find the translation which maximizes correlation 31 | between the images. 32 | 33 | If the moving image grid is not congruent with fixed image ( same origin, spacing and direction ), then it will be 34 | resampled onto the grid defined by the fixed image. 35 | 36 | Efficiency can be improved by reducing the resolution of the image or using a projection filter to reduce the 37 | dimensionality of the inputs. 38 | 39 | :param fixed: A SimpleITK image object. 40 | :param moving: Another SimpleITK Image object, which will be resampled onto the grid of the fixed image if it is not 41 | congruent. 42 | :param required_fraction_of_overlapping_pixels: The required fraction of overlapping pixels between the fixed and 43 | moving image. The value should be in the range of [0, 1]. If the value is 1, then the full overlap is required. 44 | :param initial_transform: An initial transformation to be applied to the moving image by resampling before the 45 | FFT registration. The returned transform will be of the initial_transform type with the translation updated. 46 | :param masked_pixel_value: The value of input pixels to be ignored by correlation. If None, then the 47 | FFTNormalizedCoorrelation will be used, otherwise the MaskedFFTNormalizedCorrelation will be used. 48 | :return: A TranslationTransform (or the initial_transform tyype) mapping physical points from the fixed to the 49 | moving image. 50 | """ 51 | 52 | if ( 53 | initial_transform is not None 54 | or moving.GetSpacing() != fixed.GetSpacing() 55 | or moving.GetDirection() != fixed.GetDirection() 56 | or moving.GetOrigin() != fixed.GetOrigin() 57 | ): 58 | resampler = sitk.ResampleImageFilter() 59 | resampler.SetReferenceImage(fixed) 60 | 61 | if initial_transform is not None: 62 | resampler.SetTransform(initial_transform) 63 | moving = resampler.Execute(moving) 64 | 65 | sigma = fixed.GetSpacing()[0] 66 | pixel_type = sitk.sitkFloat32 67 | 68 | fixed = sitk.Cast(sitk.SmoothingRecursiveGaussian(fixed, sigma), pixel_type) 69 | moving = sitk.Cast(sitk.SmoothingRecursiveGaussian(moving, sigma), pixel_type) 70 | 71 | if masked_pixel_value is None: 72 | xcorr = sitk.FFTNormalizedCorrelation( 73 | fixed, 74 | moving, 75 | requiredFractionOfOverlappingPixels=required_fraction_of_overlapping_pixels, 76 | ) 77 | else: 78 | xcorr = sitk.MaskedFFTNormalizedCorrelation( 79 | fixed, 80 | moving, 81 | sitk.Cast(fixed != masked_pixel_value, pixel_type), 82 | sitk.Cast(moving != masked_pixel_value, pixel_type), 83 | requiredFractionOfOverlappingPixels=required_fraction_of_overlapping_pixels, 84 | ) 85 | 86 | xcorr = sitk.SmoothingRecursiveGaussian(xcorr, sigma) 87 | 88 | cc = sitk.ConnectedComponent(sitk.RegionalMaxima(xcorr, fullyConnected=True)) 89 | stats = sitk.LabelStatisticsImageFilter() 90 | stats.Execute(xcorr, cc) 91 | labels = sorted(stats.GetLabels(), key=lambda l: stats.GetMean(l)) 92 | 93 | peak_bb = stats.GetBoundingBox(labels[-1]) 94 | # Add 0.5 for center of voxel on continuous index 95 | peak_idx = [ 96 | (min_idx + max_idx) / 2.0 + 0.5 97 | for min_idx, max_idx in zip(peak_bb[0::2], peak_bb[1::2]) 98 | ] 99 | 100 | peak_pt = xcorr.TransformContinuousIndexToPhysicalPoint(peak_idx) 101 | peak_value = stats.GetMean(labels[-1]) 102 | 103 | center_pt = xcorr.TransformContinuousIndexToPhysicalPoint( 104 | [p / 2.0 for p in xcorr.GetSize()] 105 | ) 106 | 107 | translation = [c - p for c, p in zip(center_pt, peak_pt)] 108 | if initial_transform is not None: 109 | offset = initial_transform.TransformVector(translation, point=[0, 0]) 110 | 111 | tx_out = sitk.Transform(initial_transform).Downcast() 112 | tx_out.SetTranslation( 113 | [a + b for (a, b) in zip(initial_transform.GetTranslation(), offset)] 114 | ) 115 | return tx_out 116 | 117 | return sitk.TranslationTransform(xcorr.GetDimension(), translation) 118 | -------------------------------------------------------------------------------- /SimpleITK/utilities/overlay_bounding_boxes.py: -------------------------------------------------------------------------------- 1 | # ======================================================================== 2 | # 3 | # Copyright NumFOCUS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0.txt 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ======================================================================== 18 | 19 | import SimpleITK as sitk 20 | from typing import Sequence, Tuple 21 | 22 | 23 | def overlay_bounding_boxes( 24 | image: sitk.Image, 25 | bounding_boxes: Sequence[Sequence[float]], 26 | bounding_box_format: str = "MINXY_MAXXY", 27 | normalized: bool = False, 28 | colors: Sequence[int] = [], 29 | half_line_width: int = 0, 30 | ) -> Tuple[sitk.Image, bool]: 31 | """ 32 | Overlay axis aligned bounding boxes on a 2D image. The function supports several ways of specifying a 33 | bounding box using pixel indexes: 34 | 35 | 1. "MINXY_MAXXY" - [min_x, min_y, max_x, max_y] 36 | 2. "MINXY_WH" - [min_x, min_y, width, height] 37 | 3. "CENT_WH" - [center_x, center_y, width, height] 38 | 4. "CENT_HALFWH" - [center_x, center_y, width/2, height/2] 39 | 40 | Bounding boxes are plotted in the order they appear in the iterable/list. To change the overlap between rectangles 41 | change the order in the list. The last entry in the list will be plotted on top of the previous ones. 42 | 43 | Caveat: When using larger line widths, bounding boxes that are very close to the image border may cause an exception 44 | and result in partial overlay. A trivial solution is to decrease the value of the half_line_width parameter. 45 | 46 | :param image: Input image, 2D image with scalar or RGB pixels on which we plot the bounding boxes. 47 | :param bounding_boxes: Bounding boxes to plot. Each bounding box is represented by four numbers. 48 | :param bounding_box_format: One of ["MINXY_MAXXY", "MINXY_WH", "CENT_WH", "CENT_HALFWH"] specifying the meaning of 49 | the four entries representing the bounding box. 50 | :param normalized: Indicate whether the bounding box numbers were normalized to be in [0,1]. 51 | :param colors: Specify the color for each rectangle using RGB values, triplets in [0,255]. 52 | Useful for visually representing different classes (relevant for object detection). Most often a flat 53 | list, e.g. colors = [255, 0, 0, 0, 255, 0] reresents red, and green. 54 | :param half_line_width: Plot using thicker lines. 55 | :return: A tuple where the first entry is a SimpleITK image with rectangles plotted on it and the second entry is a boolean 56 | which is true if one or more of the rectangles were out of bounds, false otherwise. 57 | """ 58 | # functions that convert from various bounding box representations to the [min_x, min_y, max_x, max_y] representation. 59 | convert_to_minxy_maxxy = { 60 | "MINXY_MAXXY": lambda original: original, 61 | "MINXY_WH": lambda original: [ 62 | [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]] 63 | for bbox in original 64 | ], 65 | "CENT_WH": lambda original: [ 66 | [ 67 | bbox[0] - bbox[2] / 2.0, 68 | bbox[1] - bbox[3] / 2.0, 69 | bbox[0] + bbox[2] / 2.0, 70 | bbox[1] + bbox[3] / 2.0, 71 | ] 72 | for bbox in original 73 | ], 74 | "CENT_HALFWH": lambda original: [ 75 | [bbox[0] - bbox[2], bbox[1] - bbox[3], bbox[0] + bbox[2], bbox[1] + bbox[3]] 76 | for bbox in original 77 | ], 78 | } 79 | 80 | # Confirm image is in expected format 81 | pixel_type = image.GetPixelID() 82 | num_channels = image.GetNumberOfComponentsPerPixel() 83 | if pixel_type not in [sitk.sitkUInt8, sitk.sitkVectorUInt8]: 84 | raise ValueError( 85 | f"Image channels expected to have type of 8-bit unsigned integer, got ({image.GetPixelIDTypeAsString()})" 86 | ) 87 | if num_channels not in [1, 3]: 88 | raise ValueError( 89 | f"Image expected to have one or three channels, got ({num_channels})" 90 | ) 91 | if num_channels == 3: 92 | overlay_image = sitk.Image(image) 93 | else: 94 | overlay_image = sitk.Compose([image] * 3) 95 | if half_line_width < 0: 96 | raise ValueError( 97 | f"Half line width parameter expected to be non-negative, got ({half_line_width})" 98 | ) 99 | # Convert bounding box information into standard format, based on user specification of the original format 100 | try: 101 | standard_bounding_boxes = convert_to_minxy_maxxy[bounding_box_format]( 102 | bounding_boxes 103 | ) 104 | if normalized: 105 | scale_x, scale_y = image.GetSize() 106 | standard_bounding_boxes = [ 107 | [ 108 | bbox[0] * scale_x, 109 | bbox[1] * scale_y, 110 | bbox[2] * scale_x, 111 | bbox[3] * scale_y, 112 | ] 113 | for bbox in standard_bounding_boxes 114 | ] 115 | # round to integer coordinates 116 | standard_bounding_boxes = [ 117 | [int(b + 0.5) for b in bbox] for bbox in standard_bounding_boxes 118 | ] 119 | except KeyError: 120 | raise ValueError( 121 | f"Unknown bounding box format ({bounding_box_format}), valid values are [MINXY_WH, MINXY_MAXXY, CENT_WH, CENT_HALFWH]" 122 | ) 123 | if not colors: # use a single color for all bounding boxes 124 | colors = [[255, 0, 0]] * len(standard_bounding_boxes) 125 | else: 126 | colors = [colors[i : i + 3] for i in range(0, len(colors), 3)] 127 | line_width = 1 + 2 * half_line_width 128 | out_of_bounds = False 129 | for bbox, color in zip(standard_bounding_boxes, colors): 130 | width = bbox[2] - bbox[0] 131 | height = bbox[3] - bbox[1] 132 | vert = sitk.Compose( 133 | [ 134 | sitk.Image([line_width, height + line_width], sitk.sitkUInt8) 135 | + color[0], 136 | sitk.Image([line_width, height + line_width], sitk.sitkUInt8) 137 | + color[1], 138 | sitk.Image([line_width, height + line_width], sitk.sitkUInt8) 139 | + color[2], 140 | ] 141 | ) 142 | horiz = sitk.Compose( 143 | [ 144 | sitk.Image([width, line_width], sitk.sitkUInt8) + color[0], 145 | sitk.Image([width, line_width], sitk.sitkUInt8) + color[1], 146 | sitk.Image([width, line_width], sitk.sitkUInt8) + color[2], 147 | ] 148 | ) 149 | try: 150 | overlay_image[ 151 | bbox[0] - half_line_width : bbox[0] + half_line_width + 1, # noqa E203 152 | bbox[1] - half_line_width : bbox[3] + half_line_width + 1, # noqa E203 153 | ] = vert 154 | overlay_image[ 155 | bbox[2] - half_line_width : bbox[2] + half_line_width + 1, # noqa E203 156 | bbox[1] - half_line_width : bbox[3] + half_line_width + 1, # noqa E203 157 | ] = vert 158 | overlay_image[ 159 | bbox[0] : bbox[2], # noqa E203 160 | bbox[1] - half_line_width : bbox[1] + half_line_width + 1, # noqa E203 161 | ] = horiz 162 | overlay_image[ 163 | bbox[0] : bbox[2], # noqa E203 164 | bbox[3] - half_line_width : bbox[3] + half_line_width + 1, # noqa E203 165 | ] = horiz 166 | except Exception: # Drawing outside the border of the image will cause problems 167 | out_of_bounds = True 168 | continue 169 | return overlay_image, out_of_bounds 170 | -------------------------------------------------------------------------------- /test/test_utilities.py: -------------------------------------------------------------------------------- 1 | import math 2 | import SimpleITK as sitk 3 | import SimpleITK.utilities as sitkutils 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | def test_Logger(): 8 | logger = sitkutils.Logger() 9 | 10 | 11 | def test_make_isotropic(): 12 | img = sitk.Image([10, 10, 5], sitk.sitkFloat32) 13 | img.SetSpacing([0.3, 0.3, 0.6]) 14 | 15 | sitkutils.make_isotropic(img) 16 | 17 | 18 | slice_call = 0 19 | 20 | 21 | def test_slice_by_slice(): 22 | @sitkutils.slice_by_slice 23 | def f(_img): 24 | global slice_call 25 | 26 | _img[:] = slice_call 27 | slice_call = 1 + slice_call 28 | return _img 29 | 30 | img = sitk.Image([10, 10, 5], sitk.sitkFloat32) 31 | img = f(img) 32 | 33 | for z in range(img.GetSize()[2]): 34 | assert img[0, 0, z] == z 35 | 36 | 37 | def test_fft_initialization(): 38 | fixed_img = sitk.Image([1024, 512], sitk.sitkInt8) 39 | 40 | fixed_img[510:520, 255:265] = 10 41 | 42 | moving_img = sitk.Image([1024, 512], sitk.sitkInt8) 43 | moving_img[425:435, 300:320] = 8 44 | 45 | tx = sitkutils.fft_based_translation_initialization(fixed_img, moving_img) 46 | assert tx.GetOffset() == (-85.0, 50.0) 47 | 48 | 49 | def test_fft_initialization2(): 50 | """Testing with different spacing and origin to force resampling.""" 51 | fixed_img = sitk.Image([1024, 512], sitk.sitkUInt8) 52 | 53 | fixed_img[510:520, 255:265] = 10 54 | 55 | moving_img = sitk.Image([1024, 512], sitk.sitkUInt8) 56 | moving_img.SetSpacing([1, 0.5]) 57 | moving_img.SetOrigin([0, -0.25]) 58 | moving_img[425:435, 305:315] = 8 59 | 60 | tx = sitkutils.fft_based_translation_initialization(fixed_img, moving_img) 61 | assert tx.GetOffset() == (-85.0, -105.0) 62 | 63 | 64 | def test_fft_initialization3(): 65 | """Testing with required fraction of overlapping pixels.""" 66 | fixed_img = sitk.Image([1024, 512], sitk.sitkUInt8) 67 | fixed_img[0:10, 0:20] = 10 68 | fixed_img[510:520, 255:265] = 10 69 | 70 | moving_img = sitk.Image([1024, 512], sitk.sitkUInt8) 71 | moving_img[425:435, 300:320] = 8 72 | 73 | tx = sitkutils.fft_based_translation_initialization( 74 | fixed_img, 75 | moving_img, 76 | ) 77 | assert tx.GetOffset() == (425, 300.0) 78 | 79 | tx = sitkutils.fft_based_translation_initialization( 80 | fixed_img, moving_img, required_fraction_of_overlapping_pixels=0.5 81 | ) 82 | assert tx.GetOffset() == (-85.0, 50.0) 83 | 84 | 85 | def test_fft_initialization4(): 86 | """Testing with initial transform.""" 87 | fixed_img = sitk.Image([1024, 512], sitk.sitkUInt8) 88 | fixed_img[510:520, 255:265] = 10 89 | 90 | moving_img = sitk.Image([1024, 512], sitk.sitkUInt8) 91 | moving_img.SetSpacing((10, 10)) 92 | moving_img[425:435, 300:310] = 8 93 | 94 | initial_transform = sitk.Similarity2DTransform(10) 95 | 96 | tx = sitkutils.fft_based_translation_initialization( 97 | fixed_img, moving_img, initial_transform=initial_transform 98 | ) 99 | assert tx.GetTranslation() == (-850.0, 450.0) 100 | 101 | 102 | def test_overlay_bounding_boxes(): 103 | bounding_boxes = [[10, 10, 60, 20], [200, 180, 230, 250]] 104 | scalar_image = sitk.Image([256, 256], sitk.sitkUInt8) 105 | rgb_image = sitk.Compose([scalar_image, scalar_image, scalar_image + 255]) 106 | 107 | scalar_hash = sitk.Hash( 108 | sitkutils.overlay_bounding_boxes( 109 | image=scalar_image, 110 | bounding_boxes=bounding_boxes, 111 | bounding_box_format="MINXY_MAXXY", 112 | )[0] 113 | ) 114 | rgb_hash = sitk.Hash( 115 | sitkutils.overlay_bounding_boxes( 116 | image=rgb_image, 117 | bounding_boxes=bounding_boxes, 118 | colors=[255, 20, 147, 255, 215, 0], 119 | half_line_width=1, 120 | bounding_box_format="MINXY_MAXXY", 121 | )[0] 122 | ) 123 | assert ( 124 | scalar_hash == "d7dde3eee4c334ffe810a636dff872a6ded592fc" 125 | and rgb_hash == "d6694a394f8fcc32ea337a1f9531dda6f4884af1" 126 | ) 127 | 128 | 129 | def test_resize(): 130 | original_image = sitk.Image([128, 128], sitk.sitkUInt8) + 50 131 | resized_image = sitkutils.resize(image=original_image, new_size=[128, 128]) 132 | assert resized_image.GetSize() == (128, 128) 133 | assert resized_image.GetSpacing() == (1.0, 1.0) 134 | assert resized_image.GetOrigin() == (0.0, 0.0) 135 | assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == ( 136 | -0.5, 137 | -0.5, 138 | ) 139 | 140 | resized_image = sitkutils.resize(image=original_image, new_size=[64, 64]) 141 | assert resized_image.GetSize() == (64, 64) 142 | assert resized_image.GetSpacing() == (2.0, 2.0) 143 | assert resized_image.GetOrigin() == (0.5, 0.5) 144 | assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == ( 145 | -0.5, 146 | -0.5, 147 | ) 148 | 149 | resized_image = sitkutils.resize( 150 | image=original_image, new_size=[64, 128], fill=False 151 | ) 152 | assert resized_image.GetSize() == (64, 64) 153 | assert resized_image.GetSpacing() == (2.0, 2.0) 154 | assert resized_image.GetOrigin() == (0.5, 0.5) 155 | assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == ( 156 | -0.5, 157 | -0.5, 158 | ) 159 | 160 | resized_image = sitkutils.resize( 161 | image=original_image, new_size=[64, 128], isotropic=False 162 | ) 163 | assert resized_image.GetSize() == (64, 128) 164 | assert resized_image.GetSpacing() == (2.0, 1.0) 165 | assert resized_image.GetOrigin() == (0.5, 0.0) 166 | assert resized_image.TransformContinuousIndexToPhysicalPoint((-0.5, -0.5)) == ( 167 | -0.5, 168 | -0.5, 169 | ) 170 | 171 | 172 | def test_resize_3d(): 173 | original_image = sitk.Image([128, 128, 128], sitk.sitkUInt8) + 50 174 | resized_image = sitkutils.resize(image=original_image, new_size=[128, 128, 128]) 175 | assert resized_image.GetSize() == (128, 128, 128) 176 | assert resized_image.GetSpacing() == (1.0, 1.0, 1.0) 177 | assert resized_image.GetOrigin() == (0.0, 0.0, 0.0) 178 | assert resized_image.TransformContinuousIndexToPhysicalPoint( 179 | (-0.5, -0.5, -0.5) 180 | ) == ( 181 | -0.5, 182 | -0.5, 183 | -0.5, 184 | ) 185 | 186 | resized_image = sitkutils.resize(image=original_image, new_size=[64, 64, 64]) 187 | assert resized_image.GetSize() == (64, 64, 64) 188 | assert resized_image.GetSpacing() == (2.0, 2.0, 2.0) 189 | assert resized_image.GetOrigin() == (0.5, 0.5, 0.5) 190 | assert resized_image.TransformContinuousIndexToPhysicalPoint( 191 | (-0.5, -0.5, -0.5) 192 | ) == ( 193 | -0.5, 194 | -0.5, 195 | -0.5, 196 | ) 197 | 198 | resized_image = sitkutils.resize(image=original_image, new_size=[64, 32, 64]) 199 | assert resized_image.GetSize() == (64, 32, 64) 200 | assert resized_image.GetSpacing() == (4.0, 4.0, 4.0) 201 | assert resized_image.GetOrigin() == (-62.5, 1.5, -62.5) 202 | assert resized_image.TransformContinuousIndexToPhysicalPoint( 203 | (-0.5, -0.5, -0.5) 204 | ) == ( 205 | -64.5, 206 | -0.5, 207 | -64.5, 208 | ) 209 | 210 | resized_image = sitkutils.resize( 211 | image=original_image, new_size=[64, 64, 32], fill=False 212 | ) 213 | assert resized_image.GetSize() == (32, 32, 32) 214 | assert resized_image.GetSpacing() == (4.0, 4.0, 4.0) 215 | assert resized_image.GetOrigin() == (1.5, 1.5, 1.5) 216 | assert resized_image.TransformContinuousIndexToPhysicalPoint( 217 | (-0.5, -0.5, -0.5) 218 | ) == ( 219 | -0.5, 220 | -0.5, 221 | -0.5, 222 | ) 223 | 224 | resized_image = sitkutils.resize( 225 | image=original_image, new_size=[32, 64, 64], isotropic=False 226 | ) 227 | assert resized_image.GetSize() == (32, 64, 64) 228 | assert resized_image.GetSpacing() == (4.0, 2.0, 2.0) 229 | assert resized_image.GetOrigin() == (1.5, 0.5, 0.5) 230 | assert resized_image.TransformContinuousIndexToPhysicalPoint( 231 | (-0.5, -0.5, -0.5) 232 | ) == ( 233 | -0.5, 234 | -0.5, 235 | -0.5, 236 | ) 237 | 238 | 239 | def test_resize_fill(): 240 | original_image = sitk.Image([16, 32], sitk.sitkFloat32) + 1.0 241 | 242 | resized_image = sitkutils.resize( 243 | image=original_image, new_size=[32, 32], fill=True, fill_value=10.0 244 | ) 245 | assert resized_image.GetSize() == (32, 32) 246 | assert resized_image.GetSpacing() == (1.0, 1.0) 247 | assert resized_image.GetOrigin() == (-8.0, 0.0) 248 | assert resized_image[0, 0] == 10.0 249 | assert resized_image[15, 15] == 1.0 250 | assert resized_image[31, 31] == 10.0 251 | 252 | resized_image = sitkutils.resize( 253 | image=original_image, 254 | new_size=[32, 32], 255 | fill=True, 256 | use_nearest_extrapolator=True, 257 | ) 258 | assert resized_image.GetSize() == (32, 32) 259 | assert resized_image.GetSpacing() == (1.0, 1.0) 260 | assert resized_image.GetOrigin() == (-8.0, 0.0) 261 | assert resized_image[0, 0] == 1.0 262 | assert resized_image[15, 15] == 1.0 263 | assert resized_image[31, 31] == 1.0 264 | 265 | 266 | def test_resize_anti_aliasing(): 267 | original_image = sitk.Image([5, 5], sitk.sitkFloat32) 268 | original_image[2, 2] = 1.0 269 | 270 | resized_image = sitkutils.resize( 271 | image=original_image, 272 | new_size=[3, 3], 273 | interpolator=sitk.sitkNearestNeighbor, 274 | anti_aliasing_sigma=0, 275 | ) 276 | assert resized_image.GetSize() == (3, 3) 277 | assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3)) 278 | assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3)) 279 | assert resized_image[0, 0] == 0.0 280 | assert resized_image[1, 1] == 1.0 281 | assert resized_image[1, 0] == 0.0 282 | assert resized_image[0, 1] == 0.0 283 | 284 | resized_image = sitkutils.resize( 285 | image=original_image, 286 | new_size=[3, 3], 287 | interpolator=sitk.sitkNearestNeighbor, 288 | anti_aliasing_sigma=None, 289 | ) 290 | assert resized_image.GetSize() == (3, 3) 291 | assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3)) 292 | assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3)) 293 | assert math.isclose(resized_image[1, 1], 0.960833, abs_tol=1e-6) 294 | assert resized_image[1, 0] == resized_image[0, 1] 295 | assert resized_image[0, 0] == resized_image[2, 2] 296 | 297 | resized_image = sitkutils.resize( 298 | image=original_image, 299 | new_size=[3, 3], 300 | interpolator=sitk.sitkNearestNeighbor, 301 | anti_aliasing_sigma=0.5, 302 | ) 303 | assert resized_image.GetSize() == (3, 3) 304 | assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3)) 305 | assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3)) 306 | assert math.isclose(resized_image[1, 1], 0.621714, abs_tol=1e-6) 307 | assert math.isclose(resized_image[0, 0], 0, abs_tol=1e-6) 308 | assert math.isclose(resized_image[1, 0], resized_image[0, 1], abs_tol=1e-8) 309 | assert math.isclose(resized_image[0, 0], resized_image[2, 2], abs_tol=1e-8) 310 | 311 | resized_image = sitkutils.resize( 312 | image=original_image, 313 | new_size=[3, 3], 314 | interpolator=sitk.sitkNearestNeighbor, 315 | anti_aliasing_sigma=[1.0, 0.0], 316 | ) 317 | assert resized_image.GetSize() == (3, 3) 318 | assert_allclose(resized_image.GetSpacing(), (5 / 3, 5 / 3)) 319 | assert_allclose(resized_image.GetOrigin(), (1 / 3, 1 / 3)) 320 | assert math.isclose(resized_image[1, 1], 0.400101, abs_tol=1e-6) 321 | assert math.isclose(resized_image[0, 0], 0, abs_tol=1e-6) 322 | assert resized_image[0, 0] == resized_image[2, 2] 323 | assert resized_image[1, 0] == 0.0 324 | assert resized_image[1, 2] == 0.0 325 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------