├── tests ├── metric_ │ ├── __init__.py │ ├── binary.py │ └── histogram.py ├── .gitignore ├── __init__.py ├── io_ │ └── __init__.py ├── filter_ │ ├── __init__.py │ ├── anisotropic_diffusion.py │ └── utilities.py ├── graphcut_ │ ├── __init__.py │ └── graph.py ├── features_ │ └── __init__.py ├── README.md └── support.py ├── doc ├── .gitignore ├── source │ ├── reference │ │ ├── io.rst │ │ ├── core.rst │ │ ├── features.rst │ │ ├── filter.rst │ │ ├── graphcut.rst │ │ ├── metric.rst │ │ ├── iterators.rst │ │ ├── neighbours.rst │ │ ├── utilities.rst │ │ └── index.rst │ ├── tutorial │ │ ├── index.rst │ │ ├── loadsave.rst │ │ └── metadata.rst │ ├── information │ │ ├── index.rst │ │ └── imageformats.rst │ ├── installation │ │ ├── osx.rst │ │ ├── index.rst │ │ ├── uninstall.rst │ │ ├── python2.rst │ │ ├── windows.rst │ │ ├── conda.rst │ │ ├── fastpath.rst │ │ ├── developmentmode.rst │ │ ├── venv.rst │ │ └── graphcutsupport.rst │ ├── index.rst │ └── notebooks │ │ └── index.rst └── README.md ├── notebooks ├── scripts │ ├── output │ │ └── .gitkeep │ ├── images │ │ ├── b0.png │ │ ├── adc.png │ │ ├── b1000.png │ │ ├── contour.png │ │ ├── flair.png │ │ ├── b0wmarker.png │ │ ├── brainmask.png │ │ ├── subvolume.png │ │ ├── watershed.png │ │ ├── b0gcvoxelmax.png │ │ ├── b0gradient.png │ │ ├── watershed_fail.png │ │ ├── watershed_colored.png │ │ ├── b0gclabelstawiaski.png │ │ ├── anisotropic_diffusion.png │ │ ├── graphcut_voxel_gradient.png │ │ ├── graphcut_voxel_grayvalues.png │ │ ├── b0.txt │ │ └── b1000.txt │ ├── resources │ │ ├── b0.nii.gz │ │ ├── b1000.nii.gz │ │ ├── flair.nii.gz │ │ ├── b0markers.nii.gz │ │ └── brainmask.nii.gz │ ├── medpy_create_empty_volume_by_example.py.ipynb │ └── medpy_watershed.py.ipynb └── flair.nii.gz ├── pytest.ini ├── MANIFEST.in ├── lib └── maxflow │ └── src │ ├── BUILD │ ├── boost-build.jam │ ├── instances.inc │ ├── CMakeLists.txt │ ├── README │ ├── Jamroot │ ├── pythongraph.h │ ├── get_edge_test.py │ ├── sum_edge_test.py │ └── graph.cpp ├── .github └── workflows │ ├── pre-commit.yml │ ├── README.md │ ├── run-tests.yml │ ├── publish.yml │ ├── build-publish-test.yml │ └── run-tests-gc.yml ├── .pre-commit-config.yaml ├── medpy ├── __init__.py ├── utilities │ └── __init__.py ├── neighbours │ ├── __init__.py │ └── knn.py ├── iterators │ └── __init__.py ├── core │ ├── exceptions.py │ ├── __init__.py │ └── logger.py ├── io │ ├── __init__.py │ ├── save.py │ └── load.py ├── graphcut │ └── write.py ├── metric │ └── image.py └── filter │ └── binary.py ├── RELEASE.md ├── CHANGES.txt ├── .gitignore ├── README.md └── bin ├── medpy_set_pixel_spacing.py ├── medpy_create_empty_volume_by_example.py ├── medpy_label_count.py ├── medpy_convert.py ├── medpy_dicom_slices_to_volume.py ├── medpy_info.py ├── medpy_merge.py ├── medpy_gradient.py ├── medpy_extract_min_max.py ├── medpy_diff.py ├── medpy_fit_into_shape.py ├── medpy_swap_dimensions.py ├── medpy_label_fit_to_mask.py ├── medpy_watershed.py ├── medpy_anisotropic_diffusion.py ├── medpy_intersection.py ├── medpy_split_xd_to_xminus1d.py ├── medpy_join_masks.py ├── medpy_shrink_image.py ├── medpy_resample.py └── medpy_morphology.py /tests/metric_/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | generated/ 2 | -------------------------------------------------------------------------------- /notebooks/scripts/output/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | .hypothesis/ 2 | -------------------------------------------------------------------------------- /doc/source/reference/io.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.io 2 | -------------------------------------------------------------------------------- /doc/source/reference/core.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.core 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --import-mode=importlib 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Holds the unittests for various classes 2 | -------------------------------------------------------------------------------- /doc/source/reference/features.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.features 2 | -------------------------------------------------------------------------------- /doc/source/reference/filter.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.filter 2 | -------------------------------------------------------------------------------- /doc/source/reference/graphcut.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.graphcut 2 | -------------------------------------------------------------------------------- /doc/source/reference/metric.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.metric 2 | -------------------------------------------------------------------------------- /doc/source/reference/iterators.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.iterators 2 | -------------------------------------------------------------------------------- /doc/source/reference/neighbours.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.neighbours 2 | -------------------------------------------------------------------------------- /doc/source/reference/utilities.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: medpy.utilities 2 | -------------------------------------------------------------------------------- /notebooks/flair.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/flair.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/images/b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b0.png -------------------------------------------------------------------------------- /notebooks/scripts/images/adc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/adc.png -------------------------------------------------------------------------------- /notebooks/scripts/images/b1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b1000.png -------------------------------------------------------------------------------- /notebooks/scripts/images/contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/contour.png -------------------------------------------------------------------------------- /notebooks/scripts/images/flair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/flair.png -------------------------------------------------------------------------------- /notebooks/scripts/images/b0wmarker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b0wmarker.png -------------------------------------------------------------------------------- /notebooks/scripts/images/brainmask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/brainmask.png -------------------------------------------------------------------------------- /notebooks/scripts/images/subvolume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/subvolume.png -------------------------------------------------------------------------------- /notebooks/scripts/images/watershed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/watershed.png -------------------------------------------------------------------------------- /notebooks/scripts/resources/b0.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/resources/b0.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/images/b0gcvoxelmax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b0gcvoxelmax.png -------------------------------------------------------------------------------- /notebooks/scripts/images/b0gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b0gradient.png -------------------------------------------------------------------------------- /notebooks/scripts/resources/b1000.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/resources/b1000.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/resources/flair.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/resources/flair.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/images/watershed_fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/watershed_fail.png -------------------------------------------------------------------------------- /notebooks/scripts/images/watershed_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/watershed_colored.png -------------------------------------------------------------------------------- /notebooks/scripts/resources/b0markers.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/resources/b0markers.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/resources/brainmask.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/resources/brainmask.nii.gz -------------------------------------------------------------------------------- /notebooks/scripts/images/b0gclabelstawiaski.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/b0gclabelstawiaski.png -------------------------------------------------------------------------------- /notebooks/scripts/images/anisotropic_diffusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/anisotropic_diffusion.png -------------------------------------------------------------------------------- /doc/source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tutorial 3 | ======== 4 | 5 | .. toctree:: 6 | :glob: 7 | :maxdepth: 1 8 | 9 | * 10 | -------------------------------------------------------------------------------- /notebooks/scripts/images/graphcut_voxel_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/graphcut_voxel_gradient.png -------------------------------------------------------------------------------- /notebooks/scripts/images/graphcut_voxel_grayvalues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/medpy/HEAD/notebooks/scripts/images/graphcut_voxel_grayvalues.png -------------------------------------------------------------------------------- /doc/source/information/index.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Information 3 | =========== 4 | 5 | .. toctree:: 6 | :glob: 7 | :maxdepth: 1 8 | 9 | * 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.md 3 | 4 | include lib/maxflow/src/*.h 5 | include lib/maxflow/src/*.cpp 6 | include lib/maxflow/src/instances.inc 7 | include lib/maxflow/src/README 8 | -------------------------------------------------------------------------------- /notebooks/scripts/images/b0.txt: -------------------------------------------------------------------------------- 1 | Case courtesy of A.Prof Frank Gaillard, Radiopaedia.org. From the case rID: 33859 2 | -------------------------------------------------------------------------------- /notebooks/scripts/images/b1000.txt: -------------------------------------------------------------------------------- 1 | Case courtesy of A.Prof Frank Gaillard, Radiopaedia.org. From the case rID: 33859 2 | -------------------------------------------------------------------------------- /tests/io_/__init__.py: -------------------------------------------------------------------------------- 1 | from .loadsave import TestIOFacilities as TestIOFacilities 2 | from .metadata import TestMetadataConsistency as TestMetadataConsistency 3 | 4 | __all__ = ["TestIOFacilities", "TestMetadataConsistency"] 5 | -------------------------------------------------------------------------------- /doc/source/installation/osx.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Installing MedPy on OsX 3 | ======================= 4 | **MedPy** does not officially support OsX. Using *pip*, it can still be installed fine 5 | 6 | .. code-block:: bash 7 | 8 | python3 -m pip install medpy 9 | -------------------------------------------------------------------------------- /doc/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Reference 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | io 9 | metric 10 | filter 11 | features 12 | iterators 13 | neighbours 14 | graphcut 15 | core 16 | utilities 17 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Building the HTML documentation 2 | 3 | Install MedPy with the `[doc]` extras 4 | 5 | pip3 install medpy[doc] 6 | 7 | Then run in `docs/` 8 | 9 | sphinx-build -aE -b html source/ build/ 10 | 11 | You can now find the HTML files in the `build/` folder. 12 | -------------------------------------------------------------------------------- /doc/source/installation/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | fastpath 9 | venv 10 | developmentmode 11 | graphcutsupport 12 | windows 13 | osx 14 | conda 15 | python2 16 | uninstall 17 | -------------------------------------------------------------------------------- /lib/maxflow/src/BUILD: -------------------------------------------------------------------------------- 1 | Requires libboost-python 2 | 3 | g++ -shared -fPIC maxflow.cpp graph.cpp wrapper.cpp -I/usr/include/python2.7 -lboost_python -lpython2.7 -omaxflow.so 4 | 5 | Or, using cmake and the CMakeList.txt in this directory: 6 | 7 | mkdir build 8 | cd build 9 | cmake ../. 10 | make 11 | -------------------------------------------------------------------------------- /tests/filter_/__init__.py: -------------------------------------------------------------------------------- 1 | from .houghtransform import TestHoughTransform as TestHoughTransform 2 | from .IntensityRangeStandardization import ( 3 | TestIntensityRangeStandardization as TestIntensityRangeStandardization, 4 | ) 5 | 6 | __all__ = ["TestHoughTransform", "TestIntensityRangeStandardization"] 7 | -------------------------------------------------------------------------------- /tests/graphcut_/__init__.py: -------------------------------------------------------------------------------- 1 | # from cut import TestCut # deactivated since faulty 2 | from .energy_label import TestEnergyLabel as TestEnergyLabel 3 | from .energy_voxel import TestEnergyVoxel as TestEnergyVoxel 4 | from .graph import TestGraph as TestGraph 5 | 6 | __all__ = ["TestEnergyLabel", "TestEnergyVoxel", "TestGraph"] 7 | -------------------------------------------------------------------------------- /tests/features_/__init__.py: -------------------------------------------------------------------------------- 1 | from .histogram import TestHistogramFeatures as TestHistogramFeatures 2 | from .intensity import TestIntensityFeatures as TestIntensityFeatures 3 | from .texture import TestTextureFeatures as TestTextureFeatures 4 | 5 | __all__ = ["TestHistogramFeatures", "TestIntensityFeatures", "TestTextureFeatures"] 6 | -------------------------------------------------------------------------------- /doc/source/installation/uninstall.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Uninstall MedPy 3 | =============== 4 | Only `pip` supports the removal of Python packages. If you have installed **MedPy** by other means, you will have to remove the package manually. With `pip`, call simply 5 | 6 | .. code-block:: bash 7 | 8 | python3 -m pip uninstall medpy 9 | -------------------------------------------------------------------------------- /doc/source/installation/python2.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Installing MedPy with Python 2 support 3 | ====================================== 4 | The newest releases of MedPy require Python 3. If you want to use Python 2, please take a look at the *0.3.0* release. 5 | 6 | With *pip* 7 | 8 | .. code-block:: bash 9 | 10 | (sudo) pip install MedPy==0.3.0 11 | -------------------------------------------------------------------------------- /doc/source/installation/windows.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Installing MedPy on Windows 3 | =========================== 4 | **MedPy** does not support Windows. Using conda, it seems anyway to the possible to install it 5 | 6 | .. code-block:: bash 7 | 8 | conda create --name medpy-venv python3 9 | conda activate medpy-venv 10 | python3 -m pip install medpy 11 | -------------------------------------------------------------------------------- /lib/maxflow/src/boost-build.jam: -------------------------------------------------------------------------------- 1 | # Copyright David Abrahams 2006. Distributed under the Boost 2 | # Software License, Version 1.0. (See accompanying 3 | # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | # Edit this path to point at the tools/build/v2 subdirectory of your 6 | # Boost installation. Absolute paths work, too. 7 | boost-build /usr/share/boost-build/kernel/ ; 8 | -------------------------------------------------------------------------------- /doc/source/installation/conda.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Installing MedPy with Conda 3 | =========================== 4 | 5 | **MedPy** can be installed from `conda forge `: 6 | 7 | .. code-block:: bash 8 | 9 | conda install conda-forge::medpy 10 | 11 | Note that the graph-cut package might not compile in the conda environement due to unmet dependencies. 12 | -------------------------------------------------------------------------------- /doc/source/installation/fastpath.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Installing MedPy the fast way 3 | ============================= 4 | .. note:: 5 | 6 | All installation instructions are for Debian derivates, 7 | such as Ubuntu, but they should be simmilar for other distributions. 8 | 9 | .. code-block:: bash 10 | 11 | sudo apt-get install libboost-python-dev build-essential 12 | python3 -m pip install medpy 13 | -------------------------------------------------------------------------------- /lib/maxflow/src/instances.inc: -------------------------------------------------------------------------------- 1 | #include "graph.h" 2 | 3 | #ifdef _MSC_VER 4 | #pragma warning(disable: 4661) 5 | #endif 6 | 7 | // Instantiations: 8 | // IMPORTANT: 9 | // flowtype should be 'larger' than tcaptype 10 | // tcaptype should be 'larger' than captype 11 | 12 | template class Graph; 13 | template class Graph; 14 | template class Graph; 15 | template class Graph; 16 | -------------------------------------------------------------------------------- /doc/source/installation/developmentmode.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | Installing MedPy in development mode 3 | ==================================== 4 | If you care to work on the source directly, you can install **MedPy** in development mode. Then the sources will remain and any changes made them them be directly available system-wide. 5 | First download the **MedPy** sources from https://pypi.python.org/pypi/MedPy/, unpack them, enter the directory and run: 6 | 7 | .. code-block:: bash 8 | 9 | python3 -m pip install -e . 10 | -------------------------------------------------------------------------------- /doc/source/installation/venv.rst: -------------------------------------------------------------------------------- 1 | ========================================== 2 | Installing MedPy in a virtual environement 3 | ========================================== 4 | We recommend installing **MedPy** inside a virtual environement to avoid package versioning conflicts with your system. 5 | To create and activate a virtual environement, call 6 | 7 | .. code-block:: bash 8 | 9 | python3 -m venv medpy-venv 10 | source medpy-venv/bin/activate 11 | 12 | and then install **MedPy** with 13 | 14 | .. code-block:: bash 15 | 16 | python3 -m pip install medpy 17 | -------------------------------------------------------------------------------- /lib/maxflow/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | PROJECT("MAXFLOW") 2 | 3 | CMAKE_MINIMUM_REQUIRED(VERSION 2.8.0 FATAL_ERROR) 4 | 5 | if(COMMAND cmake_policy) 6 | cmake_policy(SET CMP0003 NEW) 7 | cmake_policy(SET CMP0012 NEW) 8 | endif(COMMAND cmake_policy) 9 | 10 | SET(SOURCES maxflow.cpp graph.cpp wrapper.cpp) 11 | SET(LIBRARY_NAME maxflow) 12 | 13 | FIND_PACKAGE( Boost 1.46.0 COMPONENTS python REQUIRED) 14 | 15 | FIND_PACKAGE(PythonLibs REQUIRED) 16 | INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_PATH}) 17 | ADD_LIBRARY(${LIBRARY_NAME} MODULE ${SOURCES}) 18 | SET_TARGET_PROPERTIES(${LIBRARY_NAME} PROPERTIES PREFIX "") 19 | TARGET_LINK_LIBRARIES(${LIBRARY_NAME} ${Boost_PYTHON_LIBRARY} ${PYTHON_LIBRARIES} ) 20 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # MedPy unittests 2 | 3 | Part of the MedPy functionality is covered by unittests in various states 4 | of development which can be found in this folder. See instructions below 5 | for instructions. 6 | 7 | ## Run for sub-module 8 | ``` 9 | pytest tests/_/* 10 | ``` 11 | 12 | Note: `metric_/` sub-module requires hypothesis package 13 | 14 | ## Check support for image formats 15 | ``` 16 | pytest -s tests/io_/loadsave.py > myformats.log 17 | pytest -s io_/metadata.py > mymetacompatibility.log 18 | 19 | more myformats.log 20 | more mymetacompatibility.log 21 | ``` 22 | 23 | Note that this will take some time and producte a number of warnings that can be savely ignored. 24 | -------------------------------------------------------------------------------- /doc/source/tutorial/loadsave.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Loading and saving images 3 | ========================= 4 | The image loading/saving facilities can be found in :mod:`medpy.io`. Loading an image is straightforward with `~medpy.io.load`: 5 | 6 | >>> from medpy.io import load 7 | >>> image_data, image_header = load('path/to/image.xxx') 8 | 9 | ``image_data`` is a ``numpy`` ``ndarray`` with the image data and ``image_header`` is a header object holding the associated metadata. 10 | 11 | Now, to save the image, use `~medpy.io.save.save`: 12 | 13 | >>> from medpy.io import load, save 14 | >>> save(image_data, 'path/to/image.xxx', image_header) 15 | 16 | The image format is automatically deducted from the file ending. 17 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | MedPy 3 | ===== 4 | 5 | :Release: |release| 6 | :Date: |today| 7 | 8 | MedPy is a medical image processing library written in Python. MedPy requires *Python 3*. 9 | 10 | Installation 11 | ------------ 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installation/index 17 | 18 | Information 19 | ----------- 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | information/index 25 | 26 | Tutorials 27 | --------- 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | 32 | tutorial/index 33 | 34 | Notebooks 35 | --------- 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | 40 | notebooks/index 41 | 42 | 43 | Reference 44 | --------- 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | 49 | reference/index 50 | -------------------------------------------------------------------------------- /doc/source/tutorial/metadata.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Accessing metadata 3 | ================== 4 | Part of the images metadata can be read from the image data, the remaining from the header object. 5 | 6 | >>> from medpy.io import load 7 | >>> image_data, image_header = load('path/to/image.xxx') 8 | >>> image_data.shape 9 | (512, 512, 256) 10 | >>> image_data.dtype 11 | dtype('int16') 12 | 13 | Some simple access function provide a common interface to the header, independent of the image type: 14 | 15 | >>> from medpy.io import header 16 | >>> header.get_pixel_spacing(image_header) 17 | (0.5, 0.5, 2) 18 | >>> header.get_offset(image_header) 19 | (10, -23, 123) 20 | 21 | More metadata is currently not supported by **MedPy**, as the different image formats handle them quite differently. 22 | -------------------------------------------------------------------------------- /doc/source/notebooks/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Notebooks 3 | ========= 4 | 5 | `Accessing the image's meta-data `_. 6 | In this tutorial we will learn how to access and manipulate the image's meta-data form the header. 7 | 8 | `Load, threshold and save an image `_. 9 | In this tutorial you will learn how to load a medical image with MedPy, how to perform a simple thresholding operation and how to save the resulting binary image. 10 | 11 | `Simple binary image processing `_. 12 | In this tutorial you will learn some simple binary image processing. 13 | -------------------------------------------------------------------------------- /lib/maxflow/src/README: -------------------------------------------------------------------------------- 1 | Second version. 2 | - Added GraphFloat and GraphInt. 3 | - Added new method sum_edge which behaves like add_edge but does not increase the number of arcs. Instead adds the new capacities to the already existing arc. 4 | - Added new method get_edge which returns the capacity of the arc i->j. When multiple arcs exist between i->j (as happens when add_edge is called multiple times with the same node ids), the capacity of the first one encountered is returned. 5 | - Added new method get_arc, which returns the first encountered arc between two nodes. This function is not exploitet throught the Python wrappee. 6 | - Added a small python test file get_edge_test.py, which tests the get_edge method. 7 | - Addded a small python test file sum_edge_test.py, which tests the sum_edge method. 8 | - Increased the comments in the CPP files as well as for the Python wrappers. 9 | -------------------------------------------------------------------------------- /lib/maxflow/src/Jamroot: -------------------------------------------------------------------------------- 1 | # Copyright David Abrahams 2006. Distributed under the Boost 2 | # Software License, Version 1.0. (See accompanying 3 | # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | using python ; 6 | 7 | # Specify that the boost-python library exists under the name 8 | # boost_python. That is, because the library was installed at the 9 | # standard search path as /usr/lib/libboost_python.so, bjam will find 10 | # it automatically. No need to specify the absolute path. 11 | lib libboost_python : : boost_python ; 12 | 13 | # Set up the project-wide requirements that everything uses the 14 | # boost_python library. 15 | project : requirements libboost_python ; 16 | 17 | # Declare the three extension modules. You can specify multiple 18 | # source files after the colon separated by spaces. 19 | python-extension maxflow : wrapper.cpp ; 20 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # Runs the pre-commit hooks to make sure that all changes are properly formatted and such 2 | # Triggers on: All PRs that are mergable, but not for draft PRs 3 | # Triggers on: all published releases (incl draft releases) 4 | 5 | name: Pre-commit hooks 6 | 7 | on: 8 | pull_request: 9 | types: [opened, synchronize, reopened, ready_for_review] 10 | release: 11 | types: [published] 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | pre-commit: 18 | if: github.event.pull_request.draft == false 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | - uses: pre-commit/action@v3.0.0 29 | - uses: pre-commit-ci/lite-action@v1.0.1 30 | if: always() 31 | -------------------------------------------------------------------------------- /lib/maxflow/src/pythongraph.h: -------------------------------------------------------------------------------- 1 | /* pythongraph.h */ 2 | /** 3 | Extention of the Graph class that is shipped with the min-cut/max-flow algorithm by 4 | Yuri Boykov and Vladimir Kolmogorov. Simply wraps the constructor in a version not 5 | taking an exception class pointer to avoid problems with the boost:python wrapper. 6 | 7 | Author: Oskar Maier 8 | */ 9 | 10 | #ifndef __PYTHON_GRAPH_H__ 11 | #define __PYTHON_GRAPH_H__ 12 | 13 | #include "graph.h" 14 | 15 | template 16 | class Pythongraph : public Graph 17 | { 18 | public: 19 | Pythongraph(int node_num_max, int edge_num_max) : Graph(node_num_max, edge_num_max, NULL) {}; 20 | flowtype maxflow() { Graph::maxflow(); }; 21 | typename Graph::termtype what_segment(int i) { Graph::what_segment(i); }; 22 | }; 23 | #endif 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit] 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.5.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-merge-conflict 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - id: debug-statements 12 | 13 | - repo: https://github.com/pycqa/isort 14 | rev: "5.13.2" 15 | hooks: 16 | - id: isort 17 | args: ["--profile", "black", "--line-length=88"] 18 | 19 | - repo: https://github.com/psf/black 20 | rev: 23.12.0 21 | hooks: 22 | - id: black 23 | args: ["--line-length=88"] 24 | 25 | - repo: https://github.com/hadialqattan/pycln 26 | rev: "v2.4.0" 27 | hooks: 28 | - id: pycln 29 | args: ["--all"] 30 | 31 | - repo: https://github.com/Yelp/detect-secrets 32 | rev: v1.4.0 33 | hooks: 34 | - id: detect-secrets 35 | args: ["--exclude-files", ".*\\.ipynb"] 36 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # MedPy's CI/CD workflows 2 | 3 | ## Build & release 4 | Upon creating a release or a pre-release on GitHub, the package is *build* and *published* to [test.pypi.org](https://test.pypi.org). 5 | 6 | Install from test PyPi with `python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple medpy==x.y.z.`. This ensures that the dependencies are installed from the proper PyPI. 7 | 8 | After making sure that the package published there is installable and passes all tests, the final *publish* to [pypi.org](https://pypi.org) can be triggered manually from the GitHub UI. 9 | 10 | Note that publishing only works for releases created directly from the `master` branch. Releasees published from other branches should always be pre-releases and never published to [pypi.org](https://pypi.org), but only [test.pypi.org](https://test.pypi.org). 11 | 12 | ## pre-commit.yml 13 | Makes sure that all PRs and all releases adhere to the pre-commit rules. 14 | 15 | ## run-test*.yml 16 | Makes sure that all PRs and all releases pass the tests. 17 | -------------------------------------------------------------------------------- /tests/support.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Check supported image formats.""" 4 | 5 | import unittest 6 | 7 | # build-in modules 8 | import warnings 9 | 10 | # own modules 11 | import io_ 12 | 13 | # third-party modules 14 | 15 | # path changes 16 | 17 | 18 | # information 19 | __author__ = "Oskar Maier" 20 | __version__ = "r0.1.1, 2013-09-16" 21 | __email__ = "oskar.maier@googlemail.com" 22 | __status__ = "Release" 23 | __description__ = "Check supported image formats." 24 | 25 | 26 | # code 27 | def main(): 28 | # load io tests 29 | with warnings.catch_warnings(): 30 | warnings.simplefilter("ignore") 31 | suite_io = unittest.TestSuite() 32 | suite_io.addTests( 33 | unittest.TestLoader().loadTestsFromTestCase(io_.TestIOFacilities) 34 | ) 35 | suite_io.addTests( 36 | unittest.TestLoader().loadTestsFromTestCase(io_.TestMetadataConsistency) 37 | ) 38 | 39 | # execute tests 40 | unittest.TextTestRunner(verbosity=2).run(suite_io) 41 | 42 | 43 | if __name__ == "__main__": 44 | main() 45 | -------------------------------------------------------------------------------- /medpy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Medical image processing in Python. 3 | 4 | MedPy is a library and script collection for medical image processing in Python. It 5 | contains basic functionalities for reading, writing and manipulating large images of 6 | arbitrary dimensions. 7 | 8 | https://pypi.python.org/pypi/MedPy/ 9 | https://github.com/loli/medpy/ 10 | 11 | Copyright (C) 2013 Oskar Maier, 12 | 13 | This library is free software: you can redistribute it and/or modify 14 | it under the terms of the GNU General Public License as published by 15 | the Free Software Foundation, either version 3 of the License, or 16 | (at your option) any later version. 17 | 18 | This program is distributed in the hope that it will be useful, 19 | but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | GNU General Public License for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with this program. If not, see . 25 | """ 26 | 27 | __version__ = "0.5.2" 28 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Steps for a new release 2 | 3 | ## Preparations 4 | - Create a branch `Release_x.y.z` to work towards the release 5 | - Bump up the library version 6 | - `setup.py` 7 | - `medpy/__init__.py` 8 | - `doc/source/conf.py` 9 | - Run tests and make sure that all work 10 | - Run notebooks and make sure that all work 11 | - Check documentation and make sure that up to date 12 | - Update `CHANGES.txt`, highlighting only major changes 13 | - Test releases by publishing a pre-release, using the workflow detailed under [.github/workflows](.github/workflows) 14 | - Re-create documentation and upload to gihub pages to test, then revert to previous version 15 | 16 | 17 | ## Release 18 | - Open PR to master, review, and merge 19 | - Create a pre-release from master and test 20 | - Create final release from master and test 21 | - You might need to delete test package with same version number ion from test.pypi.org 22 | - Trigger publish to PyPi workflow (see under [.github/workflows](.github/workflows)) 23 | - Update conda-force recipe to new version (PR) 24 | - Update DOI 25 | 26 | ## Further readings 27 | - https://packaging.python.org/ 28 | - https://docs.github.com/en/actions 29 | -------------------------------------------------------------------------------- /medpy/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================================================================================== 3 | Utilities without a direct connection to medical image processing (:mod:`medpy.utilities`) 4 | ========================================================================================== 5 | .. currentmodule:: medpy.utilities 6 | 7 | Note that the methods/classes from the sub-modules are not loaded into 8 | :mod:`medpy.utilities` directly, but have to be imported like 9 | 10 | >>> from medpy.utilities.argparseu import sequenceOfIntegers 11 | 12 | Custom types for the `argparse `_ commandline parser 13 | ===================================================================================================== 14 | 15 | .. module:: medpy.utilities.argparseu 16 | .. autosummary:: 17 | :toctree: generated/ 18 | 19 | sequenceOfIntegers 20 | sequenceOfIntegersGt 21 | sequenceOfIntegersGe 22 | sequenceOfIntegersLt 23 | sequenceOfIntegersLe 24 | sequenceOfIntegersGeAscendingStrict 25 | sequenceOfFloats 26 | sequenceOfFloatsGt 27 | sequenceOfFloatsGe 28 | sequenceOfFloatsLt 29 | sequenceOfFloatsLe 30 | 31 | """ 32 | from . import argparseu as argparseu 33 | 34 | __all__ = ["argparseu"] 35 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # Install the package and run all tests except the graph-cut ones 2 | # Triggers on: All PRs that are mergable, but not for draft PRs 3 | # Triggers on: all published releases (incl pre-releases) 4 | 5 | name: Run tests (wo graphcut) 6 | 7 | on: 8 | pull_request: 9 | types: [opened, synchronize, reopened, ready_for_review] 10 | release: 11 | types: [published] 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | run-tests: 18 | if: github.event.pull_request.draft == false 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ["3.8", "3.9", "3.10", "3.11"] 24 | os: [ubuntu-latest, macos-latest] 25 | 26 | runs-on: ${{ matrix.os }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install with test dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | python -m pip install .[test] 38 | - name: Test with pytest 39 | run: | 40 | pytest tests/features_/* 41 | pytest tests/filter_/* 42 | pytest tests/io_/* 43 | pytest tests/metric_/* 44 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.5.2, 2024-07-23 -- Hotfixes 2 | v0.5.1, 2024-04-03 -- Hotfixes 3 | v0.5.0, 2024-04-03 -- Addressed all depreciation warnings and incompatabilities 4 | Updated documentation 5 | Updated and fixed tests 6 | Added github workflows as system 7 | Introduced formatting rules and pre-commit to enforce them 8 | Removed dockerfile creation files 9 | v0.4.0, 2018-02-XX -- Switched to Python 3: finally compatible with modern development environements 10 | Switched to simple itk for image loading/saving: read dicom series, more formats, less dependencies, cleaner code, easier to maintain 11 | Documentation: installation instructions for Windows and OsX 12 | Others: improved filters, cleanup, bugfixes 13 | v0.3.0, 2017-09-20 -- Extensive cleanup, many new functionalities, updated documentation, notebook tutorials, Python 3 branch 14 | v0.2.2, 2014-09-18 -- Changes the documentation engine to Sphinx and fixed a number of bugs 15 | v0.2.1, 2014-08-19 -- ez_setup.py has not been include 16 | v0.2.0, 2014-08-19 -- Little clean-up, many new functionalities; in generally simpler structure and usage; complilation of C++ module not required anymore 17 | v0.1.0, 2013-04-15 -- Initial release. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | TODO.txt 2 | 3 | 4 | # Images 5 | *.nii 6 | *.mhd 7 | *.raw 8 | 9 | # Local virtual envs 10 | .venv/ 11 | 12 | # DOC dirs 13 | doc/build/ 14 | doc/generated/ 15 | doc/source/generated/ 16 | 17 | # Notebooks dirs 18 | .ipynb_checkpoints 19 | 20 | # BUILD dirs 21 | build/ 22 | dist/ 23 | MedPy.egg-info/ 24 | 25 | # Only locally used, temporary .py scripts. 26 | _*.py 27 | !__init__.py 28 | 29 | # Backup files 30 | *.bak 31 | 32 | # Compiled source 33 | *.com 34 | *.class 35 | *.dll 36 | *.exe 37 | *.o 38 | *.so 39 | *.pyc 40 | *.pyo 41 | 42 | # Packages 43 | # it's better to unpack these files and commit the raw source 44 | # git has its own built in compression methods 45 | *.7z 46 | *.dmg 47 | *.gz 48 | *.iso 49 | *.jar 50 | *.rar 51 | *.tar 52 | *.zip 53 | 54 | # Logs and databases 55 | *.log 56 | *.sql 57 | *.sqlite 58 | 59 | # OS generated files 60 | .DS_Store* 61 | ehthumbs.db 62 | Icon? 63 | Thumbs.db 64 | *~ 65 | 66 | # Eclipse and PyDev project files 67 | .project 68 | .pydevproject 69 | .settings/ 70 | .metadata/ 71 | 72 | # Suggestions by GitHub for Python projects 73 | # Packages 74 | *.egg 75 | *.egg-info 76 | dist 77 | build 78 | eggs 79 | parts 80 | var 81 | sdist 82 | develop-eggs 83 | .installed.cfg 84 | 85 | # Installer logs 86 | pip-log.txt 87 | 88 | # Unit test / coverage reports 89 | .coverage 90 | .tox 91 | .hypothesis 92 | 93 | #Translations 94 | *.mo 95 | 96 | #Mr Developer 97 | .mr.developer.cfg 98 | -------------------------------------------------------------------------------- /medpy/neighbours/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ==================================== 3 | Neighbours (:mod:`medpy.neighbours`) 4 | ==================================== 5 | .. currentmodule:: medpy.neighbours 6 | 7 | This package contains nearest neighbour methods. 8 | 9 | Patch-wise :mod:`medpy.neighbours.knn` 10 | =========================================== 11 | K-nearest-neighbours based methods. The interfaces are loosely based on the 12 | ``sklear.neighbours`` methods. The methods can be considered complementary to 13 | the ones found there. 14 | 15 | .. module:: medpy.neighbours.knn 16 | .. autosummary:: 17 | :toctree: generated/ 18 | 19 | mkneighbors_graph 20 | pdist 21 | 22 | """ 23 | 24 | # Copyright (C) 2013 Oskar Maier 25 | # 26 | # This program is free software: you can redistribute it and/or modify 27 | # it under the terms of the GNU General Public License as published by 28 | # the Free Software Foundation, either version 3 of the License, or 29 | # (at your option) any later version. 30 | # 31 | # This program is distributed in the hope that it will be useful, 32 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 34 | # GNU General Public License for more details. 35 | # 36 | # You should have received a copy of the GNU General Public License 37 | # along with this program. If not, see . 38 | 39 | from .knn import mkneighbors_graph as mkneighbors_graph 40 | from .knn import pdist as pdist 41 | 42 | __all__ = [ 43 | "mkneighbors_graph", 44 | "pdist", 45 | ] 46 | -------------------------------------------------------------------------------- /tests/filter_/anisotropic_diffusion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from medpy.filter import anisotropic_diffusion 4 | 5 | # Purpose of these tests is to ensure the filter code does not crash 6 | # Depending on Python versions 7 | 8 | 9 | def test_anisotropic_diffusion_powerof2_single_channel(): 10 | arr = np.random.uniform(size=(64, 64)) 11 | filtered = anisotropic_diffusion(arr) 12 | assert filtered.shape == arr.shape 13 | 14 | 15 | def test_anisotropic_diffusion_powerof2_three_channels(): 16 | # Purpose of this test is to ensure the filter code does not crash 17 | # Depending on Python versions 18 | arr = np.random.uniform(size=(64, 64, 3)) 19 | filtered = anisotropic_diffusion(arr) 20 | assert filtered.shape == arr.shape 21 | 22 | 23 | def test_anisotropic_diffusion_single_channel(): 24 | # Purpose of this test is to ensure the filter code does not crash 25 | # Depending on Python versions 26 | arr = np.random.uniform(size=(60, 31)) 27 | filtered = anisotropic_diffusion(arr) 28 | assert filtered.shape == arr.shape 29 | 30 | 31 | def test_anisotropic_diffusion_three_channels(): 32 | # Purpose of this test is to ensure the filter code does not crash 33 | # Depending on Python versions 34 | arr = np.random.uniform(size=(60, 31, 3)) 35 | filtered = anisotropic_diffusion(arr) 36 | assert filtered.shape == arr.shape 37 | 38 | 39 | def test_anisotropic_diffusion_voxel_spacing_array(): 40 | # Purpose of this test is to ensure the filter code does not crash 41 | # Depending on Python versions 42 | arr = np.random.uniform(size=(60, 31, 3)) 43 | filtered = anisotropic_diffusion(arr, voxelspacing=np.array([1, 1, 1.0])) 44 | assert filtered.shape == arr.shape 45 | -------------------------------------------------------------------------------- /medpy/iterators/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ======================================== 3 | Image iterators (:mod:`medpy.iterators`) 4 | ======================================== 5 | .. currentmodule:: medpy.iterators 6 | 7 | This package contains iterators for images. 8 | 9 | Patch-wise :mod:`medpy.iterators.patchwise` 10 | =========================================== 11 | Iterators to extract patches from images. 12 | 13 | .. module:: medpy.iterators.patchwise 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | SlidingWindowIterator 18 | CentredPatchIterator 19 | CentredPatchIteratorOverlapping 20 | 21 | 22 | """ 23 | 24 | # Copyright (C) 2013 Oskar Maier 25 | # 26 | # This program is free software: you can redistribute it and/or modify 27 | # it under the terms of the GNU General Public License as published by 28 | # the Free Software Foundation, either version 3 of the License, or 29 | # (at your option) any later version. 30 | # 31 | # This program is distributed in the hope that it will be useful, 32 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 34 | # GNU General Public License for more details. 35 | # 36 | # You should have received a copy of the GNU General Public License 37 | # along with this program. If not, see . 38 | 39 | from .patchwise import CentredPatchIterator as CentredPatchIterator 40 | from .patchwise import ( 41 | CentredPatchIteratorOverlapping as CentredPatchIteratorOverlapping, 42 | ) 43 | from .patchwise import SlidingWindowIterator as SlidingWindowIterator 44 | 45 | __all__ = [ 46 | "CentredPatchIterator", 47 | "CentredPatchIteratorOverlapping", 48 | "SlidingWindowIterator", 49 | ] 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Publish a release to PyPI 2 | # Requires build package workflow to run first 3 | # This version releases to https://pypi.org/, only trigger if the release has been thorough tested 4 | 5 | name: Build package & release to PyPI 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | tag: 11 | description: "Select release to publish" 12 | required: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | ref: ${{ inputs.tag }} 24 | - name: Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install build 32 | - name: Build a source tarball 33 | run: python -m build --sdist 34 | - name: Store the distribution packages 35 | uses: actions/upload-artifact@v4.3.0 36 | with: 37 | name: python-package-distributions-${{ inputs.tag }} 38 | path: dist/ 39 | 40 | publish: 41 | needs: 42 | - build 43 | runs-on: ubuntu-latest 44 | environment: 45 | name: pypi-publish 46 | url: https://pypi.org/p/medpy 47 | permissions: 48 | id-token: write # IMPORTANT: mandatory for trusted publishing 49 | steps: 50 | - name: Download dists 51 | uses: actions/download-artifact@v4.1.1 # make sure that same major version as actions/upload-artifact 52 | with: 53 | name: python-package-distributions-${{ inputs.tag }} 54 | path: dist/ 55 | - name: Publish package 56 | uses: pypa/gh-action-pypi-publish@v1.8.11 57 | -------------------------------------------------------------------------------- /.github/workflows/build-publish-test.yml: -------------------------------------------------------------------------------- 1 | # Build package & publish a release to PyPI (test) 2 | # Given a tag, downloads the associated code, builds the package, and uploads the source tarball as artifact 3 | # This version releases to https://test.pypi.org/ for testing purposes 4 | # Triggers on: all published releases (incl pre-releases) 5 | 6 | name: Build package & release to PyPI (test) 7 | 8 | on: 9 | release: 10 | types: [published] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.x 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install build 28 | - name: Build a source tarball 29 | run: python -m build --sdist 30 | - name: Store the distribution packages 31 | uses: actions/upload-artifact@v4.3.0 32 | with: 33 | name: python-package-distributions-${{ github.ref_name }} 34 | path: dist/ 35 | 36 | publish-test: 37 | needs: 38 | - build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: pypi-publish-test 42 | url: https://test.pypi.org/p/medpy 43 | permissions: 44 | id-token: write # IMPORTANT: mandatory for trusted publishing 45 | steps: 46 | - name: Download dists 47 | uses: actions/download-artifact@v4.1.1 # make sure that same major version as actions/upload-artifact 48 | with: 49 | name: python-package-distributions-${{ github.ref_name }} 50 | path: dist/ 51 | - name: Publish package 52 | uses: pypa/gh-action-pypi-publish@v1.8.11 53 | with: 54 | repository-url: https://test.pypi.org/legacy/ # test publish platform 55 | -------------------------------------------------------------------------------- /doc/source/information/imageformats.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Supported medical image formats 3 | =============================== 4 | 5 | .. note:: 6 | 7 | You can check your currently supported image formats by grabbing the source code from `Github `_ and running *python3 tests/support.py*. 8 | 9 | **MedPy** relies on *SimpleITK*, which enables the power of ITK for image loading and saving. 10 | The supported image file formats should include at least the following. 11 | 12 | Medical formats: 13 | 14 | - ITK MetaImage (.mha/.raw, .mhd) 15 | - Neuroimaging Informatics Technology Initiative (NIfTI) (.nia, .nii, .nii.gz, .hdr, .img, .img.gz) 16 | - Analyze (plain, SPM99, SPM2) (.hdr/.img, .img.gz) 17 | - Digital Imaging and Communications in Medicine (DICOM) (.dcm, .dicom) 18 | - Digital Imaging and Communications in Medicine (DICOM) series (/) 19 | - Nearly Raw Raster Data (Nrrd) (.nrrd, .nhdr) 20 | - Medical Imaging NetCDF (MINC) (.mnc, .MNC) 21 | - Guys Image Processing Lab (GIPL) (.gipl, .gipl.gz) 22 | 23 | Microscopy formats: 24 | 25 | - Medical Research Council (MRC) (.mrc, .rec) 26 | - Bio-Rad (.pic, .PIC) 27 | - LSM (Zeiss) microscopy images (.tif, .TIF, .tiff, .TIFF, .lsm, .LSM) 28 | - Stimulate / Signal Data (SDT) (.sdt) 29 | 30 | Visualization formats: 31 | 32 | - VTK images (.vtk) 33 | 34 | Other formats: 35 | 36 | - Portable Network Graphics (PNG) (.png, .PNG) 37 | - Joint Photographic Experts Group (JPEG) (.jpg, .JPG, .jpeg, .JPEG) 38 | - Tagged Image File Format (TIFF) (.tif, .TIF, .tiff, .TIFF) 39 | - Windows bitmap (.bmp, .BMP) 40 | - Hierarchical Data Format (HDF5) (.h5 , .hdf5 , .he5) 41 | - MSX-DOS Screen-x (.ge4, .ge5) 42 | 43 | For informations about which image formats, dimensionalities and pixel data types 44 | your current configuration supports, run `python3 tests/support.py > myformats.log`. 45 | 46 | Further information see https://simpleitk.readthedocs.io . 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/MedPy.svg)](https://pypi.python.org/pypi/MedPy/) 2 | [![anaconda version](https://anaconda.org/conda-forge/medpy/badges/version.svg)](https://anaconda.org/conda-forge/medpy) 3 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/MedPy.svg)](https://pypi.python.org/pypi/MedPy/) 4 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 5 | [![Downloads](https://pepy.tech/badge/medpy/month)](https://pepy.tech/project/medpy) 6 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2565940.svg)](https://doi.org/10.5281/zenodo.2565940) 7 | 8 | 9 | [GitHub](https://github.com/loli/medpy/) | [Documentation](http://loli.github.io/medpy/) | [Tutorials](http://loli.github.io/medpy/) | [Issue tracker](https://github.com/loli/medpy/issues) 10 | 11 | # medpy - Medical Image Processing in Python 12 | 13 | MedPy is an image processing library and collection of scripts targeted towards medical (i.e. high dimensional) image processing. 14 | 15 | ## Stable releases 16 | 17 | - Download (stable release): https://pypi.python.org/pypi/medpy 18 | - HTML documentation and installation instruction (stable release): http://loli.github.io/medpy/ 19 | - Download from [Conda-Forge](https://conda-forge.org): https://anaconda.org/conda-forge/medpy 20 | 21 | ## Development version 22 | 23 | - Download (development version): https://github.com/loli/medpy 24 | - HTML documentation and installation instruction (development version): create this from doc/ folder following instructions in contained README file 25 | 26 | ## Contribute 27 | 28 | - Clone `master` branch from [github](https://github.com/loli/medpy) 29 | - Install [pre-commit](https://pre-commit.com/) hooks or with `[dev,test]` extras 30 | - Submit your change as a PR request 31 | 32 | ## Python 2 version 33 | 34 | Python 2 is no longer supported. But you can still use the older releases `<=0.3.0`. 35 | 36 | ## Other links 37 | 38 | - Issue tracker: https://github.com/loli/medpy/issues 39 | -------------------------------------------------------------------------------- /medpy/core/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.2 18 | # since 2011-12-11 19 | # status Development 20 | 21 | # build-in module 22 | 23 | # third-party modules 24 | 25 | # path changes 26 | 27 | # own modules 28 | 29 | 30 | # code 31 | class ArgumentError(Exception): 32 | r"""Thrown by an application when an invalid command line argument has been 33 | supplied.""" 34 | pass 35 | 36 | 37 | class FunctionError(Exception): 38 | r"""Thrown when a supplied function returns unexpected results.""" 39 | pass 40 | 41 | 42 | class SubprocessError(Exception): 43 | r"""Thrown by an application when a subprocess execution failed.""" 44 | pass 45 | 46 | 47 | class ImageTypeError(Exception): 48 | r"""Thrown when trying to load or save an image of unknown type.""" 49 | pass 50 | 51 | 52 | class DependencyError(Exception): 53 | r"""Thrown when a required module could not be loaded.""" 54 | pass 55 | 56 | 57 | class ImageLoadingError(Exception): 58 | r"""Thrown when a image could not be loaded.""" 59 | pass 60 | 61 | 62 | class ImageSavingError(Exception): 63 | r"""Thrown when a image could not be saved.""" 64 | pass 65 | 66 | 67 | class MetaDataError(Exception): 68 | r"""Thrown when an image meta data failure occurred.""" 69 | pass 70 | -------------------------------------------------------------------------------- /tests/metric_/binary.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unittest for medpy.features.histogram. 3 | 4 | @author Oskar Maier 5 | @version r0.1.0 6 | @since 2024-07-23 7 | @status Release 8 | """ 9 | 10 | import numpy as np 11 | 12 | from medpy.metric import asd, assd, obj_asd, obj_assd 13 | 14 | result_min = np.asarray([1, 0]).astype(bool) 15 | reference_min = np.asarray([0, 1]).astype(bool) 16 | result_sym = np.asarray( 17 | [[1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0]] 18 | ).astype(bool) 19 | reference_sym = np.asarray( 20 | [[1, 1, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0]] 21 | ).astype(bool) 22 | 23 | 24 | def test_asd_identity(): 25 | assert asd(result_min, result_min) == 0 26 | 27 | 28 | def test_assd_identity(): 29 | assert assd(result_min, result_min) == 0 30 | 31 | 32 | def test_obj_asd_identity(): 33 | assert obj_asd(result_min, result_min) == 0 34 | 35 | 36 | def test_obj_assd_identity(): 37 | assert obj_assd(result_min, result_min) == 0 38 | 39 | 40 | def test_asd_distance(): 41 | assert asd(result_min, reference_min) == 1.0 42 | 43 | 44 | def test_assd_distance(): 45 | assert assd(result_min, reference_min) == 1.0 46 | 47 | 48 | def test_asd_voxelspacing(): 49 | assert asd(result_min, reference_min, voxelspacing=[2]) == 2.0 50 | 51 | 52 | def test_assd_voxelspacing(): 53 | assert assd(result_min, reference_min, voxelspacing=[2]) == 2.0 54 | 55 | 56 | def test_asd_is_not_symetric(): 57 | asd_1 = asd(result_sym, reference_sym) 58 | asd_2 = asd(reference_sym, result_sym) 59 | assert asd_1 != asd_2 60 | 61 | 62 | def test_assd_is_symetric(): 63 | assd_1 = assd(result_sym, reference_sym) 64 | assd_2 = assd(reference_sym, result_sym) 65 | assert assd_1 == assd_2 66 | 67 | 68 | def test_obj_asd_is_not_symetric(): 69 | asd_1 = obj_asd(result_sym, reference_sym) 70 | asd_2 = obj_asd(reference_sym, result_sym) 71 | assert asd_1 != asd_2 72 | 73 | 74 | def test_obj_assd_is_symetric(): 75 | assd_1 = obj_assd(result_sym, reference_sym) 76 | assd_2 = obj_assd(reference_sym, result_sym) 77 | assert assd_1 == assd_2 78 | -------------------------------------------------------------------------------- /.github/workflows/run-tests-gc.yml: -------------------------------------------------------------------------------- 1 | # Install the package and run the graph-cut tests 2 | # This test is kept separate, as the graphcut functionality is optional and unstable 3 | # Triggers on: All PRs that are mergable, but not for draft PRs 4 | # Triggers on: all published releases (incl pre-releases) 5 | 6 | # Note: the dependency libboost_python will always be installed against the OS's main python version, 7 | # independent of the python version set-up. They are 22.04 = 3.10 and 20.04 = 3.8. 8 | 9 | name: Run tests (graphcut only) 10 | 11 | on: 12 | pull_request: 13 | types: [opened, synchronize, reopened, ready_for_review] 14 | release: 15 | types: [published] 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | run-tests-gc-ubuntu-22_04: 22 | if: github.event.pull_request.draft == false 23 | runs-on: ubuntu-22.04 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 3.10 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.10" 30 | - name: Install system dependencies for graphcut functionality 31 | run: sudo apt-get install -y libboost-python-dev build-essential 32 | - name: Install with test dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | python -m pip install -v .[test] 36 | - name: Test with pytest (graphcut test only) 37 | run: cd tests && pytest graphcut_/* 38 | 39 | run-tests-gc-test-ubuntu-20_04: 40 | if: github.event.pull_request.draft == false 41 | runs-on: ubuntu-20.04 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Set up Python 3.8 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: "3.8" 48 | - name: Install system dependencies for graphcut functionality 49 | run: sudo apt-get install -y libboost-python-dev build-essential 50 | - name: Install with test dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | python -m pip install -v .[test] 54 | - name: Test with pytest (graphcut test only) 55 | run: cd tests && pytest graphcut_/* 56 | -------------------------------------------------------------------------------- /lib/maxflow/src/get_edge_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import random 4 | 5 | from maxflow import GraphDouble, GraphFloat, GraphInt 6 | 7 | 8 | def main(): 9 | print("GRAPHDOUBLE") 10 | test(GraphDouble, 100) 11 | print("GRAPHFLOAT") 12 | test(GraphFloat, 100) 13 | print("GRAPHINT") 14 | test(GraphInt, 100) 15 | 16 | 17 | def test(graphtype, runs): 18 | print("#### FIRST ####") 19 | g = graphtype(2, 1) 20 | g.add_node(3) 21 | g.add_edge(0, 1, 2, 2) 22 | g.add_edge(0, 2, 4, 5) 23 | 24 | p(g, 0, 1, 2) 25 | p(g, 1, 0, 2) 26 | p(g, 0, 2, 4) 27 | p(g, 2, 0, 5) 28 | p(g, 1, 2, 0) 29 | p(g, 2, 1, 0) 30 | # p(g,1,3,1) # should raise error: node id out of bounds 31 | 32 | print("#### SECOND ####") 33 | g = graphtype(2, 1) 34 | g.add_node(2) 35 | g.add_edge(0, 1, 2, 3) 36 | p(g, 0, 1, 2) 37 | p(g, 1, 0, 3) 38 | # p(g,1,2,1) # should raise error: node id unknown, as add_node has not been often enough called 39 | 40 | print("#### THIRD: RANDOM ####") 41 | nodes = runs 42 | edges = nodes * (nodes - 1) 43 | g = graphtype(nodes, edges) 44 | g.add_node(nodes) 45 | connection = dict() 46 | for fr in range(nodes): 47 | for to in range(fr, nodes): 48 | if fr == to: 49 | continue 50 | connection[(fr, to)] = (random.randint(1, 10), random.randint(1, 10)) 51 | g.add_edge(fr, to, connection[(fr, to)][0], connection[(fr, to)][1]) 52 | print("Testing {} random edge weights...".format(edges)) 53 | for fr in range(nodes): 54 | for to in range(fr, nodes): 55 | if fr == to: 56 | continue 57 | p2(g, fr, to, connection[(fr, to)][0]) 58 | p2(g, to, fr, connection[(fr, to)][1]) 59 | print("Finished.") 60 | 61 | 62 | def p(g, f, t, exp): 63 | if exp != g.get_edge(f, t): 64 | print("!Failed:", end=" ") 65 | else: 66 | print("Passed:", end=" ") 67 | print("{}->{}:{} (expected: {})".format(f, t, g.get_edge(f, t), exp)) 68 | 69 | 70 | def p2(g, f, t, exp): 71 | if exp != g.get_edge(f, t): 72 | print("!Failed:", end=" ") 73 | print("{}->{}:{} (expected: {})".format(f, t, g.get_edge(f, t), exp)) 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /lib/maxflow/src/sum_edge_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | from maxflow import GraphDouble, GraphFloat, GraphInt 5 | 6 | 7 | def main(): 8 | print("\nGRAPHINT") 9 | test(GraphInt) 10 | print("\nGRAPHFLOAT") 11 | test(GraphFloat) 12 | print("\nGRAPHDOUBLE") 13 | test(GraphDouble) 14 | print("\nADDITIONAL TESTS") 15 | test_sum(GraphDouble) 16 | test_multiple_arcs(GraphDouble) 17 | test_overflow(GraphDouble) 18 | 19 | 20 | def test(graphtype): 21 | g = graphtype(4, 4) 22 | g.add_node(4) 23 | 24 | g.add_tweights(0, 99, 0) 25 | g.add_tweights(3, 0, 99) 26 | 27 | g.add_edge(0, 1, 1, 1) 28 | g.add_edge(0, 2, 1, 1) 29 | g.add_edge(1, 3, 2, 2) 30 | g.add_edge(2, 3, 2, 2) 31 | print("Flow: {}".format(g.maxflow())) 32 | print_cut(g, 4) 33 | 34 | g.add_edge(0, 1, 2, 2) 35 | g.add_edge(0, 2, 2, 2) 36 | print("Flow: {}".format(g.maxflow())) 37 | print_cut(g, 4) 38 | 39 | 40 | def test_sum(graphtype): 41 | g = graphtype(2, 1) 42 | g.add_node(2) 43 | 44 | print( 45 | "Expected to go all the way to 20 without increasing the memory requirements..." 46 | ) 47 | for i in range(20): 48 | print(i, end=" ") 49 | g.sum_edge(0, 1, 1, 2) 50 | 51 | v1 = g.get_edge(0, 1) 52 | v2 = g.get_edge(1, 0) 53 | print("\nFinal edge weight should be 20 resp. 40. Found {} resp. {}".format(v1, v2)) 54 | 55 | 56 | def test_multiple_arcs(graphtype): 57 | g = graphtype(2, 1) 58 | g.add_node(2) 59 | 60 | g.add_edge(0, 1, 1, 2) 61 | g.add_edge(0, 1, 1, 2) 62 | 63 | v1 = g.get_edge(0, 1) 64 | v2 = g.get_edge(1, 0) 65 | print("Final edge weight should be 1 resp. 2. Found {} resp. {}".format(v1, v2)) 66 | 67 | 68 | def test_overflow(graphtype): 69 | g = graphtype(2, 1) 70 | g.add_node(2) 71 | 72 | print("Memory expected to double after 15...") 73 | for i in range(20): 74 | g.add_edge(0, 1, 1, 2) 75 | print(i, end=" ") 76 | 77 | v1 = g.get_edge(0, 1) 78 | v2 = g.get_edge(1, 0) 79 | print("\nFinal edge weight should be 1 resp. 2. Found {} resp. {}".format(v1, v2)) 80 | 81 | 82 | def print_cut(g, nodes): 83 | for n in range(nodes): 84 | print("{} in {}".format(n, g.what_segment(n))) 85 | 86 | 87 | if __name__ == "__main__": 88 | main() 89 | -------------------------------------------------------------------------------- /medpy/io/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =========================================== 3 | Image I/O functionalities (:mod:`medpy.io`) 4 | =========================================== 5 | .. currentmodule:: medpy.io 6 | 7 | This package provides functionalities for loading and saving images, 8 | as well as the handling of image metadata. 9 | 10 | Loading an image 11 | ================ 12 | 13 | .. module:: medpy.io.load 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | load 18 | 19 | Saving an image 20 | =============== 21 | 22 | .. module:: medpy.io.save 23 | .. autosummary:: 24 | :toctree: generated/ 25 | 26 | save 27 | 28 | Reading / writing metadata (:mod:`medpy.io.header`) 29 | =================================================== 30 | 31 | .. module:: medpy.io.header 32 | .. autosummary:: 33 | :toctree: generated/ 34 | 35 | Header 36 | 37 | """ 38 | 39 | # Copyright (C) 2013 Oskar Maier 40 | # 41 | # This program is free software: you can redistribute it and/or modify 42 | # it under the terms of the GNU General Public License as published by 43 | # the Free Software Foundation, either version 3 of the License, or 44 | # (at your option) any later version. 45 | # 46 | # This program is distributed in the hope that it will be useful, 47 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 48 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 49 | # GNU General Public License for more details. 50 | # 51 | # You should have received a copy of the GNU General Public License 52 | # along with this program. If not, see . 53 | 54 | 55 | from .header import Header as Header 56 | from .header import copy_meta_data as copy_meta_data 57 | from .header import get_offset as get_offset 58 | from .header import get_pixel_spacing as get_pixel_spacing 59 | from .header import get_voxel_spacing as get_voxel_spacing 60 | from .header import set_offset as set_offset 61 | from .header import set_pixel_spacing as set_pixel_spacing 62 | from .header import set_voxel_spacing as set_voxel_spacing 63 | from .load import load as load 64 | from .save import save as save 65 | 66 | __all__ = [ 67 | "load", 68 | "save", 69 | "Header", 70 | "get_voxel_spacing", 71 | "get_pixel_spacing", 72 | "get_offset", 73 | "set_voxel_spacing", 74 | "set_pixel_spacing", 75 | "set_offset", 76 | "copy_meta_data", 77 | ] 78 | -------------------------------------------------------------------------------- /medpy/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ===================================================================== 3 | Core functionalities and shared exception objects (:mod:`medpy.core`) 4 | ===================================================================== 5 | .. currentmodule:: medpy.core 6 | 7 | This package collect the packages core functionalities, such as an 8 | event Logger and shared exception classes. If you do not intend to 9 | develop MedPy, you usually won't have to touch this. 10 | 11 | Logger :mod:`medy.core.logger` 12 | ============================== 13 | 14 | .. module:: medpy.core.logger 15 | .. autosummary:: 16 | :toctree: generated/ 17 | 18 | Logger 19 | 20 | 21 | Exceptions :mod:`medpy.core.exceptions` 22 | ======================================= 23 | 24 | .. module:: medpy.core.exceptions 25 | .. autosummary:: 26 | :toctree: generated/ 27 | 28 | ArgumentError 29 | FunctionError 30 | SubprocessError 31 | ImageLoadingError 32 | DependencyError 33 | ImageSavingError 34 | ImageTypeError 35 | MetaDataError 36 | 37 | """ 38 | 39 | # Copyright (C) 2013 Oskar Maier 40 | # 41 | # This program is free software: you can redistribute it and/or modify 42 | # it under the terms of the GNU General Public License as published by 43 | # the Free Software Foundation, either version 3 of the License, or 44 | # (at your option) any later version. 45 | # 46 | # This program is distributed in the hope that it will be useful, 47 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 48 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 49 | # GNU General Public License for more details. 50 | # 51 | # You should have received a copy of the GNU General Public License 52 | # along with this program. If not, see . 53 | 54 | from .exceptions import ArgumentError as ArgumentError 55 | from .exceptions import DependencyError as DependencyError 56 | from .exceptions import FunctionError as FunctionError 57 | from .exceptions import ImageLoadingError as ImageLoadingError 58 | from .exceptions import ImageSavingError as ImageSavingError 59 | from .exceptions import ImageTypeError as ImageTypeError 60 | from .exceptions import MetaDataError as MetaDataError 61 | from .exceptions import SubprocessError as SubprocessError 62 | from .logger import Logger as Logger 63 | 64 | __all__ = [ 65 | "Logger", 66 | "ArgumentError", 67 | "FunctionError", 68 | "SubprocessError", 69 | "ImageLoadingError", 70 | "DependencyError", 71 | "ImageSavingError", 72 | "ImageTypeError", 73 | "MetaDataError", 74 | ] 75 | -------------------------------------------------------------------------------- /doc/source/installation/graphcutsupport.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Installing MedPy with GraphCut support 3 | ====================================== 4 | The GraphCut functionalities of **MedPy** depend on the `max-flow/min-cut library `_ by Boykov and Kolmogorov. 5 | During installation, **MedPy** will try to compile it and its python wrappers. If the compilation fails, **MedPy** will be installed without the GraphCut module. 6 | To enable the GraphCut functionality of **MedPy**, the dependencies of the library must be met *before* installing **MedPy** (although it can always be simply re-installed). 7 | 8 | Dependencies 9 | ------------ 10 | * Boost.Python 11 | * g++ 12 | * gcc 13 | 14 | These dependencies can be found in the repositories of all major distribution. For e.g. Ubuntu, you can simply call: 15 | 16 | .. code-block:: bash 17 | 18 | sudo apt-get install libboost-python-dev build-essential 19 | 20 | Then install **MedPy** the usual way. 21 | 22 | Troubleshooting 23 | --------------- 24 | 25 | If you experience an error like `ModuleNotFoundError: No module named 'medpy.graphcut.maxflow'`, this usually means 26 | that the `graphcut` module has not been compiled successfully. To check the error log, try re-installing **MedPy** with: 27 | 28 | .. code-block:: bash 29 | 30 | pip install medpy --no-cache-dir --force-reinstall -v 31 | 32 | In the logs, you might see the following warning: 33 | 34 | :: 35 | 36 | 2021-06-30T11:07:32,684 *************************************************************************** 37 | 2021-06-30T11:07:32,685 WARNING: The medpy.graphcut.maxflow external C++ package could not be compiled, all graphcut functionality will be disabled. You might be missing Boost.Python or some build essentials like g++. 38 | 2021-06-30T11:07:32,685 Failure information, if any, is above. 39 | 2021-06-30T11:07:32,685 I'm retrying the build without the graphcut C++ module now. 40 | 2021-06-30T11:07:32,685 *************************************************************************** 41 | 42 | The error should be detailed in the lines just above. 43 | 44 | Usually, it is a problem with the linking of the `(lib)boost_python3` lib. 45 | There are some inconsistent naming conventions around, rendering the file undiscoverable to **MedPy**. 46 | 47 | On Ubuntu, you should be able to locate your *libboost_python3x.so* under `/usr/lib/x86_64-linux-gnu/`. 48 | If your shared library file is named differently than **MedPy** expects, you might have to create a softlink like, e.g.: 49 | 50 | .. code-block:: bash 51 | 52 | sudo ln -s libboost_python38.so libboost_python3.so 53 | -------------------------------------------------------------------------------- /medpy/graphcut/write.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.1.0 18 | # since 2012-02-06 19 | # status Release 20 | 21 | # build-in modules 22 | 23 | # third-party modules 24 | 25 | # own modules 26 | 27 | 28 | # code 29 | def graph_to_dimacs(g, f): 30 | """ 31 | Persists the supplied graph in valid dimacs format into the file. 32 | 33 | Parameters 34 | ---------- 35 | g : `~medpy.graphcut.graph.Graph` 36 | A graph object to persist. 37 | f : file 38 | A file-like object. 39 | """ 40 | # write comments 41 | f.write("c Created by medpy\n") 42 | f.write("c Oskar Maier, oskar.maier@googlemail.com\n") 43 | f.write("c\n") 44 | 45 | # write problem 46 | f.write("c problem line\n") 47 | f.write( 48 | "p max {} {}\n".format(g.get_node_count() + 2, len(g.get_edges())) 49 | ) # +2 as terminal nodes also count in dimacs format # no-nodes / no-edges 50 | 51 | # denote source and sink 52 | f.write("c source descriptor\n") 53 | f.write("n 1 s\n") 54 | f.write("c sink descriptor\n") 55 | f.write("n 2 t\n") 56 | 57 | # write terminal arcs (t-weights) 58 | f.write("c terminal arcs (t-weights)\n") 59 | for node, weight in list(g.get_tweights().items()): 60 | # Note: the nodes ids of the graph start from 1, but 1 and 2 are reserved for source and sink respectively, therefore add 2 61 | if not 0 == weight[0]: # 0 weights are implicit 62 | f.write("a 1 {} {}\n".format(node + 2, weight[0])) 63 | if not 0 == weight[1]: # 0 weights are implicit 64 | f.write("a {} 2 {}\n".format(node + 2, weight[1])) 65 | 66 | # write inter-node arcs (n-weights) 67 | f.write("c inter-node arcs (n-weights)\n") 68 | for edge, weight in list(g.get_nweights().items()): 69 | if not 0 == weight[0]: # 0 weights are implicit 70 | f.write("a {} {} {}\n".format(edge[0] + 2, edge[1] + 2, weight[0])) 71 | # reversed weights have to follow directly in the next line 72 | if not 0 == weight[1]: # 0 weights are implicit 73 | f.write("a {} {} {}\n".format(edge[1] + 2, edge[0] + 2, weight[1])) 74 | 75 | # end comment 76 | f.write("c end-of-file") 77 | -------------------------------------------------------------------------------- /bin/medpy_set_pixel_spacing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Manually add pixel spacing to an image file. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # own modules 27 | from medpy.core import Logger 28 | from medpy.io import header, load, save 29 | 30 | # third-party modules 31 | 32 | # path changes 33 | 34 | 35 | # information 36 | __author__ = "Oskar Maier" 37 | __version__ = "r0.1.0, 2012-06-04" 38 | __email__ = "oskar.maier@googlemail.com" 39 | __status__ = "Release" 40 | __description__ = """ 41 | Change an image's pixel spacing in-place. 42 | 43 | Copyright (C) 2013 Oskar Maier 44 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 45 | and you are welcome to redistribute it under certain conditions; see 46 | the LICENSE file or for details. 47 | """ 48 | 49 | 50 | # code 51 | def main(): 52 | args = getArguments(getParser()) 53 | 54 | # prepare logger 55 | logger = Logger.getInstance() 56 | if args.debug: 57 | logger.setLevel(logging.DEBUG) 58 | elif args.verbose: 59 | logger.setLevel(logging.INFO) 60 | 61 | # load input 62 | data_input, header_input = load(args.image) 63 | 64 | # change pixel spacing 65 | logger.info( 66 | "Setting pixel spacing along {} to {}...".format(data_input.shape, args.spacing) 67 | ) 68 | header.set_pixel_spacing(header_input, args.spacing) 69 | 70 | # save file 71 | save(data_input.copy(), args.image, header_input, True) 72 | 73 | logger.info("Successfully terminated.") 74 | 75 | 76 | def getArguments(parser): 77 | "Provides additional validation of the arguments collected by argparse." 78 | return parser.parse_args() 79 | 80 | 81 | def getParser(): 82 | "Creates and returns the argparse parser object." 83 | parser = argparse.ArgumentParser(description=__description__) 84 | parser.add_argument("image", help="Image volume.") 85 | parser.add_argument("spacing", type=float, nargs="+", help="The spacing values.") 86 | parser.add_argument( 87 | "-v", dest="verbose", action="store_true", help="Display more information." 88 | ) 89 | parser.add_argument( 90 | "-d", dest="debug", action="store_true", help="Display debug information." 91 | ) 92 | return parser 93 | 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /bin/medpy_create_empty_volume_by_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Creates an empty volume with the same attributes as the passes example image. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see .""" 20 | 21 | # build-in modules 22 | import argparse 23 | import logging 24 | 25 | # third-party modules 26 | import numpy 27 | 28 | # own modules 29 | from medpy.core import Logger 30 | from medpy.io import load, save 31 | 32 | # path changes 33 | 34 | 35 | # information 36 | __author__ = "Oskar Maier" 37 | __version__ = "r0.1.0, 2012-08-24" 38 | __email__ = "oskar.maier@googlemail.com" 39 | __status__ = "Release" 40 | __description__ = """ 41 | Creates an empty volume with the same attributes as the passes example image. 42 | 43 | Copyright (C) 2013 Oskar Maier 44 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 45 | and you are welcome to redistribute it under certain conditions; see 46 | the LICENSE file or for details. 47 | """ 48 | 49 | 50 | # code 51 | def main(): 52 | args = getArguments(getParser()) 53 | 54 | # prepare logger 55 | logger = Logger.getInstance() 56 | if args.debug: 57 | logger.setLevel(logging.DEBUG) 58 | elif args.verbose: 59 | logger.setLevel(logging.INFO) 60 | 61 | # loading input image 62 | input_data, input_header = load(args.example) 63 | 64 | # create empty volume with same attributes 65 | output_data = numpy.zeros(input_data.shape, dtype=input_data.dtype) 66 | 67 | # save resulting image 68 | save(output_data, args.output, input_header, args.force) 69 | 70 | logger.info("Successfully terminated.") 71 | 72 | 73 | def getArguments(parser): 74 | "Provides additional validation of the arguments collected by argparse." 75 | return parser.parse_args() 76 | 77 | 78 | def getParser(): 79 | "Creates and returns the argparse parser object." 80 | parser = argparse.ArgumentParser( 81 | description=__description__, formatter_class=argparse.RawTextHelpFormatter 82 | ) 83 | parser.add_argument("example", help="The example volume.") 84 | parser.add_argument("output", help="Target volume.") 85 | parser.add_argument( 86 | "-v", dest="verbose", action="store_true", help="Display more information." 87 | ) 88 | parser.add_argument( 89 | "-d", dest="debug", action="store_true", help="Display debug information." 90 | ) 91 | parser.add_argument( 92 | "-f", 93 | dest="force", 94 | action="store_true", 95 | help="Silently override existing output images.", 96 | ) 97 | return parser 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /lib/maxflow/src/graph.cpp: -------------------------------------------------------------------------------- 1 | /* graph.cpp */ 2 | 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "graph.h" 9 | 10 | 11 | template 12 | Graph::Graph(int node_num_max, int edge_num_max, void (*err_function)(char *)) 13 | : node_num(0), 14 | nodeptr_block(NULL), 15 | error_function(err_function) 16 | { 17 | if (node_num_max < 16) node_num_max = 16; 18 | if (edge_num_max < 16) edge_num_max = 16; 19 | 20 | nodes = (node*) malloc(node_num_max*sizeof(node)); 21 | arcs = (arc*) malloc(2*edge_num_max*sizeof(arc)); 22 | if (!nodes || !arcs) { if (error_function) (*error_function)("Not enough memory!"); exit(1); } 23 | 24 | node_last = nodes; 25 | node_max = nodes + node_num_max; 26 | arc_last = arcs; 27 | arc_max = arcs + 2*edge_num_max; 28 | 29 | maxflow_iteration = 0; 30 | flow = 0; 31 | } 32 | 33 | template 34 | Graph::~Graph() 35 | { 36 | if (nodeptr_block) 37 | { 38 | delete nodeptr_block; 39 | nodeptr_block = NULL; 40 | } 41 | free(nodes); 42 | free(arcs); 43 | } 44 | 45 | template 46 | void Graph::reset() 47 | { 48 | node_last = nodes; 49 | arc_last = arcs; 50 | node_num = 0; 51 | 52 | if (nodeptr_block) 53 | { 54 | delete nodeptr_block; 55 | nodeptr_block = NULL; 56 | } 57 | 58 | maxflow_iteration = 0; 59 | flow = 0; 60 | } 61 | 62 | template 63 | void Graph::reallocate_nodes(int num) 64 | { 65 | int node_num_max = (int)(node_max - nodes); 66 | node* nodes_old = nodes; 67 | 68 | node_num_max += node_num_max / 2; 69 | if (node_num_max < node_num + num) node_num_max = node_num + num; 70 | nodes = (node*) realloc(nodes_old, node_num_max*sizeof(node)); 71 | if (!nodes) { if (error_function) (*error_function)("Not enough memory!"); exit(1); } 72 | 73 | node_last = nodes + node_num; 74 | node_max = nodes + node_num_max; 75 | 76 | if (nodes != nodes_old) 77 | { 78 | arc* a; 79 | for (a=arcs; ahead = (node*) ((char*)a->head + (((char*) nodes) - ((char*) nodes_old))); 82 | } 83 | } 84 | } 85 | 86 | template 87 | void Graph::reallocate_arcs() 88 | { 89 | int arc_num_max = (int)(arc_max - arcs); 90 | int arc_num = (int)(arc_last - arcs); 91 | arc* arcs_old = arcs; 92 | 93 | arc_num_max += arc_num_max / 2; if (arc_num_max & 1) arc_num_max ++; 94 | arcs = (arc*) realloc(arcs_old, arc_num_max*sizeof(arc)); 95 | if (!arcs) { if (error_function) (*error_function)("Not enough memory!"); exit(1); } 96 | 97 | arc_last = arcs + arc_num; 98 | arc_max = arcs + arc_num_max; 99 | 100 | if (arcs != arcs_old) 101 | { 102 | node* i; 103 | arc* a; 104 | for (i=nodes; ifirst) i->first = (arc*) ((char*)i->first + (((char*) arcs) - ((char*) arcs_old))); 107 | } 108 | for (a=arcs; anext) a->next = (arc*) ((char*)a->next + (((char*) arcs) - ((char*) arcs_old))); 111 | a->sister = (arc*) ((char*)a->sister + (((char*) arcs) - ((char*) arcs_old))); 112 | } 113 | } 114 | } 115 | 116 | #include "instances.inc" 117 | -------------------------------------------------------------------------------- /bin/medpy_label_count.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Takes a number of label images and counts their regions. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see .""" 20 | 21 | # build-in modules 22 | import argparse 23 | import logging 24 | import sys 25 | 26 | # third-party modules 27 | import numpy 28 | 29 | from medpy.core import Logger 30 | 31 | # own modules 32 | from medpy.io import load 33 | 34 | # path changes 35 | 36 | 37 | # information 38 | __author__ = "Oskar Maier" 39 | __version__ = "r0.2, 2011-12-13" 40 | __email__ = "oskar.maier@googlemail.com" 41 | __status__ = "Release" 42 | __description__ = """ 43 | Counts the regions in a number of label images and prints the results 44 | to the stdout in csv syntax. 45 | 46 | Copyright (C) 2013 Oskar Maier 47 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 48 | and you are welcome to redistribute it under certain conditions; see 49 | the LICENSE file or for details. 50 | """ 51 | 52 | 53 | # code 54 | def main(): 55 | # parse cmd arguments 56 | parser = getParser() 57 | parser.parse_args() 58 | args = getArguments(parser) 59 | 60 | # prepare logger 61 | logger = Logger.getInstance() 62 | if args.debug: 63 | logger.setLevel(logging.DEBUG) 64 | elif args.verbose: 65 | logger.setLevel(logging.INFO) 66 | 67 | # write header line 68 | print("image;labels\n") 69 | 70 | # iterate over input images 71 | for image in args.images: 72 | # get and prepare image data 73 | logger.info("Processing image {}...".format(image)) 74 | image_data, _ = load(image) 75 | 76 | # count number of labels and flag a warning if they reach the ushort border 77 | count = len(numpy.unique(image_data)) 78 | 79 | # count number of labels and write 80 | print("{};{}\n".format(image.split("/")[-1], count)) 81 | 82 | sys.stdout.flush() 83 | 84 | logger.info("Successfully terminated.") 85 | 86 | 87 | def getArguments(parser): 88 | "Provides additional validation of the arguments collected by argparse." 89 | return parser.parse_args() 90 | 91 | 92 | def getParser(): 93 | "Creates and returns the argparse parser object." 94 | parser = argparse.ArgumentParser(description=__description__) 95 | parser.add_argument("images", nargs="+", help="One or more label images.") 96 | parser.add_argument( 97 | "-v", dest="verbose", action="store_true", help="Display more information." 98 | ) 99 | parser.add_argument( 100 | "-d", dest="debug", action="store_true", help="Display debug information." 101 | ) 102 | 103 | return parser 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /notebooks/scripts/medpy_create_empty_volume_by_example.py.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Simple script to create an empty volume by taking the metadata from an example image." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 2, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "!medpy_create_empty_volume_by_example.py resources/flair.nii.gz output/empty.nii.gz" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Let's compare the metadata." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 3, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "\n", 36 | "Informations obtained from image header:\n", 37 | "header type=\n", 38 | "voxel spacing=(1.0, 1.0)\n", 39 | "offset=(0.0, 0.0)\n", 40 | "\n", 41 | "Informations obtained from image array:\n", 42 | "datatype=float32,dimensions=2,shape=(181, 217)\n", 43 | "first and last element: 0.0 / 0.0\n" 44 | ] 45 | } 46 | ], 47 | "source": [ 48 | "!medpy_info.py resources/flair.nii.gz" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 4, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stdout", 58 | "output_type": "stream", 59 | "text": [ 60 | "\n", 61 | "Informations obtained from image header:\n", 62 | "header type=\n", 63 | "voxel spacing=(1.0, 1.0)\n", 64 | "offset=(0.0, 0.0)\n", 65 | "\n", 66 | "Informations obtained from image array:\n", 67 | "datatype=float32,dimensions=2,shape=(181, 217)\n", 68 | "first and last element: 0.0 / 0.0\n" 69 | ] 70 | } 71 | ], 72 | "source": [ 73 | "!medpy_info.py output/empty.nii.gz" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "The metadata is identical. But they do differ in every (non-zero) voxel." 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 5, 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "name": "stdout", 90 | "output_type": "stream", 91 | "text": [ 92 | "Voxel differ: 16453 of 39277 total voxels\n", 93 | "Max difference: 51866.8203125\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "!medpy_diff.py resources/flair.nii.gz output/empty.nii.gz" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": { 105 | "collapsed": true, 106 | "jupyter": { 107 | "outputs_hidden": true 108 | } 109 | }, 110 | "outputs": [], 111 | "source": [] 112 | } 113 | ], 114 | "metadata": { 115 | "kernelspec": { 116 | "display_name": "Python 3 (ipykernel)", 117 | "language": "python", 118 | "name": "python3" 119 | }, 120 | "language_info": { 121 | "codemirror_mode": { 122 | "name": "ipython", 123 | "version": 3 124 | }, 125 | "file_extension": ".py", 126 | "mimetype": "text/x-python", 127 | "name": "python", 128 | "nbconvert_exporter": "python", 129 | "pygments_lexer": "ipython3", 130 | "version": "3.10.12" 131 | } 132 | }, 133 | "nbformat": 4, 134 | "nbformat_minor": 4 135 | } 136 | -------------------------------------------------------------------------------- /bin/medpy_convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Convert an image from one format into another. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # own modules 27 | from medpy.core import Logger 28 | from medpy.io import load, save 29 | 30 | # third-party modules 31 | 32 | # path changes 33 | 34 | 35 | # information 36 | __author__ = "Oskar Maier" 37 | __version__ = "r0.1.1, 2012-05-25" 38 | __email__ = "oskar.maier@googlemail.com" 39 | __status__ = "Release" 40 | __description__ = """ 41 | Convert an image from one format into another. The image type is 42 | determined by the file suffixes. 43 | 44 | Copyright (C) 2013 Oskar Maier 45 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 46 | and you are welcome to redistribute it under certain conditions; see 47 | the LICENSE file or for details. 48 | """ 49 | 50 | 51 | # code 52 | def main(): 53 | args = getArguments(getParser()) 54 | 55 | # prepare logger 56 | logger = Logger.getInstance() 57 | if args.debug: 58 | logger.setLevel(logging.DEBUG) 59 | elif args.verbose: 60 | logger.setLevel(logging.INFO) 61 | 62 | # load input image 63 | data_input, header_input = load(args.input) 64 | 65 | # eventually empty data 66 | if args.empty: 67 | data_input.fill(False) 68 | 69 | # save resulting volume 70 | save(data_input, args.output, header_input, args.force) 71 | 72 | logger.info("Successfully terminated.") 73 | 74 | 75 | def getArguments(parser): 76 | "Provides additional validation of the arguments collected by argparse." 77 | return parser.parse_args() 78 | 79 | 80 | def getParser(): 81 | "Creates and returns the argparse parser object." 82 | parser = argparse.ArgumentParser(description=__description__) 83 | parser.add_argument("input", help="Source volume.") 84 | parser.add_argument("output", help="Target volume.") 85 | parser.add_argument( 86 | "-e", 87 | dest="empty", 88 | action="store_true", 89 | help="Instead of copying the voxel data, create an empty copy conserving all meta-data if possible.", 90 | ) 91 | parser.add_argument( 92 | "-v", dest="verbose", action="store_true", help="Display more information." 93 | ) 94 | parser.add_argument( 95 | "-d", dest="debug", action="store_true", help="Display debug information." 96 | ) 97 | parser.add_argument( 98 | "-f", 99 | dest="force", 100 | action="store_true", 101 | help="Silently override existing output images.", 102 | ) 103 | return parser 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /bin/medpy_dicom_slices_to_volume.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Converts a collection of DICOM slices into a proper image volume. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see .""" 20 | 21 | # build-in modules 22 | import argparse 23 | import logging 24 | 25 | # own modules 26 | from medpy.core import Logger 27 | from medpy.io import load, save 28 | 29 | # third-party modules 30 | 31 | # path changes 32 | 33 | 34 | # information 35 | __author__ = "Oskar Maier" 36 | __version__ = "r0.2.1, 2012-06-13" 37 | __email__ = "oskar.maier@googlemail.com" 38 | __status__ = "Release" 39 | __description__ = """ 40 | Converts a collection of DICOM slices (a DICOM series) into a proper 41 | image volume. Note that this operation does not preserve header 42 | information. 43 | 44 | Copyright (C) 2013 Oskar Maier 45 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 46 | and you are welcome to redistribute it under certain conditions; see 47 | the LICENSE file or for details. 48 | """ 49 | 50 | 51 | # code 52 | def main(): 53 | args = getArguments(getParser()) 54 | 55 | # prepare logger 56 | logger = Logger.getInstance() 57 | if args.debug: 58 | logger.setLevel(logging.DEBUG) 59 | elif args.verbose: 60 | logger.setLevel(logging.INFO) 61 | 62 | img, hdr = load(args.input) 63 | 64 | if args.spacing: 65 | print("{}".format(hdr.get_voxel_spacing())) 66 | return 0 67 | 68 | logger.debug("Resulting shape is {}.".format(img.shape)) 69 | 70 | # save resulting volume 71 | save(img, args.output, hdr, args.force) 72 | 73 | logger.info("Successfully terminated.") 74 | 75 | 76 | def getArguments(parser): 77 | "Provides additional validation of the arguments collected by argparse." 78 | return parser.parse_args() 79 | 80 | 81 | def getParser(): 82 | "Creates and returns the argparse parser object." 83 | parser = argparse.ArgumentParser(description=__description__) 84 | parser.add_argument("input", help="Source folder.") 85 | parser.add_argument("output", help="Target volume.") 86 | parser.add_argument( 87 | "-s", dest="spacing", action="store_true", help="Just print spacing and exit." 88 | ) 89 | parser.add_argument( 90 | "-v", dest="verbose", action="store_true", help="Display more information." 91 | ) 92 | parser.add_argument( 93 | "-d", dest="debug", action="store_true", help="Display debug information." 94 | ) 95 | parser.add_argument( 96 | "-f", 97 | dest="force", 98 | action="store_true", 99 | help="Silently override existing output images.", 100 | ) 101 | return parser 102 | 103 | 104 | if __name__ == "__main__": 105 | main() 106 | -------------------------------------------------------------------------------- /bin/medpy_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Print information about an image volume. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | from medpy.core import Logger 27 | 28 | # own modules 29 | from medpy.io import get_offset, get_pixel_spacing, load 30 | 31 | # third-party modules 32 | 33 | # path changes 34 | 35 | 36 | # information 37 | __author__ = "Oskar Maier" 38 | __version__ = "r0.2.1, 2012-05-24" 39 | __email__ = "oskar.maier@googlemail.com" 40 | __status__ = "Release" 41 | __description__ = """ 42 | Prints information about an image volume to the command line. 43 | 44 | Copyright (C) 2013 Oskar Maier 45 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 46 | and you are welcome to redistribute it under certain conditions; see 47 | the LICENSE file or for details. 48 | """ 49 | 50 | 51 | # code 52 | def main(): 53 | # parse cmd arguments 54 | parser = getParser() 55 | parser.parse_args() 56 | args = getArguments(parser) 57 | 58 | # prepare logger 59 | logger = Logger.getInstance() 60 | if args.debug: 61 | logger.setLevel(logging.DEBUG) 62 | elif args.verbose: 63 | logger.setLevel(logging.INFO) 64 | 65 | # load input image 66 | input_data, input_header = load(args.input) 67 | 68 | # print information about the image 69 | printInfo(input_data, input_header) 70 | 71 | logger.info("Successfully terminated.") 72 | 73 | 74 | def printInfo(data, header): 75 | # print image information 76 | print("\nInformations obtained from image header:") 77 | print("header type={}".format(type(header))) 78 | try: 79 | print("voxel spacing={}".format(get_pixel_spacing(header))) 80 | except AttributeError: 81 | print("Failed to retrieve voxel spacing.") 82 | try: 83 | print("offset={}".format(get_offset(header))) 84 | except AttributeError: 85 | print("Failed to retrieve offset.") 86 | 87 | print("\nInformations obtained from image array:") 88 | print( 89 | "datatype={},dimensions={},shape={}".format(data.dtype, data.ndim, data.shape) 90 | ) 91 | print( 92 | "first and last element: {} / {}".format(data.flatten()[0], data.flatten()[-1]) 93 | ) 94 | 95 | 96 | def getArguments(parser): 97 | "Provides additional validation of the arguments collected by argparse." 98 | return parser.parse_args() 99 | 100 | 101 | def getParser(): 102 | "Creates and returns the argparse parser object." 103 | parser = argparse.ArgumentParser(description=__description__) 104 | 105 | parser.add_argument("input", help="The image to analyse.") 106 | parser.add_argument( 107 | "-v", dest="verbose", action="store_true", help="Display more information." 108 | ) 109 | parser.add_argument( 110 | "-d", dest="debug", action="store_true", help="Display debug information." 111 | ) 112 | 113 | return parser 114 | 115 | 116 | if __name__ == "__main__": 117 | main() 118 | -------------------------------------------------------------------------------- /bin/medpy_merge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Merges to images into one. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # own modules 27 | from medpy.core import Logger 28 | from medpy.io import load, save 29 | 30 | # third-party modules 31 | 32 | # path changes 33 | 34 | 35 | # information 36 | __author__ = "Oskar Maier" 37 | __version__ = "r0.1.0, 2012-05-25" 38 | __email__ = "oskar.maier@googlemail.com" 39 | __status__ = "Release" 40 | __description__ = """ 41 | Merges to images into one. 42 | 43 | All voxels of the first supplied image that equal False (e.g. zeros), 44 | are replaced by the corresponding voxels of the second image. 45 | 46 | A common use case is the merging of two marker images. 47 | 48 | Copyright (C) 2013 Oskar Maier 49 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 50 | and you are welcome to redistribute it under certain conditions; see 51 | the LICENSE file or for details. 52 | """ 53 | 54 | 55 | # code 56 | def main(): 57 | args = getArguments(getParser()) 58 | 59 | # prepare logger 60 | logger = Logger.getInstance() 61 | if args.debug: 62 | logger.setLevel(logging.DEBUG) 63 | elif args.verbose: 64 | logger.setLevel(logging.INFO) 65 | 66 | # load first input image 67 | data_input1, header_input1 = load(args.input1) 68 | 69 | # load second input image 70 | data_input2, _ = load(args.input2) 71 | 72 | # merge 73 | data_input1[data_input1 == False] += data_input2[data_input1 == False] 74 | 75 | # save resulting volume 76 | save(data_input1, args.output, header_input1, args.force) 77 | 78 | logger.info("Successfully terminated.") 79 | 80 | 81 | def getArguments(parser): 82 | "Provides additional validation of the arguments collected by argparse." 83 | return parser.parse_args() 84 | 85 | 86 | def getParser(): 87 | "Creates and returns the argparse parser object." 88 | parser = argparse.ArgumentParser(description=__description__) 89 | parser.add_argument("input1", help="Source volume one.") 90 | parser.add_argument("input2", help="Source volume two.") 91 | parser.add_argument("output", help="Target volume.") 92 | parser.add_argument( 93 | "-e", 94 | dest="empty", 95 | action="store_true", 96 | help="Instead of copying the voxel data, create an empty copy conserving all meta-data if possible.", 97 | ) 98 | parser.add_argument( 99 | "-v", dest="verbose", action="store_true", help="Display more information." 100 | ) 101 | parser.add_argument( 102 | "-d", dest="debug", action="store_true", help="Display debug information." 103 | ) 104 | parser.add_argument( 105 | "-f", 106 | dest="force", 107 | action="store_true", 108 | help="Silently override existing output images.", 109 | ) 110 | return parser 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /tests/metric_/histogram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit and Hypothesis Tests for histogram metrics 3 | 4 | """ 5 | 6 | import numpy as np 7 | from hypothesis import assume, given 8 | from hypothesis import settings as hyp_settings 9 | from hypothesis import strategies 10 | 11 | from medpy.metric import histogram 12 | 13 | metric_list = ["manhattan", "minowski", "euclidean", "noelle_2", "noelle_4", "noelle_5"] 14 | metric_list_to_doublecheck = ["cosine_1"] 15 | 16 | unknown_property = ["histogram_intersection"] 17 | still_under_dev = ["quadratic_forms"] 18 | similarity_funcs = ["correlate", "cosine", "cosine_2", "cosine_alt", "fidelity_based"] 19 | semi_metric_list = [ 20 | "kullback_leibler", 21 | "jensen_shannon", 22 | "chi_square", 23 | "chebyshev", 24 | "chebyshev_neg", 25 | "histogram_intersection_1", 26 | "relative_deviation", 27 | "relative_bin_deviation", 28 | "noelle_1", 29 | "noelle_3", 30 | "correlate_1", 31 | ] 32 | 33 | default_feature_dim = 1000 34 | default_num_bins = 20 35 | 36 | range_feature_dim = [10, 10000] 37 | range_num_bins = [5, 200] 38 | 39 | 40 | def within_tolerance(x, y): 41 | "Function to indicate acceptable level of tolerance in numerical differences" 42 | 43 | # as the np.allcose function is not symmetric, 44 | # this ensurers second arg is larger 45 | if x < y: 46 | smaller = x 47 | larger = y 48 | else: 49 | smaller = y 50 | larger = x 51 | 52 | # atol=np.finfo(float).eps is super strict, choosing 1e-3 53 | # rtol=1e-2 approx. matches two decimal points 54 | return bool(np.allclose(smaller, larger, rtol=1e-2, atol=1e-3)) 55 | 56 | 57 | def make_random_histogram(length=default_feature_dim, num_bins=default_num_bins): 58 | "Returns a sequence of histogram density values that sum to 1.0" 59 | 60 | hist, bin_edges = np.histogram( 61 | np.random.random(length), bins=num_bins, density=True 62 | ) 63 | 64 | # to ensure they sum to 1.0 65 | hist = hist / sum(hist) 66 | 67 | if len(hist) < 2: 68 | raise ValueError("Invalid histogram") 69 | 70 | return hist 71 | 72 | 73 | # Increasing the number of examples to try 74 | @hyp_settings(max_examples=1000) # , verbosity=Verbosity.verbose) 75 | @given( 76 | strategies.sampled_from(metric_list), 77 | strategies.integers(range_feature_dim[0], range_feature_dim[1]), 78 | strategies.integers(range_num_bins[0], range_num_bins[1]), 79 | ) 80 | def test_math_properties_metric(method_str, feat_dim, num_bins): 81 | """Trying to test the four properties on the same set of histograms""" 82 | 83 | # checking for bad examples 84 | assume(not num_bins > feat_dim) 85 | 86 | h1 = make_random_histogram(feat_dim, num_bins) 87 | h2 = make_random_histogram(feat_dim, num_bins) 88 | h3 = make_random_histogram(feat_dim, num_bins) 89 | 90 | method = getattr(histogram, method_str) 91 | 92 | check_indiscernibility(method, h1) 93 | check_symmetry(method, h1, h2) 94 | check_nonnegativity(method, h1, h2) 95 | check_triangle_inequality(method, h1, h2, h3) 96 | 97 | 98 | def check_indiscernibility(method, hist): 99 | """a must be unique, and a is identical to a if and only if dist(a,a)=0""" 100 | 101 | assert within_tolerance(method(hist, hist), 0.0) 102 | 103 | 104 | def check_symmetry(method, h1, h2): 105 | """symmetry test""" 106 | 107 | d12 = method(h1, h2) 108 | d21 = method(h2, h1) 109 | 110 | assert within_tolerance(d12, d21) 111 | 112 | 113 | def check_nonnegativity(method, h1, h2): 114 | """distance between two samples must be >= 0.0""" 115 | 116 | assert method(h1, h2) >= 0.0 117 | 118 | 119 | def check_triangle_inequality(method, h1, h2, h3): 120 | """Classic test for a metric: dist(a,b) < dist(a,b) + dist(a,c)""" 121 | 122 | d12 = method(h1, h2) 123 | d23 = method(h2, h3) 124 | d13 = method(h1, h3) 125 | assert d12 <= d13 + d23 126 | -------------------------------------------------------------------------------- /tests/graphcut_/graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unittest for the medpy.graphcut.graph classes. 3 | 4 | !TODO: 5 | - Implement the test_Graph() test for the Graph class. Follow the line along test_GCGraph. 6 | 7 | @author Oskar Maier 8 | @version d0.2.1 9 | @since 2011-01-19 10 | @status Development 11 | """ 12 | 13 | # build-in modules 14 | import unittest 15 | 16 | # own modules 17 | from medpy.graphcut import GCGraph 18 | 19 | # third-party modules 20 | 21 | 22 | # code 23 | class TestGraph(unittest.TestCase): 24 | def test_Graph(self): 25 | """Test the @link medpy.graphcut.graph.Graph implementation.""" 26 | pass 27 | 28 | def test_GCGraph(self): 29 | """Test the @link medpy.graphcut.graph.GCGraph implementation.""" 30 | # set test parmeters 31 | nodes = 10 32 | edges = 20 33 | 34 | # construct graph 35 | graph = GCGraph(nodes, edges) # nodes edges 36 | 37 | # SETTER TESTS 38 | # set_source_nodes should accept a sequence and raise an error if an invalid node id was passed 39 | graph.set_source_nodes(list(range(0, nodes))) 40 | self.assertRaises(ValueError, graph.set_source_nodes, [-1]) 41 | self.assertRaises(ValueError, graph.set_source_nodes, [nodes]) 42 | # set_sink_nodes should accept a sequence and raise an error if an invalid node id was passed 43 | graph.set_sink_nodes(list(range(0, nodes))) 44 | self.assertRaises(ValueError, graph.set_sink_nodes, [-1]) 45 | self.assertRaises(ValueError, graph.set_sink_nodes, [nodes]) 46 | # set_nweight should accept integers resp. floats and raise an error if an invalid node id was passed or the weight is zero or negative 47 | graph.set_nweight(0, nodes - 1, 1, 2) 48 | graph.set_nweight(nodes - 1, 0, 0.5, 1.5) 49 | self.assertRaises(ValueError, graph.set_nweight, -1, 0, 1, 1) 50 | self.assertRaises(ValueError, graph.set_nweight, 0, nodes, 1, 1) 51 | self.assertRaises(ValueError, graph.set_nweight, 0, 0, 1, 1) 52 | self.assertRaises(ValueError, graph.set_nweight, 0, nodes - 1, 0, 0) 53 | self.assertRaises(ValueError, graph.set_nweight, 0, nodes - 1, -1, -2) 54 | self.assertRaises(ValueError, graph.set_nweight, 0, nodes - 1, -0.5, -1.5) 55 | # set_nweights works as set_nweight but takes a dictionary as argument 56 | graph.set_nweights({(0, nodes - 1): (1, 2)}) 57 | graph.set_nweights({(nodes - 1, 0): (0.5, 1.5)}) 58 | self.assertRaises(ValueError, graph.set_nweights, {(-1, 0): (1, 1)}) 59 | self.assertRaises(ValueError, graph.set_nweights, {(0, nodes): (1, 1)}) 60 | self.assertRaises(ValueError, graph.set_nweights, {(0, 0): (1, 1)}) 61 | self.assertRaises(ValueError, graph.set_nweights, {(0, nodes - 1): (0, 0)}) 62 | self.assertRaises(ValueError, graph.set_nweights, {(0, nodes - 1): (-1, -2)}) 63 | self.assertRaises( 64 | ValueError, graph.set_nweights, {(0, nodes - 1): (-0.5, -1.5)} 65 | ) 66 | # set_tweight should accept integers resp. floats and raise an error if an invalid node id was passed or the weight is zero or negative 67 | graph.set_tweight(0, 1, 2) 68 | graph.set_tweight(nodes - 1, 0.5, 1.5) 69 | graph.set_tweight(0, -1, -2) 70 | graph.set_tweight(0, 0, 0) 71 | self.assertRaises(ValueError, graph.set_tweight, -1, 1, 1) 72 | self.assertRaises(ValueError, graph.set_tweight, nodes, 1, 1) 73 | # set_tweights works as set_tweight but takes a dictionary as argument 74 | graph.set_tweights({0: (1, 2)}) 75 | graph.set_tweights({nodes - 1: (0.5, 1.5)}) 76 | graph.set_tweights({0: (-1, -2)}) 77 | graph.set_tweights({0: (0, 0)}) 78 | self.assertRaises(ValueError, graph.set_tweights, {-1: (1, 1)}) 79 | self.assertRaises(ValueError, graph.set_tweights, {nodes: (1, 1)}) 80 | 81 | # SOME MINOR GETTERS 82 | self.assertEqual(graph.get_node_count(), nodes) 83 | self.assertEqual(graph.get_edge_count(), edges) 84 | self.assertSequenceEqual(graph.get_nodes(), list(range(0, nodes))) 85 | 86 | 87 | if __name__ == "__main__": 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /bin/medpy_gradient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Executes gradient magnitude filter over images. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # third-party modules 27 | import numpy 28 | from scipy.ndimage import generic_gradient_magnitude, prewitt 29 | 30 | from medpy.core import Logger 31 | 32 | # own modules 33 | from medpy.io import load, save 34 | 35 | # path changes 36 | 37 | 38 | # information 39 | __author__ = "Oskar Maier" 40 | __version__ = "r0.2.0, 2011-12-12" 41 | __email__ = "oskar.maier@googlemail.com" 42 | __status__ = "Release" 43 | __description__ = """ 44 | Creates a height map of the input images using the gradient magnitude 45 | filter. 46 | The pixel type of the resulting image will be float. 47 | 48 | Copyright (C) 2013 Oskar Maier 49 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 50 | and you are welcome to redistribute it under certain conditions; see 51 | the LICENSE file or for details. 52 | """ 53 | 54 | 55 | # code 56 | def main(): 57 | # parse cmd arguments 58 | parser = getParser() 59 | parser.parse_args() 60 | args = getArguments(parser) 61 | 62 | # prepare logger 63 | logger = Logger.getInstance() 64 | if args.debug: 65 | logger.setLevel(logging.DEBUG) 66 | elif args.verbose: 67 | logger.setLevel(logging.INFO) 68 | 69 | # laod input image 70 | data_input, header_input = load(args.input) 71 | 72 | # # check if output image exists 73 | # if not args.force: 74 | # if os.path.exists(image_gradient_name): 75 | # logger.warning('The output image {} already exists. Skipping this step.'.format(image_gradient_name)) 76 | # continue 77 | 78 | # prepare result image 79 | data_output = numpy.zeros(data_input.shape, dtype=numpy.float32) 80 | 81 | # apply the gradient magnitude filter 82 | logger.info("Computing the gradient magnitude with Prewitt operator...") 83 | generic_gradient_magnitude( 84 | data_input, prewitt, output=data_output 85 | ) # alternative to prewitt is sobel 86 | 87 | # save resulting mask 88 | save(data_output, args.output, header_input, args.force) 89 | 90 | logger.info("Successfully terminated.") 91 | 92 | 93 | def getArguments(parser): 94 | "Provides additional validation of the arguments collected by argparse." 95 | return parser.parse_args() 96 | 97 | 98 | def getParser(): 99 | "Creates and returns the argparse parser object." 100 | parser = argparse.ArgumentParser(description=__description__) 101 | parser.add_argument("input", help="Source volume.") 102 | parser.add_argument("output", help="Target volume.") 103 | parser.add_argument( 104 | "-v", dest="verbose", action="store_true", help="Display more information." 105 | ) 106 | parser.add_argument( 107 | "-d", dest="debug", action="store_true", help="Display debug information." 108 | ) 109 | parser.add_argument( 110 | "-f", 111 | dest="force", 112 | action="store_true", 113 | help="Silently override existing output images.", 114 | ) 115 | 116 | return parser 117 | 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /bin/medpy_extract_min_max.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Extracts and displays the min/max values of a number of images. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | import os 26 | import sys 27 | 28 | # own modules 29 | from medpy.core import Logger 30 | from medpy.io import load 31 | 32 | # third-party modules 33 | 34 | # path changes 35 | 36 | 37 | # information 38 | __author__ = "Oskar Maier" 39 | __version__ = "r0.2, 2011-12-13" 40 | __email__ = "oskar.maier@googlemail.com" 41 | __status__ = "Release" 42 | __description__ = """ 43 | Extracts and displays the min/max values of a number of images 44 | and prints the results to the stdout in csv format. 45 | 46 | Copyright (C) 2013 Oskar Maier 47 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 48 | and you are welcome to redistribute it under certain conditions; see 49 | the LICENSE file or for details. 50 | """ 51 | 52 | 53 | # code 54 | def main(): 55 | # parse cmd arguments 56 | parser = getParser() 57 | parser.parse_args() 58 | args = getArguments(parser) 59 | 60 | # prepare logger 61 | logger = Logger.getInstance() 62 | if args.debug: 63 | logger.setLevel(logging.DEBUG) 64 | elif args.verbose: 65 | logger.setLevel(logging.INFO) 66 | 67 | # build output file name 68 | file_csv_name = args.csv + ".csv" 69 | 70 | # check if output file exists 71 | if not args.force: 72 | if os.path.exists(file_csv_name): 73 | logger.warning( 74 | "The output file {} already exists. Skipping.".format(file_csv_name) 75 | ) 76 | sys.exit(0) 77 | 78 | # write header line 79 | print("image;min;max\n") 80 | 81 | # iterate over input images 82 | for image in args.images: 83 | # get and prepare image data 84 | logger.info("Processing image {}...".format(image)) 85 | image_data, _ = load(image) 86 | 87 | # count number of labels and flag a warning if they reach the ushort border 88 | min_value = image_data.min() 89 | max_value = image_data.max() 90 | 91 | # count number of labels and write 92 | print("{};{};{}\n".format(image.split("/")[-1], min_value, max_value)) 93 | 94 | sys.stdout.flush() 95 | 96 | logger.info("Successfully terminated.") 97 | 98 | 99 | def getArguments(parser): 100 | "Provides additional validation of the arguments collected by argparse." 101 | return parser.parse_args() 102 | 103 | 104 | def getParser(): 105 | "Creates and returns the argparse parser object." 106 | parser = argparse.ArgumentParser(description=__description__) 107 | 108 | parser.add_argument("csv", help="The file to store the results in (\wo suffix).") 109 | parser.add_argument("images", nargs="+", help="One or more images.") 110 | parser.add_argument( 111 | "-v", dest="verbose", action="store_true", help="Display more information." 112 | ) 113 | parser.add_argument( 114 | "-d", dest="debug", action="store_true", help="Display debug information." 115 | ) 116 | parser.add_argument( 117 | "-f", 118 | dest="force", 119 | action="store_true", 120 | help="Silently override existing output images.", 121 | ) 122 | 123 | return parser 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /bin/medpy_diff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Compares the pixel values of two images and gives a measure of the difference. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see .""" 20 | 21 | import argparse 22 | import logging 23 | 24 | # build-in modules 25 | import sys 26 | from functools import reduce 27 | 28 | # third-party modules 29 | import numpy 30 | 31 | # own modules 32 | from medpy.core import Logger 33 | from medpy.io import load 34 | 35 | # path changes 36 | 37 | 38 | # information 39 | __author__ = "Oskar Maier" 40 | __version__ = "r0.1.0, 2012-05-25" 41 | __email__ = "oskar.maier@googlemail.com" 42 | __status__ = "Release" 43 | __description__ = """ 44 | Compares the pixel values of two images and gives a measure of the difference. 45 | 46 | Also compares the dtype and shape. 47 | 48 | Copyright (C) 2013 Oskar Maier 49 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 50 | and you are welcome to redistribute it under certain conditions; see 51 | the LICENSE file or for details. 52 | """ 53 | 54 | 55 | # code 56 | def main(): 57 | args = getArguments(getParser()) 58 | 59 | # prepare logger 60 | logger = Logger.getInstance() 61 | if args.debug: 62 | logger.setLevel(logging.DEBUG) 63 | elif args.verbose: 64 | logger.setLevel(logging.INFO) 65 | 66 | # load input image1 67 | data_input1, _ = load(args.input1) 68 | 69 | # load input image2 70 | data_input2, _ = load(args.input2) 71 | 72 | # compare dtype and shape 73 | if not data_input1.dtype == data_input2.dtype: 74 | print("Dtype differs: {} to {}".format(data_input1.dtype, data_input2.dtype)) 75 | if not data_input1.shape == data_input2.shape: 76 | print("Shape differs: {} to {}".format(data_input1.shape, data_input2.shape)) 77 | print( 78 | "The voxel content of images of different shape can not be compared. Exiting." 79 | ) 80 | sys.exit(-1) 81 | 82 | # compare image data 83 | voxel_total = reduce(lambda x, y: x * y, data_input1.shape) 84 | voxel_difference = len((data_input1 != data_input2).nonzero()[0]) 85 | if not 0 == voxel_difference: 86 | print( 87 | "Voxel differ: {} of {} total voxels".format(voxel_difference, voxel_total) 88 | ) 89 | print( 90 | "Max difference: {}".format(numpy.absolute(data_input1 - data_input2).max()) 91 | ) 92 | else: 93 | print("No other difference.") 94 | 95 | logger.info("Successfully terminated.") 96 | 97 | 98 | def getArguments(parser): 99 | "Provides additional validation of the arguments collected by argparse." 100 | return parser.parse_args() 101 | 102 | 103 | def getParser(): 104 | "Creates and returns the argparse parser object." 105 | parser = argparse.ArgumentParser(description=__description__) 106 | parser.add_argument("input1", help="Source volume one.") 107 | parser.add_argument("input2", help="Source volume two.") 108 | parser.add_argument( 109 | "-v", dest="verbose", action="store_true", help="Display more information." 110 | ) 111 | parser.add_argument( 112 | "-d", dest="debug", action="store_true", help="Display debug information." 113 | ) 114 | parser.add_argument( 115 | "-f", 116 | dest="force", 117 | action="store_true", 118 | help="Silently override existing output images.", 119 | ) 120 | return parser 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /medpy/metric/image.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.1.0 18 | # since 2013-07-09 19 | # status Release 20 | 21 | # build-in modules 22 | 23 | # third-party modules 24 | import numpy 25 | 26 | # own modules 27 | from ..core import ArgumentError 28 | 29 | 30 | # code 31 | def mutual_information(i1, i2, bins=256): 32 | r""" 33 | Computes the mutual information (MI) (a measure of entropy) between two images. 34 | 35 | MI is not real metric, but a symmetric and nonnegative similarity measures that 36 | takes high values for similar images. Negative values are also possible. 37 | 38 | Intuitively, mutual information measures the information that ``i1`` and ``i2`` share: it 39 | measures how much knowing one of these variables reduces uncertainty about the other. 40 | 41 | The Entropy is defined as: 42 | 43 | .. math:: 44 | 45 | H(X) = - \sum_i p(g_i) * ln(p(g_i) 46 | 47 | with :math:`p(g_i)` being the intensity probability of the images grey value :math:`g_i`. 48 | 49 | Assuming two images :math:`R` and :math:`T`, the mutual information is then computed by comparing the 50 | images entropy values (i.e. a measure how well-structured the common histogram is). 51 | The distance metric is then calculated as follows: 52 | 53 | .. math:: 54 | 55 | MI(R,T) = H(R) + H(T) - H(R,T) = H(R) - H(R|T) = H(T) - H(T|R) 56 | 57 | A maximization of the mutual information is equal to a minimization of the joint 58 | entropy. 59 | 60 | Parameters 61 | ---------- 62 | i1 : array_like 63 | The first image. 64 | i2 : array_like 65 | The second image. 66 | bins : integer 67 | The number of histogram bins (squared for the joined histogram). 68 | 69 | Returns 70 | ------- 71 | mutual_information : float 72 | The mutual information distance value between the supplied images. 73 | 74 | Raises 75 | ------ 76 | ArgumentError 77 | If the supplied arrays are of different shape. 78 | """ 79 | # pre-process function arguments 80 | i1 = numpy.asarray(i1) 81 | i2 = numpy.asarray(i2) 82 | 83 | # validate function arguments 84 | if not i1.shape == i2.shape: 85 | raise ArgumentError( 86 | "the two supplied array-like sequences i1 and i2 must be of the same shape" 87 | ) 88 | 89 | # compute i1 and i2 histogram range 90 | i1_range = __range(i1, bins) 91 | i2_range = __range(i2, bins) 92 | 93 | # compute joined and separated normed histograms 94 | i1i2_hist, _, _ = numpy.histogram2d( 95 | i1.flatten(), i2.flatten(), bins=bins, range=[i1_range, i2_range] 96 | ) # Note: histogram2d does not flatten array on its own 97 | i1_hist, _ = numpy.histogram(i1, bins=bins, range=i1_range) 98 | i2_hist, _ = numpy.histogram(i2, bins=bins, range=i2_range) 99 | 100 | # compute joined and separated entropy 101 | i1i2_entropy = __entropy(i1i2_hist) 102 | i1_entropy = __entropy(i1_hist) 103 | i2_entropy = __entropy(i2_hist) 104 | 105 | # compute and return the mutual information distance 106 | return i1_entropy + i2_entropy - i1i2_entropy 107 | 108 | 109 | def __range(a, bins): 110 | """Compute the histogram range of the values in the array a according to 111 | scipy.stats.histogram.""" 112 | a = numpy.asarray(a) 113 | a_max = a.max() 114 | a_min = a.min() 115 | s = 0.5 * (a_max - a_min) / float(bins - 1) 116 | return (a_min - s, a_max + s) 117 | 118 | 119 | def __entropy(data): 120 | """Compute entropy of the flattened data set (e.g. a density distribution).""" 121 | # normalize and convert to float 122 | data = data / float(numpy.sum(data)) 123 | # for each grey-value g with a probability p(g) = 0, the entropy is defined as 0, therefore we remove these values and also flatten the histogram 124 | data = data[numpy.nonzero(data)] 125 | # compute entropy 126 | return -1.0 * numpy.sum(data * numpy.log2(data)) 127 | -------------------------------------------------------------------------------- /medpy/io/save.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.2.1 18 | # since 2012-05-28 19 | # status Release 20 | 21 | # build-in modules 22 | import os 23 | 24 | # third-party modules 25 | import numpy as np 26 | import SimpleITK as sitk 27 | 28 | # own modules 29 | from ..core import ImageSavingError, Logger 30 | 31 | 32 | # code 33 | def save(arr, filename, hdr=False, force=True, use_compression=False): 34 | r""" 35 | Save the image ``arr`` as filename using information encoded in ``hdr``. The target image 36 | format is determined by the ``filename`` suffix. If the ``force`` parameter is set to true, 37 | an already existing image is overwritten silently. Otherwise an error is thrown. 38 | 39 | The header (``hdr``) object is the one returned by `~medpy.io.load.load` and is used 40 | opportunistically, possibly loosing some meta-information. 41 | 42 | Generally this function does not guarantee, that metadata other than the image shape 43 | and pixel data type are kept. 44 | 45 | MedPy relies on SimpleITK, which enables the power of ITK for image loading and saving. 46 | The supported image file formats should include at least the following. 47 | 48 | Medical formats: 49 | 50 | - ITK MetaImage (.mha/.raw, .mhd) 51 | - Neuroimaging Informatics Technology Initiative (NIfTI) (.nia, .nii, .nii.gz, .hdr, .img, .img.gz) 52 | - Analyze (plain, SPM99, SPM2) (.hdr/.img, .img.gz) 53 | - Digital Imaging and Communications in Medicine (DICOM) (.dcm, .dicom) 54 | - Digital Imaging and Communications in Medicine (DICOM) series (/) 55 | - Nearly Raw Raster Data (Nrrd) (.nrrd, .nhdr) 56 | - Medical Imaging NetCDF (MINC) (.mnc, .MNC) 57 | - Guys Image Processing Lab (GIPL) (.gipl, .gipl.gz) 58 | 59 | Microscopy formats: 60 | 61 | - Medical Research Council (MRC) (.mrc, .rec) 62 | - Bio-Rad (.pic, .PIC) 63 | - LSM (Zeiss) microscopy images (.tif, .TIF, .tiff, .TIFF, .lsm, .LSM) 64 | - Stimulate / Signal Data (SDT) (.sdt) 65 | 66 | Visualization formats: 67 | 68 | - VTK images (.vtk) 69 | 70 | Other formats: 71 | 72 | - Portable Network Graphics (PNG) (.png, .PNG) 73 | - Joint Photographic Experts Group (JPEG) (.jpg, .JPG, .jpeg, .JPEG) 74 | - Tagged Image File Format (TIFF) (.tif, .TIF, .tiff, .TIFF) 75 | - Windows bitmap (.bmp, .BMP) 76 | - Hierarchical Data Format (HDF5) (.h5 , .hdf5 , .he5) 77 | - MSX-DOS Screen-x (.ge4, .ge5) 78 | 79 | For informations about which image formats, dimensionalities and pixel data types 80 | your current configuration supports, run `python3 tests/support.py > myformats.log`. 81 | 82 | Further information see https://simpleitk.readthedocs.io . 83 | 84 | Parameters 85 | ---------- 86 | arr : array_like 87 | The image data with order `x,y,z,c`. 88 | filename : string 89 | Where to save the image; path and filename including the image suffix. 90 | hdr : object 91 | The image header containing the metadata. 92 | force : bool 93 | Set to True to overwrite already exiting image silently. 94 | use_compression : bool 95 | Use data compression of the target format supports it. 96 | 97 | Raises 98 | ------ 99 | ImageSavingError 100 | If the image could not be saved due to various reasons 101 | """ 102 | logger = Logger.getInstance() 103 | logger.info("Saving image as {}...".format(filename)) 104 | 105 | # Check image file existance 106 | if not force and os.path.exists(filename): 107 | raise ImageSavingError("The target file {} already exists.".format(filename)) 108 | 109 | # Roll axes from x,y,z,c to z,y,x,c 110 | if arr.ndim == 4: 111 | arr = np.moveaxis(arr, -1, 0) 112 | arr = arr.T 113 | 114 | # treat unsupported dtypes 115 | if arr.dtype == bool: 116 | arr = arr.astype(np.uint8) 117 | 118 | sitkimage = sitk.GetImageFromArray(arr) 119 | 120 | # Copy met-data as far as possible 121 | if hdr: 122 | hdr.copy_to(sitkimage) 123 | 124 | sitk.WriteImage(sitkimage, filename, use_compression) 125 | -------------------------------------------------------------------------------- /bin/medpy_fit_into_shape.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Fit an existing image into a new shape. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | import argparse 23 | import logging 24 | 25 | # build-in modules 26 | import os 27 | 28 | # third-party modules 29 | import numpy 30 | 31 | # own modules 32 | from medpy.core import Logger 33 | from medpy.io import load, save 34 | from medpy.utilities import argparseu 35 | 36 | # information 37 | __author__ = "Oskar Maier" 38 | __version__ = "r0.1.0, 2014-11-25" 39 | __email__ = "oskar.maier@googlemail.com" 40 | __status__ = "Release" 41 | __description__ = """ 42 | Fit an existing image into a new shape. 43 | 44 | If larger, the original image is placed centered in all dimensions. If smaller, 45 | it is cut equally at all sides. 46 | 47 | Copyright (C) 2013 Oskar Maier 48 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 49 | and you are welcome to redistribute it under certain conditions; see 50 | the LICENSE file or for details. 51 | """ 52 | 53 | 54 | # code 55 | def main(): 56 | parser = getParser() 57 | args = getArguments(parser) 58 | 59 | # prepare logger 60 | logger = Logger.getInstance() 61 | if args.debug: 62 | logger.setLevel(logging.DEBUG) 63 | elif args.verbose: 64 | logger.setLevel(logging.INFO) 65 | 66 | # loading input images 67 | img, hdr = load(args.input) 68 | 69 | # check shape dimensionality 70 | if not len(args.shape) == img.ndim: 71 | parser.error( 72 | "The image has {} dimensions, but {} shape parameters have been supplied.".format( 73 | img.ndim, len(args.shape) 74 | ) 75 | ) 76 | 77 | # check if output image exists 78 | if not args.force and os.path.exists(args.output): 79 | parser.error("The output image {} already exists.".format(args.output)) 80 | 81 | # compute required cropping and extention 82 | slicers_cut = [] 83 | slicers_extend = [] 84 | for dim in range(len(img.shape)): 85 | slicers_cut.append(slice(None)) 86 | slicers_extend.append(slice(None)) 87 | if args.shape[dim] != img.shape[dim]: 88 | difference = abs(img.shape[dim] - args.shape[dim]) 89 | cutoff_left = difference / 2 90 | cutoff_right = difference / 2 + difference % 2 91 | if args.shape[dim] > img.shape[dim]: 92 | slicers_extend[-1] = slice(cutoff_left, -1 * cutoff_right) 93 | else: 94 | slicers_cut[-1] = slice(cutoff_left, -1 * cutoff_right) 95 | 96 | # crop original image 97 | img = img[tuple(slicers_cut)] 98 | 99 | # create output image and place input image centered 100 | out = numpy.zeros(args.shape, img.dtype) 101 | out[tuple(slicers_extend)] = img 102 | 103 | # saving the resulting image 104 | save(out, args.output, hdr, args.force) 105 | 106 | 107 | def getArguments(parser): 108 | "Provides additional validation of the arguments collected by argparse." 109 | return parser.parse_args() 110 | 111 | 112 | def getParser(): 113 | "Creates and returns the argparse parser object." 114 | parser = argparse.ArgumentParser( 115 | formatter_class=argparse.RawDescriptionHelpFormatter, 116 | description=__description__, 117 | ) 118 | parser.add_argument("input", help="the input image") 119 | parser.add_argument("output", help="the output image") 120 | parser.add_argument( 121 | "shape", 122 | type=argparseu.sequenceOfIntegersGt, 123 | help="the desired shape in colon-separated values, e.g. 255,255,32", 124 | ) 125 | 126 | parser.add_argument( 127 | "-v", "--verbose", dest="verbose", action="store_true", help="verbose output" 128 | ) 129 | parser.add_argument( 130 | "-d", dest="debug", action="store_true", help="Display debug information." 131 | ) 132 | parser.add_argument( 133 | "-f", 134 | "--force", 135 | dest="force", 136 | action="store_true", 137 | help="overwrite existing files", 138 | ) 139 | return parser 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /medpy/neighbours/knn.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.1.0 18 | # since 2014-10-15 19 | # status Release 20 | 21 | # build-in modules 22 | import warnings 23 | from itertools import combinations 24 | 25 | # third-party modules 26 | import numpy 27 | from scipy.sparse.csr import csr_matrix 28 | 29 | # own modules 30 | 31 | # constants 32 | 33 | 34 | # code 35 | def mkneighbors_graph( 36 | observations, n_neighbours, metric, mode="connectivity", metric_params=None 37 | ): 38 | """ 39 | Computes the (weighted) graph of mutual k-Neighbors for observations. 40 | 41 | Notes 42 | ----- 43 | The distance between an observation and itself is never computed and instead set to 44 | ``numpy.inf``. I.e. only in the case of k>=n_observations or when the ``metric`` 45 | returns ``numpy.inf``, the returned graph can contain loops. 46 | 47 | Parameters 48 | ---------- 49 | observations : sequence 50 | Sequence of observations. 51 | n_neighbours : int 52 | Maximum number of neighbours for each sample. 53 | metric : function 54 | The distance metric taking two observations and returning a numeric value > 0. 55 | mode : {'connectivity', 'distance', 'both'}, optional 56 | Type of returned matrix: 'connectivity' will return the connectivity matrix with 57 | ones and zeros, in 'distance' the edges are distances between points, while 58 | 'both' returns a (connectivity, distance) tuple. 59 | metric_params : dict, optional (default = None) 60 | Additional keyword arguments for the metric function. 61 | 62 | Returns 63 | ------- 64 | mkneighbors_graph : ndarray 65 | Sparse matrix in CSR format, shape = [n_observations, n_observations]. 66 | mkneighbors_graph[i, j] is assigned the weight of edge that connects i to j. 67 | Might contain ``numpy.inf`` values. 68 | 69 | """ 70 | # compute their pairwise-distances 71 | pdists = pdist(observations, metric) 72 | 73 | # get the k nearest neighbours for each patch 74 | k_nearest_nbhs = numpy.argsort(pdists)[:, :n_neighbours] 75 | 76 | # create a mask denoting the k nearest neighbours in image_pdist 77 | k_nearest_mutual_nbhs_mask = numpy.zeros(pdists.shape, numpy.bool_) 78 | for _mask_row, _nbhs_row in zip(k_nearest_mutual_nbhs_mask, k_nearest_nbhs): 79 | _mask_row[_nbhs_row] = True 80 | 81 | # and with transposed to remove non-mutual nearest neighbours 82 | k_nearest_mutual_nbhs_mask &= k_nearest_mutual_nbhs_mask.T 83 | 84 | # set distance not in the mutual k nearest neighbour set to zero 85 | pdists[~k_nearest_mutual_nbhs_mask] = 0 86 | 87 | # check for edges with zero-weight 88 | if numpy.any(pdists[k_nearest_mutual_nbhs_mask] == 0): 89 | warnings.warn('The graph contains at least one edge with a weight of "0".') 90 | 91 | if "connectivity" == mode: 92 | return csr_matrix(k_nearest_mutual_nbhs_mask) 93 | elif "distance" == mode: 94 | return csr_matrix(pdists) 95 | else: 96 | return csr_matrix(k_nearest_mutual_nbhs_mask), csr_matrix(pdists) 97 | 98 | 99 | def pdist(objects, dmeasure, diagval=numpy.inf): 100 | """ 101 | Compute the pair-wise distances between arbitrary objects. 102 | 103 | Notes 104 | ----- 105 | ``dmeasure`` is assumed to be *symmetry* i.e. between object *a* and object *b* the 106 | function will be called only ones. 107 | 108 | Parameters 109 | ---------- 110 | objects : sequence 111 | A sequence of objects of length *n*. 112 | dmeasure : function 113 | A callable function that takes two objects as input at returns a number. 114 | diagval : number 115 | The diagonal values of the resulting array. 116 | 117 | Returns 118 | ------- 119 | pdists : ndarray 120 | An *nxn* symmetric float array containing the pair-wise distances. 121 | """ 122 | out = numpy.zeros([len(objects)] * 2, float) 123 | numpy.fill_diagonal(out, diagval) 124 | for idx1, idx2 in combinations(list(range(len(objects))), 2): 125 | out[idx1, idx2] = dmeasure(objects[idx1], objects[idx2]) 126 | return out + out.T 127 | -------------------------------------------------------------------------------- /bin/medpy_swap_dimensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Loads an image and saves it with two dimensions swapped. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # third-party modules 27 | import numpy 28 | 29 | # own modules 30 | from medpy.core import Logger 31 | from medpy.core.exceptions import ArgumentError 32 | from medpy.io import header, load, save 33 | 34 | # path changes 35 | 36 | 37 | # information 38 | __author__ = "Oskar Maier" 39 | __version__ = "r0.1.0, 2012-05-25" 40 | __email__ = "oskar.maier@googlemail.com" 41 | __status__ = "Release" 42 | __description__ = """ 43 | Two of the input images dimensions are swapped. A (200,100,10) image 44 | can such be turned into a (200,10,100) one. 45 | 46 | Copyright (C) 2013 Oskar Maier 47 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 48 | and you are welcome to redistribute it under certain conditions; see 49 | the LICENSE file or for details. 50 | """ 51 | 52 | 53 | # code 54 | def main(): 55 | args = getArguments(getParser()) 56 | 57 | # prepare logger 58 | logger = Logger.getInstance() 59 | if args.debug: 60 | logger.setLevel(logging.DEBUG) 61 | elif args.verbose: 62 | logger.setLevel(logging.INFO) 63 | 64 | # load input image 65 | data_input, header_input = load(args.input) 66 | 67 | logger.debug("Original shape = {}.".format(data_input.shape)) 68 | 69 | # check if supplied dimension parameters is inside the images dimensions 70 | if args.dimension1 >= data_input.ndim or args.dimension1 < 0: 71 | raise ArgumentError( 72 | "The first swap-dimension {} exceeds the number of input volume dimensions {}.".format( 73 | args.dimension1, data_input.ndim 74 | ) 75 | ) 76 | elif args.dimension2 >= data_input.ndim or args.dimension2 < 0: 77 | raise ArgumentError( 78 | "The second swap-dimension {} exceeds the number of input volume dimensions {}.".format( 79 | args.dimension2, data_input.ndim 80 | ) 81 | ) 82 | 83 | # swap axes 84 | data_output = numpy.swapaxes(data_input, args.dimension1, args.dimension2) 85 | # swap pixel spacing and offset 86 | ps = list(header.get_pixel_spacing(header_input)) 87 | ps[args.dimension1], ps[args.dimension2] = ps[args.dimension2], ps[args.dimension1] 88 | header.set_pixel_spacing(header_input, ps) 89 | os = list(header.get_offset(header_input)) 90 | os[args.dimension1], os[args.dimension2] = os[args.dimension2], os[args.dimension1] 91 | header.set_offset(header_input, os) 92 | 93 | logger.debug("Resulting shape = {}.".format(data_output.shape)) 94 | 95 | # save resulting volume 96 | save(data_output, args.output, header_input, args.force) 97 | 98 | logger.info("Successfully terminated.") 99 | 100 | 101 | def getArguments(parser): 102 | "Provides additional validation of the arguments collected by argparse." 103 | return parser.parse_args() 104 | 105 | 106 | def getParser(): 107 | "Creates and returns the argparse parser object." 108 | parser = argparse.ArgumentParser(description=__description__) 109 | parser.add_argument("input", help="Source volume.") 110 | parser.add_argument("output", help="Target volume.") 111 | parser.add_argument( 112 | "dimension1", type=int, help="First dimension to swap (starting from 0)." 113 | ) 114 | parser.add_argument( 115 | "dimension2", type=int, help="Second dimension to swap (starting from 0)." 116 | ) 117 | parser.add_argument( 118 | "-v", dest="verbose", action="store_true", help="Display more information." 119 | ) 120 | parser.add_argument( 121 | "-d", dest="debug", action="store_true", help="Display debug information." 122 | ) 123 | parser.add_argument( 124 | "-f", 125 | dest="force", 126 | action="store_true", 127 | help="Silently override existing output images.", 128 | ) 129 | return parser 130 | 131 | 132 | if __name__ == "__main__": 133 | main() 134 | -------------------------------------------------------------------------------- /medpy/io/load.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.3.1 18 | # since 2012-05-28 19 | # status Release 20 | 21 | # build-in modules 22 | import os.path 23 | 24 | # third-party modules 25 | import numpy as np 26 | import SimpleITK as sitk 27 | 28 | from ..core import ImageLoadingError, Logger 29 | 30 | # own modules 31 | from .header import Header 32 | 33 | 34 | # code 35 | def load(image): 36 | r""" 37 | Loads the ``image`` and returns a ndarray with the image's pixel content as well as 38 | a header object. 39 | 40 | The header can, with restrictions, be used to extract additional meta-information 41 | about the image (e.g. using the methods in `~medpy.io.Header`). Additionally 42 | it serves as meta-data container that can be passes to `~medpy.io.save.save` when the 43 | altered image is saved to the hard drive again. Note that the transfer of meta-data is 44 | only possible, and even then not guaranteed, when the source and target image formats 45 | are the same. 46 | 47 | MedPy relies on SimpleITK, which enables the power of ITK for image loading and saving. 48 | The supported image file formats should include at least the following. 49 | 50 | Medical formats: 51 | 52 | - ITK MetaImage (.mha/.raw, .mhd) 53 | - Neuroimaging Informatics Technology Initiative (NIfTI) (.nia, .nii, .nii.gz, .hdr, .img, .img.gz) 54 | - Analyze (plain, SPM99, SPM2) (.hdr/.img, .img.gz) 55 | - Digital Imaging and Communications in Medicine (DICOM) (.dcm, .dicom) 56 | - Digital Imaging and Communications in Medicine (DICOM) series (/) 57 | - Nearly Raw Raster Data (Nrrd) (.nrrd, .nhdr) 58 | - Medical Imaging NetCDF (MINC) (.mnc, .MNC) 59 | - Guys Image Processing Lab (GIPL) (.gipl, .gipl.gz) 60 | 61 | Microscopy formats: 62 | 63 | - Medical Research Council (MRC) (.mrc, .rec) 64 | - Bio-Rad (.pic, .PIC) 65 | - LSM (Zeiss) microscopy images (.tif, .TIF, .tiff, .TIFF, .lsm, .LSM) 66 | - Stimulate / Signal Data (SDT) (.sdt) 67 | 68 | Visualization formats: 69 | 70 | - VTK images (.vtk) 71 | 72 | Other formats: 73 | 74 | - Portable Network Graphics (PNG) (.png, .PNG) 75 | - Joint Photographic Experts Group (JPEG) (.jpg, .JPG, .jpeg, .JPEG) 76 | - Tagged Image File Format (TIFF) (.tif, .TIF, .tiff, .TIFF) 77 | - Windows bitmap (.bmp, .BMP) 78 | - Hierarchical Data Format (HDF5) (.h5 , .hdf5 , .he5) 79 | - MSX-DOS Screen-x (.ge4, .ge5) 80 | 81 | For informations about which image formats, dimensionalities and pixel data types 82 | your current configuration supports, run `python3 tests/support.py > myformats.log`. 83 | 84 | Further information see https://simpleitk.readthedocs.io . 85 | 86 | Parameters 87 | ---------- 88 | image : string 89 | Path to the image to load. 90 | 91 | Returns 92 | ------- 93 | image_data : ndarray 94 | The image data as numpy array with order `x,y,z,c`. 95 | image_header : Header 96 | The image metadata as :mod:`medpy.io.Header`. 97 | 98 | Raises 99 | ------ 100 | ImageLoadingError 101 | If the image could not be loaded due to some reason. 102 | """ 103 | logger = Logger.getInstance() 104 | logger.info("Loading image {}...".format(image)) 105 | 106 | if not os.path.exists(image): 107 | raise ImageLoadingError("The supplied image {} does not exist.".format(image)) 108 | 109 | if os.path.isdir(image): 110 | # !TODO: this does not load the meta-data, find a way to load it from a series, too 111 | logger.info( 112 | "Loading image as DICOM series. If more than one found in folder {} defaulting to first.".format( 113 | image 114 | ) 115 | ) 116 | sitkimage = sitk.ReadImage(sitk.ImageSeriesReader_GetGDCMSeriesFileNames(image)) 117 | else: 118 | sitkimage = sitk.ReadImage(image) 119 | 120 | # Make image array data and header 121 | header = Header(sitkimage=sitkimage) 122 | image = sitk.GetArrayFromImage(sitkimage) 123 | 124 | # Roll axes from z,y,x,c to x,y,z,c 125 | if image.ndim == 4: 126 | image = np.moveaxis(image, -1, 0) 127 | image = image.T 128 | 129 | return image, header 130 | -------------------------------------------------------------------------------- /bin/medpy_label_fit_to_mask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Executes a reduce operation taking a mask and a number of label images as input. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | import os 26 | 27 | # third-party modules 28 | import numpy 29 | 30 | from medpy.core import Logger 31 | from medpy.filter import fit_labels_to_mask 32 | 33 | # own modules 34 | from medpy.io import load, save 35 | 36 | # path changes 37 | 38 | 39 | # information 40 | __author__ = "Oskar Maier" 41 | __version__ = "r0.2.0, 2011-12-12" 42 | __email__ = "oskar.maier@googlemail.com" 43 | __status__ = "Release" 44 | __description__ = """ 45 | Reduces label images by fitting them as best as possible to a supplied 46 | mask and subsequently creating mask out of them. 47 | The resulting image is saved in the supplied folder with the same 48 | name as the input image, but with a suffix '_reduced' attached. 49 | For each region the intersection with the reference mask is computed 50 | and if the value exceeds 50% of the total region size, it is marked 51 | as mask, otherwise as background. For more details on how the fitting 52 | is performed @see filter.fit_labels_to_mask. 53 | 54 | Copyright (C) 2013 Oskar Maier 55 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 56 | and you are welcome to redistribute it under certain conditions; see 57 | the LICENSE file or for details. 58 | """ 59 | 60 | 61 | # code 62 | def main(): 63 | # parse cmd arguments 64 | parser = getParser() 65 | parser.parse_args() 66 | args = getArguments(parser) 67 | 68 | # prepare logger 69 | logger = Logger.getInstance() 70 | if args.debug: 71 | logger.setLevel(logging.DEBUG) 72 | elif args.verbose: 73 | logger.setLevel(logging.INFO) 74 | 75 | # load input image 76 | logger.info("Loading image {}...".format(args.input)) 77 | image_labels_data, _ = load(args.image) 78 | 79 | # load mask image 80 | logger.info("Loading mask {}...".format(args.mask)) 81 | image_mask_data, image_mask_data_header = load(args.mask) 82 | 83 | # check if output image exists 84 | if not args.force: 85 | if os.path.exists(args.output): 86 | logger.warning( 87 | "The output image {} already exists. Skipping this image.".format( 88 | args.output 89 | ) 90 | ) 91 | 92 | # create a mask from the label image 93 | logger.info("Reducing the label image...") 94 | image_reduced_data = fit_labels_to_mask(image_labels_data, image_mask_data) 95 | 96 | # save resulting mask 97 | logger.info( 98 | "Saving resulting mask as {} in the same format as input mask, only with data-type int8...".format( 99 | args.output 100 | ) 101 | ) 102 | image_reduced_data = image_reduced_data.astype( 103 | numpy.bool_, copy=False 104 | ) # bool sadly not recognized 105 | save(image_reduced_data, args.output, image_mask_data_header, args.force) 106 | 107 | logger.info("Successfully terminated.") 108 | 109 | 110 | def getArguments(parser): 111 | "Provides additional validation of the arguments collected by argparse." 112 | return parser.parse_args() 113 | 114 | 115 | def getParser(): 116 | "Creates and returns the argparse parser object." 117 | parser = argparse.ArgumentParser(description=__description__) 118 | 119 | parser.add_argument("image", nargs="+", help="The input label image.") 120 | parser.add_argument("mask", help="The mask image to which to fit the label images.") 121 | parser.add_argument("output", help="The output image.") 122 | parser.add_argument( 123 | "-v", dest="verbose", action="store_true", help="Display more information." 124 | ) 125 | parser.add_argument( 126 | "-d", dest="debug", action="store_true", help="Display debug information." 127 | ) 128 | parser.add_argument( 129 | "-f", 130 | dest="force", 131 | action="store_true", 132 | help="Silently override existing output images.", 133 | ) 134 | 135 | return parser 136 | 137 | 138 | if __name__ == "__main__": 139 | main() 140 | -------------------------------------------------------------------------------- /bin/medpy_watershed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Executes the watershed algorithm over images. 5 | requires the skimage package to be installed. 6 | 7 | Copyright (C) 2013 Oskar Maier 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program. If not, see . 21 | """ 22 | 23 | # build-in modules 24 | import argparse 25 | import logging 26 | import os 27 | 28 | # third-party modules 29 | import numpy 30 | from scipy.ndimage import label 31 | from skimage.segmentation import watershed 32 | 33 | from medpy.core import ArgumentError, Logger 34 | from medpy.filter import local_minima 35 | 36 | # own modules 37 | from medpy.io import load, save 38 | 39 | # path changes 40 | 41 | 42 | # information 43 | __author__ = "Oskar Maier" 44 | __version__ = "r0.1.2, 2013-12-11" 45 | __email__ = "oskar.maier@googlemail.com" 46 | __status__ = "Release" 47 | __description__ = """ 48 | Applies the watershed segmentation an image using the supplied 49 | parameters. 50 | Note that this version does not take the voxel-spacing into account. 51 | 52 | Copyright (C) 2013 Oskar Maier 53 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 54 | and you are welcome to redistribute it under certain conditions; see 55 | the LICENSE file or for details. 56 | """ 57 | 58 | 59 | # code 60 | def main(): 61 | # parse cmd arguments 62 | parser = getParser() 63 | parser.parse_args() 64 | args = getArguments(parser) 65 | 66 | # prepare logger 67 | logger = Logger.getInstance() 68 | if args.debug: 69 | logger.setLevel(logging.DEBUG) 70 | elif args.verbose: 71 | logger.setLevel(logging.INFO) 72 | 73 | # check if output image exists (will also be performed before saving, but as the watershed might be very time intensity, a initial check can save frustration) 74 | if not args.force: 75 | if os.path.exists(args.output): 76 | raise ArgumentError( 77 | "The output image {} already exists.".format(args.output) 78 | ) 79 | 80 | # loading images 81 | data_input, header_input = load(args.input) 82 | if args.mask: 83 | mask = load(args.mask)[0].astype(numpy.bool_) 84 | else: 85 | mask = None 86 | 87 | # extract local minima and convert to markers 88 | logger.info( 89 | "Extract local minima with minimum distance of {}...".format(args.mindist) 90 | ) 91 | lm, _ = local_minima(data_input, args.mindist) 92 | lm_indices = tuple([numpy.asarray(x) for x in lm.T]) 93 | minima_labels = numpy.zeros(data_input.shape, dtype=numpy.uint64) 94 | minima_labels[lm_indices] = 1 95 | if not None == mask: 96 | minima_labels[~mask] = 0 97 | minima_labels, _ = label(minima_labels) 98 | 99 | # apply the watershed 100 | logger.info("Watershedding...") 101 | data_output = watershed(data_input, minima_labels, mask=mask) 102 | 103 | # save file 104 | save(data_output, args.output, header_input, args.force) 105 | 106 | logger.info("Successfully terminated.") 107 | 108 | 109 | def getArguments(parser): 110 | "Provides additional validation of the arguments collected by argparse." 111 | return parser.parse_args() 112 | 113 | 114 | def getParser(): 115 | "Creates and returns the argparse parser object." 116 | parser = argparse.ArgumentParser(description=__description__) 117 | parser.add_argument("input", help="Source volume (usually a gradient image).") 118 | parser.add_argument("output", help="Target volume.") 119 | parser.add_argument( 120 | "--mindist", 121 | type=int, 122 | default=2, 123 | help="The minimum distance between local minima in voxel units.", 124 | ) 125 | parser.add_argument( 126 | "--mask", 127 | help="Optional binary mask image denoting the area over which to compute the watershed.", 128 | ) 129 | parser.add_argument( 130 | "-v", dest="verbose", action="store_true", help="Display more information." 131 | ) 132 | parser.add_argument( 133 | "-d", dest="debug", action="store_true", help="Display debug information." 134 | ) 135 | parser.add_argument( 136 | "-f", 137 | dest="force", 138 | action="store_true", 139 | help="Silently override existing output images.", 140 | ) 141 | 142 | return parser 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /bin/medpy_anisotropic_diffusion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Executes gradient anisotropic diffusion filter over an image. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | import os 26 | 27 | from medpy.core import Logger 28 | from medpy.filter.smoothing import anisotropic_diffusion 29 | 30 | # own modules 31 | from medpy.io import get_pixel_spacing, load, save 32 | 33 | # third-party modules 34 | 35 | # path changes 36 | 37 | 38 | # information 39 | __author__ = "Oskar Maier" 40 | __version__ = "r0.1.0, 2013-08-24" 41 | __email__ = "oskar.maier@googlemail.com" 42 | __status__ = "Release" 43 | __description__ = """ 44 | Executes gradient anisotropic diffusion filter over an image. 45 | This smoothing algorithm is edges preserving. 46 | To achieve the best effects, the image should be scaled to 47 | values between 0 and 1 beforehand. 48 | 49 | Note that the images voxel-spacing will be taken into account. 50 | 51 | Copyright (C) 2013 Oskar Maier 52 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 53 | and you are welcome to redistribute it under certain conditions; see 54 | the LICENSE file or for details. 55 | """ 56 | 57 | 58 | # code 59 | def main(): 60 | # parse cmd arguments 61 | parser = getParser() 62 | parser.parse_args() 63 | args = getArguments(parser) 64 | 65 | # prepare logger 66 | logger = Logger.getInstance() 67 | if args.debug: 68 | logger.setLevel(logging.DEBUG) 69 | elif args.verbose: 70 | logger.setLevel(logging.INFO) 71 | 72 | # check if output image exists (will also be performed before saving, but as the smoothing might be very time intensity, a initial check can save frustration) 73 | if not args.force: 74 | if os.path.exists(args.output): 75 | raise parser.error( 76 | "The output image {} already exists.".format(args.output) 77 | ) 78 | 79 | # loading image 80 | data_input, header_input = load(args.input) 81 | 82 | # apply the watershed 83 | logger.info( 84 | "Applying anisotropic diffusion with settings: niter={} / kappa={} / gamma={}...".format( 85 | args.iterations, args.kappa, args.gamma 86 | ) 87 | ) 88 | data_output = anisotropic_diffusion( 89 | data_input, 90 | args.iterations, 91 | args.kappa, 92 | args.gamma, 93 | get_pixel_spacing(header_input), 94 | ) 95 | 96 | # save file 97 | save(data_output, args.output, header_input, args.force) 98 | 99 | logger.info("Successfully terminated.") 100 | 101 | 102 | def getArguments(parser): 103 | "Provides additional validation of the arguments collected by argparse." 104 | return parser.parse_args() 105 | 106 | 107 | def getParser(): 108 | "Creates and returns the argparse parser object." 109 | parser = argparse.ArgumentParser(description=__description__) 110 | parser.add_argument("input", help="Source volume.") 111 | parser.add_argument("output", help="Target volume.") 112 | parser.add_argument( 113 | "-i", 114 | "--iterations", 115 | type=int, 116 | default=1, 117 | help="The number of smoothing iterations. Strong parameter.", 118 | ) 119 | parser.add_argument( 120 | "-k", 121 | "--kappa", 122 | type=int, 123 | default=50, 124 | help="The algorithms kappa parameter. The higher the more edges are smoothed over.", 125 | ) 126 | parser.add_argument( 127 | "-g", 128 | "--gamma", 129 | type=float, 130 | default=0.1, 131 | help="The algorithms gamma parameter. The higher, the stronger the plateaus between edges are smeared.", 132 | ) 133 | parser.add_argument( 134 | "-v", dest="verbose", action="store_true", help="Display more information." 135 | ) 136 | parser.add_argument( 137 | "-d", dest="debug", action="store_true", help="Display debug information." 138 | ) 139 | parser.add_argument( 140 | "-f", 141 | dest="force", 142 | action="store_true", 143 | help="Silently override existing output images.", 144 | ) 145 | 146 | return parser 147 | 148 | 149 | if __name__ == "__main__": 150 | main() 151 | -------------------------------------------------------------------------------- /bin/medpy_intersection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Extracts the intersecting parts of two volumes regarding offset and voxel-spacing. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | import os 26 | 27 | from medpy.core import Logger 28 | from medpy.filter.utilities import intersection 29 | 30 | # own modules 31 | from medpy.io import header, load, save 32 | 33 | # third-party modules 34 | 35 | # path changes 36 | 37 | 38 | # information 39 | __author__ = "Oskar Maier" 40 | __version__ = "r0.0.1, 2014-04-25" 41 | __email__ = "oskar.maier@googlemail.com" 42 | __status__ = "Development" 43 | __description__ = """ 44 | Extracts the intersecting parts of two volumes regarding offset 45 | and voxel-spacing. 46 | 47 | Copyright (C) 2013 Oskar Maier 48 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 49 | and you are welcome to redistribute it under certain conditions; see 50 | the LICENSE file or for details. 51 | """ 52 | 53 | 54 | # code 55 | def main(): 56 | # parse cmd arguments 57 | parser = getParser() 58 | parser.parse_args() 59 | args = getArguments(parser) 60 | 61 | # prepare logger 62 | logger = Logger.getInstance() 63 | if args.debug: 64 | logger.setLevel(logging.DEBUG) 65 | elif args.verbose: 66 | logger.setLevel(logging.INFO) 67 | 68 | # check if output image exists (will also be performed before saving, but as the smoothing might be very time intensity, a initial check can save frustration) 69 | if not args.force: 70 | if os.path.exists(args.output1): 71 | raise parser.error( 72 | "The output image {} already exists.".format(args.output1) 73 | ) 74 | if os.path.exists(args.output2): 75 | raise parser.error( 76 | "The output image {} already exists.".format(args.output2) 77 | ) 78 | 79 | # loading images 80 | data_input1, header_input1 = load(args.input1) 81 | data_input2, header_input2 = load(args.input2) 82 | logger.debug( 83 | "Original image sizes are {} and {}.".format( 84 | data_input1.shape, data_input2.shape 85 | ) 86 | ) 87 | 88 | # compute intersection volumes (punch) 89 | logger.info("Computing the intersection.") 90 | inters1, inters2, new_offset = intersection( 91 | data_input1, header_input1, data_input2, header_input2 92 | ) 93 | logger.debug( 94 | "Punched images are of sizes {} and {} with new offset {}.".format( 95 | inters1.shape, inters2.shape, new_offset 96 | ) 97 | ) 98 | 99 | # check if any intersection could be found at all 100 | if 0 == inters1.size: 101 | logger.warning( 102 | "No intersection could be found between the images. Please check their meta-data e.g. with medpy_info" 103 | ) 104 | 105 | # update header informations 106 | header.set_offset(header_input1, new_offset) 107 | header.set_offset(header_input2, new_offset) 108 | 109 | # save punched images 110 | save(inters1, args.output1, header_input1, args.force) 111 | save(inters2, args.output2, header_input2, args.force) 112 | 113 | logger.info("Successfully terminated.") 114 | 115 | 116 | def getArguments(parser): 117 | "Provides additional validation of the arguments collected by argparse." 118 | return parser.parse_args() 119 | 120 | 121 | def getParser(): 122 | "Creates and returns the argparse parser object." 123 | parser = argparse.ArgumentParser(description=__description__) 124 | parser.add_argument("input1", help="First source volume.") 125 | parser.add_argument("input2", help="Second source volume.") 126 | parser.add_argument("output1", help="First target volume.") 127 | parser.add_argument("output2", help="Second target volume.") 128 | parser.add_argument( 129 | "-v", dest="verbose", action="store_true", help="Display more information." 130 | ) 131 | parser.add_argument( 132 | "-d", dest="debug", action="store_true", help="Display debug information." 133 | ) 134 | parser.add_argument( 135 | "-f", 136 | dest="force", 137 | action="store_true", 138 | help="Silently override existing output images.", 139 | ) 140 | 141 | return parser 142 | 143 | 144 | if __name__ == "__main__": 145 | main() 146 | -------------------------------------------------------------------------------- /bin/medpy_split_xd_to_xminus1d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Splits a XD into a number of (X-1)D volumes. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # third-party modules 27 | import numpy 28 | 29 | from medpy.core import Logger 30 | from medpy.core.exceptions import ArgumentError 31 | 32 | # own modules 33 | from medpy.io import header, load, save 34 | 35 | # path changes 36 | 37 | 38 | # information 39 | __author__ = "Oskar Maier" 40 | __version__ = "r0.1.2, 2012-05-25" 41 | __email__ = "oskar.maier@googlemail.com" 42 | __status__ = "Release" 43 | __description__ = """ 44 | Splits a XD into a number of (X-1)D volumes. 45 | 46 | One common use case is the creation of manual markers for 4D images. 47 | This script allows to split a 4D into a number of either spatial or 48 | temporal 3D volumes, for which one then can create the markers. These 49 | can be rejoined using the join_xd_to_xplus1d.py script. 50 | 51 | Copyright (C) 2013 Oskar Maier 52 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 53 | and you are welcome to redistribute it under certain conditions; see 54 | the LICENSE file or for details. 55 | """ 56 | 57 | 58 | # code 59 | def main(): 60 | # parse cmd arguments 61 | parser = getParser() 62 | parser.parse_args() 63 | args = getArguments(parser) 64 | 65 | # prepare logger 66 | logger = Logger.getInstance() 67 | if args.debug: 68 | logger.setLevel(logging.DEBUG) 69 | elif args.verbose: 70 | logger.setLevel(logging.INFO) 71 | 72 | # load input image 73 | data_input, header_input = load(args.input) 74 | 75 | # check if the supplied dimension is valid 76 | if args.dimension >= data_input.ndim or args.dimension < 0: 77 | raise ArgumentError( 78 | "The supplied cut-dimension {} exceeds the image dimensionality of 0 to {}.".format( 79 | args.dimension, data_input.ndim - 1 80 | ) 81 | ) 82 | 83 | # prepare output file string 84 | name_output = args.output.replace("{}", "{:03d}") 85 | 86 | # compute the new the voxel spacing 87 | spacing = list(header.get_pixel_spacing(header_input)) 88 | del spacing[args.dimension] 89 | 90 | # iterate over the cut dimension 91 | slices = data_input.ndim * [slice(None)] 92 | for idx in range(data_input.shape[args.dimension]): 93 | # cut the current slice from the original image 94 | slices[args.dimension] = slice(idx, idx + 1) 95 | data_output = numpy.squeeze(data_input[tuple(slices)]) 96 | # update the header and set the voxel spacing 97 | header_input.set_voxel_spacing(spacing) 98 | # save current slice 99 | save(data_output, name_output.format(idx), header_input, args.force) 100 | 101 | logger.info("Successfully terminated.") 102 | 103 | 104 | def getArguments(parser): 105 | "Provides additional validation of the arguments collected by argparse." 106 | args = parser.parse_args() 107 | if not "{}" in args.output: 108 | raise argparse.ArgumentError( 109 | args.output, 'The output argument string must contain the sequence "{}".' 110 | ) 111 | return args 112 | 113 | 114 | def getParser(): 115 | "Creates and returns the argparse parser object." 116 | parser = argparse.ArgumentParser(description=__description__) 117 | parser.add_argument("input", help="Source volume.") 118 | parser.add_argument( 119 | "output", 120 | help='Target volumes. Has to include the sequence "{}" in the place where the volume number should be placed.', 121 | ) 122 | parser.add_argument( 123 | "dimension", 124 | type=int, 125 | help="The dimension along which to split (starting from 0).", 126 | ) 127 | parser.add_argument( 128 | "-v", dest="verbose", action="store_true", help="Display more information." 129 | ) 130 | parser.add_argument( 131 | "-d", dest="debug", action="store_true", help="Display debug information." 132 | ) 133 | parser.add_argument( 134 | "-f", 135 | dest="force", 136 | action="store_true", 137 | help="Silently override existing output images.", 138 | ) 139 | return parser 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /notebooks/scripts/medpy_watershed.py.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Applies the watershed filter to an image, i.e., separating it in nearly homogeneous regions." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 3, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "!medpy_watershed.py resources/b0.nii.gz output/watershed.nii.gz -f" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Looking at our result we get\n", 24 | "\n", 25 | "\n", 26 | "\n", 27 | "\n", 28 | "\n", 29 | "\n", 30 | "\n", 31 | "\n", 32 | "\n", 33 | "
\"Original\"Region
Original imageRegion image
\n", 34 | "That is not quite what we expected. If your resulting imageshows this gradient-like transition of gray values from one to the other site, it means that too many regions where created. In fact, this can result in that many regions, that the numeric type overflows.\n", 35 | "\n", 36 | "To fix this, let's first take a look at the script's parameters." 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 5, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stdout", 46 | "output_type": "stream", 47 | "text": [ 48 | "usage: medpy_watershed.py [-h] [--mindist MINDIST] [--mask MASK] [-v] [-d]\n", 49 | " [-f]\n", 50 | " input output\n", 51 | "\n", 52 | "Applies the watershed segmentation an image using the supplied parameters.\n", 53 | "Note that this version does not take the voxel-spacing into account. Copyright\n", 54 | "(C) 2013 Oskar Maier This program comes with ABSOLUTELY NO WARRANTY; This is\n", 55 | "free software, and you are welcome to redistribute it under certain\n", 56 | "conditions; see the LICENSE file or for\n", 57 | "details.\n", 58 | "\n", 59 | "positional arguments:\n", 60 | " input Source volume (usually a gradient image).\n", 61 | " output Target volume.\n", 62 | "\n", 63 | "options:\n", 64 | " -h, --help show this help message and exit\n", 65 | " --mindist MINDIST The minimum distance between local minima in voxel units.\n", 66 | " --mask MASK Optional binary mask image denoting the area over which\n", 67 | " to compute the watershed.\n", 68 | " -v Display more information.\n", 69 | " -d Display debug information.\n", 70 | " -f Silently override existing output images.\n" 71 | ] 72 | } 73 | ], 74 | "source": [ 75 | "!medpy_watershed.py -h" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "Note the MINDIST value. With this, the minimal distance between to local minima can be set and thus the minimal size of the regions roughly controlled. let's try it." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 6, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "!medpy_watershed.py resources/b0.nii.gz output/watershed.nii.gz --mindist 10 -f" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "\n", 99 | "\n", 100 | "\n", 101 | "\n", 102 | "\n", 103 | "\n", 104 | "\n", 105 | "\n", 106 | "\n", 107 | "\n", 108 | "\n", 109 | "
\"Original\"Region\"Region
Original imageRegion imageRegion image (colored)
" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "Better. In the colored version, the brain becomes visible." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": { 123 | "collapsed": true, 124 | "jupyter": { 125 | "outputs_hidden": true 126 | } 127 | }, 128 | "outputs": [], 129 | "source": [] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "Python 3 (ipykernel)", 135 | "language": "python", 136 | "name": "python3" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 3 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython3", 148 | "version": "3.10.12" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 4 153 | } 154 | -------------------------------------------------------------------------------- /tests/filter_/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unittest for medpy.filter.utilities 3 | 4 | @author Oskar Maier 5 | @version r0.1.0 6 | @since 2013-12-03 7 | @status Release 8 | """ 9 | 10 | # build-in modules 11 | import unittest 12 | 13 | # third-party modules 14 | import numpy 15 | 16 | # own modules 17 | from medpy.filter import pad 18 | 19 | 20 | # code 21 | class TestUtilities(unittest.TestCase): 22 | def setUp(self): 23 | pass 24 | 25 | def test_pad_bordercases(self): 26 | "Test pad for border cases in 3D" 27 | input = numpy.ones((3, 3, 3)) 28 | 29 | # no padding in all dimensions 30 | pad(input=input, size=1, mode="reflect") 31 | pad(input=input, size=1, mode="mirror") 32 | pad(input=input, size=1, mode="constant") 33 | pad(input=input, size=1, mode="nearest") 34 | pad(input=input, size=1, mode="wrap") 35 | 36 | # no padding in one dimension 37 | pad(input=input, size=(1, 2, 2), mode="reflect") 38 | pad(input=input, size=(1, 2, 2), mode="mirror") 39 | pad(input=input, size=(1, 2, 2), mode="constant") 40 | pad(input=input, size=(1, 2, 2), mode="nearest") 41 | pad(input=input, size=(1, 2, 2), mode="wrap") 42 | 43 | # same size as image 44 | pad(input=input, size=3, mode="reflect") 45 | pad(input=input, size=3, mode="mirror") 46 | pad(input=input, size=3, mode="constant") 47 | pad(input=input, size=3, mode="nearest") 48 | pad(input=input, size=3, mode="wrap") 49 | 50 | # bigger than image 51 | pad(input=input, size=4, mode="reflect") 52 | pad(input=input, size=4, mode="mirror") 53 | pad(input=input, size=4, mode="constant") 54 | pad(input=input, size=4, mode="nearest") 55 | pad(input=input, size=4, mode="wrap") 56 | 57 | def test_pad_odd(self): 58 | "Test pad for odd footprints in 2D" 59 | input = numpy.asarray([[1, 3, 4], [2, 2, 2]]) 60 | size = 3 61 | 62 | expected = numpy.asarray( 63 | [[2, 2, 2, 2, 2], [3, 1, 3, 4, 3], [2, 2, 2, 2, 2], [3, 1, 3, 4, 3]] 64 | ) 65 | result = pad(input=input, size=size, mode="mirror") 66 | self.assertTrue(numpy.all(result == expected)) 67 | 68 | expected = numpy.asarray( 69 | [[1, 1, 3, 4, 4], [1, 1, 3, 4, 4], [2, 2, 2, 2, 2], [2, 2, 2, 2, 2]] 70 | ) 71 | result = pad(input=input, size=size, mode="reflect") 72 | self.assertTrue(numpy.all(result == expected)) 73 | 74 | expected = numpy.asarray( 75 | [[2, 2, 2, 2, 2], [4, 1, 3, 4, 1], [2, 2, 2, 2, 2], [4, 1, 3, 4, 1]] 76 | ) 77 | result = pad(input=input, size=size, mode="wrap") 78 | self.assertTrue(numpy.all(result == expected)) 79 | 80 | expected = numpy.asarray( 81 | [[1, 1, 3, 4, 4], [1, 1, 3, 4, 4], [2, 2, 2, 2, 2], [2, 2, 2, 2, 2]] 82 | ) 83 | result = pad(input=input, size=size, mode="nearest") 84 | numpy.testing.assert_array_equal(result, expected) 85 | self.assertTrue(numpy.all(result == expected)) 86 | 87 | expected = numpy.asarray( 88 | [[0, 0, 0, 0, 0], [0, 1, 3, 4, 0], [0, 2, 2, 2, 0], [0, 0, 0, 0, 0]] 89 | ) 90 | result = pad(input=input, size=size, mode="constant", cval=0) 91 | self.assertTrue(numpy.all(result == expected)) 92 | 93 | expected = numpy.asarray( 94 | [[9, 9, 9, 9, 9], [9, 1, 3, 4, 9], [9, 2, 2, 2, 9], [9, 9, 9, 9, 9]] 95 | ) 96 | result = pad(input=input, size=size, mode="constant", cval=9) 97 | self.assertTrue(numpy.all(result == expected)) 98 | 99 | def test_pad_even(self): 100 | "Test pad for even footprints in 2D" 101 | input = numpy.asarray([[1, 3, 4], [2, 2, 2]]) 102 | size = (2, 3) 103 | 104 | expected = numpy.asarray([[3, 1, 3, 4, 3], [2, 2, 2, 2, 2], [3, 1, 3, 4, 3]]) 105 | result = pad(input=input, size=size, mode="mirror") 106 | self.assertTrue(numpy.all(result == expected)) 107 | 108 | expected = numpy.asarray([[1, 1, 3, 4, 4], [2, 2, 2, 2, 2], [2, 2, 2, 2, 2]]) 109 | result = pad(input=input, size=size, mode="reflect") 110 | self.assertTrue(numpy.all(result == expected)) 111 | 112 | expected = numpy.asarray([[4, 1, 3, 4, 1], [2, 2, 2, 2, 2], [4, 1, 3, 4, 1]]) 113 | result = pad(input=input, size=size, mode="wrap") 114 | self.assertTrue(numpy.all(result == expected)) 115 | 116 | expected = numpy.asarray([[1, 1, 3, 4, 4], [2, 2, 2, 2, 2], [2, 2, 2, 2, 2]]) 117 | result = pad(input=input, size=size, mode="nearest") 118 | self.assertTrue(numpy.all(result == expected)) 119 | 120 | expected = numpy.asarray([[0, 1, 3, 4, 0], [0, 2, 2, 2, 0], [0, 0, 0, 0, 0]]) 121 | result = pad(input=input, size=size, mode="constant", cval=0) 122 | self.assertTrue(numpy.all(result == expected)) 123 | 124 | expected = numpy.asarray([[9, 1, 3, 4, 9], [9, 2, 2, 2, 9], [9, 9, 9, 9, 9]]) 125 | result = pad(input=input, size=size, mode="constant", cval=9) 126 | self.assertTrue(numpy.all(result == expected)) 127 | 128 | 129 | if __name__ == "__main__": 130 | unittest.main() 131 | -------------------------------------------------------------------------------- /bin/medpy_join_masks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Joins a number of binary images into a single conjunction. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see .""" 20 | 21 | import argparse 22 | import logging 23 | 24 | # third-party modules 25 | import numpy 26 | 27 | # own modules 28 | from medpy.core import Logger 29 | from medpy.io import header, load, save 30 | 31 | # build-in modules 32 | 33 | 34 | # path changes 35 | 36 | 37 | # information 38 | __author__ = "Oskar Maier" 39 | __version__ = "r0.1.0, 2014-05-15" 40 | __email__ = "oskar.maier@googlemail.com" 41 | __status__ = "Release" 42 | __description__ = """ 43 | Joins a number of binary images into a single conjunction. 44 | 45 | The available combinatorial operations are sum, avg, max and min. 46 | In the case of max and min, the output volumes are also binary images, 47 | in the case of sum they are uint8 and in the case of avg of type float. 48 | 49 | All input images must be of same shape and voxel spacing. 50 | 51 | WARNING: Does not consider image offset. 52 | 53 | Copyright (C) 2013 Oskar Maier 54 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 55 | and you are welcome to redistribute it under certain conditions; see 56 | the LICENSE file or for details. 57 | """ 58 | 59 | 60 | # code 61 | def main(): 62 | args = getArguments(getParser()) 63 | 64 | # prepare logger 65 | logger = Logger.getInstance() 66 | if args.debug: 67 | logger.setLevel(logging.DEBUG) 68 | elif args.verbose: 69 | logger.setLevel(logging.INFO) 70 | 71 | # load input images and cast to bool 72 | images = [] 73 | for input_ in args.inputs: 74 | t = load(input_) 75 | images.append((t[0], t[1])) 76 | 77 | # check if their shapes and voxel spacings are all equal 78 | s0 = images[0][0].shape 79 | if not numpy.all([i[0].shape == s0 for i in images[1:]]): 80 | raise argparse.ArgumentError( 81 | args.input, 82 | "At least one input image is of a different shape than the others.", 83 | ) 84 | vs0 = header.get_pixel_spacing(images[0][1]) 85 | if not numpy.all([header.get_pixel_spacing(i[1]) == vs0 for i in images[1:]]): 86 | raise argparse.ArgumentError( 87 | args.input, 88 | "At least one input image has a different voxel spacing than the others.", 89 | ) 90 | 91 | # execute operation 92 | logger.debug( 93 | "Executing operation {} over {} images.".format(args.operation, len(images)) 94 | ) 95 | if "max" == args.operation: 96 | out = numpy.maximum.reduce([t[0] for t in images]) 97 | elif "min" == args.operation: 98 | out = numpy.minimum.reduce([t[0] for t in images]) 99 | elif "sum" == args.operation: 100 | out = numpy.sum([t[0] for t in images], 0).astype(numpy.uint8) 101 | else: # avg 102 | out = numpy.average([t[0] for t in images], 0).astype(numpy.float32) 103 | 104 | # save output 105 | save(out, args.output, images[0][1], args.force) 106 | 107 | logger.info("Successfully terminated.") 108 | 109 | 110 | def getArguments(parser): 111 | "Provides additional validation of the arguments collected by argparse." 112 | return parser.parse_args() 113 | 114 | 115 | def getParser(): 116 | "Creates and returns the argparse parser object." 117 | parser = argparse.ArgumentParser( 118 | formatter_class=argparse.RawDescriptionHelpFormatter, 119 | description=__description__, 120 | ) 121 | parser.add_argument("output", help="Target volume.") 122 | parser.add_argument("inputs", nargs="+", help="Source volume(s).") 123 | parser.add_argument( 124 | "-o", 125 | "--operation", 126 | dest="operation", 127 | choices=["sum", "avg", "max", "min"], 128 | default="avg", 129 | help="Combinatorial operation to conduct.", 130 | ) 131 | parser.add_argument( 132 | "-v", dest="verbose", action="store_true", help="Display more information." 133 | ) 134 | parser.add_argument( 135 | "-d", dest="debug", action="store_true", help="Display debug information." 136 | ) 137 | parser.add_argument( 138 | "-f", 139 | dest="force", 140 | action="store_true", 141 | help="Silently override existing output images.", 142 | ) 143 | return parser 144 | 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /medpy/core/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.1 18 | # since 2011-12-12 19 | # status Release 20 | 21 | import logging 22 | 23 | # build-in module 24 | import sys 25 | from logging import Logger as NativeLogger 26 | 27 | # third-party modules 28 | 29 | # own modules 30 | 31 | # constants 32 | 33 | 34 | # code 35 | class Logger(NativeLogger): 36 | r"""Logger to be used by all applications and classes. 37 | 38 | Notes 39 | ----- 40 | Singleton class i.e. setting the log level changes the output globally. 41 | 42 | Examples 43 | -------- 44 | Initializing the logger 45 | 46 | >>> from medpy.core import Logger 47 | >>> logger = Logger.getInstance() 48 | 49 | Error messages are passed to stdout 50 | 51 | >>> logger.error('error message') 52 | 15.09.2014 12:40:25 [ERROR ] error message 53 | >>> logger.error('critical message') 54 | 15.09.2014 12:40:42 [CRITICAL] critical message 55 | 56 | But debug and info messages are suppressed 57 | 58 | >>> logger.info('info message') 59 | >>> logger.debug('debug message') 60 | 61 | Unless the log level is set accordingly 62 | 63 | >>> import logging 64 | >>> logger.setLevel(logging.DEBUG) 65 | 66 | >>> logger.info('info message') 67 | 15.09.2014 12:43:06 [INFO ] info message (in .:1) 68 | >>> logger.debug('debug message') 69 | 15.09.2014 12:42:50 [DEBUG ] debug message (in .:1) 70 | 71 | """ 72 | 73 | class LoggerHelper(object): 74 | r"""A helper class which performs the actual initialization.""" 75 | 76 | def __call__(self, *args, **kw): 77 | # If an instance of TestSingleton does not exist, 78 | # create one and assign it to TestSingleton.instance. 79 | if Logger._instance is None: 80 | Logger._instance = Logger() 81 | # Return TestSingleton.instance, which should contain 82 | # a reference to the only instance of TestSingleton 83 | # in the system. 84 | return Logger._instance 85 | 86 | r"""Member variable initiating and returning the instance of the class.""" 87 | getInstance = LoggerHelper() 88 | r"""The member variable holding the actual instance of the class.""" 89 | _instance = None 90 | r"""Holds the loggers handler for format changes.""" 91 | _handler = None 92 | 93 | def __init__(self, name="MedPyLogger", level=0): 94 | # To guarantee that no one created more than one instance of Logger: 95 | if Logger._instance is not None: 96 | raise RuntimeError("Only one instance of Logger is allowed!") 97 | 98 | # initialize parent 99 | NativeLogger.__init__(self, name, level) 100 | 101 | # set attributes 102 | self.setHandler(logging.StreamHandler(sys.stdout)) 103 | self.setLevel(logging.WARNING) 104 | 105 | def setHandler(self, hdlr): 106 | r"""Replace the current handler with a new one. 107 | 108 | Parameters 109 | ---------- 110 | hdlr : logging.Handler 111 | A subclass of Handler that should used to handle the logging output. 112 | 113 | Notes 114 | ----- 115 | If none should be replaces, but just one added, use the parent classes 116 | addHandler() method. 117 | """ 118 | if self._handler is not None: 119 | self.removeHandler(self._handler) 120 | self._handler = hdlr 121 | self.addHandler(self._handler) 122 | 123 | def setLevel(self, level): 124 | r"""Overrides the parent method to adapt the formatting string to the level. 125 | 126 | Parameters 127 | ---------- 128 | level : int 129 | The new log level to set. See the logging levels in the logging module for details. 130 | 131 | Examples 132 | -------- 133 | >>> import logging 134 | >>> Logger.setLevel(logging.DEBUG) 135 | """ 136 | if logging.DEBUG >= level: 137 | formatter = logging.Formatter( 138 | "%(asctime)s [%(levelname)-8s] %(message)s (in %(module)s.%(funcName)s:%(lineno)s)", 139 | "%d.%m.%Y %H:%M:%S", 140 | ) 141 | self._handler.setFormatter(formatter) 142 | else: 143 | formatter = logging.Formatter( 144 | "%(asctime)s [%(levelname)-8s] %(message)s", "%d.%m.%Y %H:%M:%S" 145 | ) 146 | self._handler.setFormatter(formatter) 147 | 148 | NativeLogger.setLevel(self, level) 149 | -------------------------------------------------------------------------------- /bin/medpy_shrink_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Shrink an image by skipping every the slides between every x slides. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # third-party modules 27 | import numpy 28 | 29 | # own modules 30 | from medpy.core import Logger 31 | from medpy.io import header, load, save 32 | 33 | # path changes 34 | 35 | 36 | # information 37 | __author__ = "Oskar Maier" 38 | __version__ = "d0.2.0, 2012-06-13" 39 | __email__ = "oskar.maier@googlemail.com" 40 | __status__ = "Development" 41 | __description__ = """ 42 | Shrinks an image by discarding slices. Reverse operation of zoom_image.py. 43 | Reduces the image by keeping one slice, then discarding "discard" slices, then 44 | keeping the next and so on. 45 | 46 | Copyright (C) 2013 Oskar Maier 47 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 48 | and you are welcome to redistribute it under certain conditions; see 49 | the LICENSE file or for details. 50 | """ 51 | 52 | 53 | # code 54 | def main(): 55 | args = getArguments(getParser()) 56 | 57 | # prepare logger 58 | logger = Logger.getInstance() 59 | if args.debug: 60 | logger.setLevel(logging.DEBUG) 61 | elif args.verbose: 62 | logger.setLevel(logging.INFO) 63 | 64 | # load input data 65 | input_data, input_header = load(args.input) 66 | 67 | logger.debug("Old shape = {}.".format(input_data.shape)) 68 | 69 | # compute new shape 70 | new_shape = list(input_data.shape) 71 | new_shape[args.dimension] = 1 + (new_shape[args.dimension] - 1) / (args.discard + 1) 72 | 73 | # prepare output image 74 | output_data = numpy.zeros(new_shape, dtype=input_data.dtype) 75 | 76 | # prepare slicers 77 | slicer_in = [slice(None)] * input_data.ndim 78 | slicer_out = [slice(None)] * input_data.ndim 79 | 80 | # prepare skip-counter and output image slice counter 81 | skipc = 0 82 | slicec = 0 83 | 84 | logger.debug("Shrinking from {} to {}...".format(input_data.shape, new_shape)) 85 | for idx in range(input_data.shape[args.dimension]): 86 | if 0 == skipc: 87 | # transfer slice 88 | slicer_in[args.dimension] = slice(idx, idx + 1) 89 | slicer_out[args.dimension] = slice(slicec, slicec + 1) 90 | output_data[tuple(slicer_out)] = input_data[tuple(slicer_in)] 91 | 92 | # resert resp. increase counter 93 | skipc = args.discard 94 | slicec += 1 95 | 96 | else: # skip slice 97 | # decrease skip counter 98 | skipc -= 1 99 | 100 | # set new pixel spacing 101 | new_spacing = list(header.get_pixel_spacing(input_header)) 102 | new_spacing[args.dimension] = new_spacing[args.dimension] * float(args.discard + 1) 103 | logger.debug( 104 | "Setting pixel spacing from {} to {}....".format( 105 | header.get_pixel_spacing(input_header), new_spacing 106 | ) 107 | ) 108 | header.set_pixel_spacing(input_header, tuple(new_spacing)) 109 | 110 | save(output_data, args.output, input_header, args.force) 111 | 112 | 113 | def getArguments(parser): 114 | "Provides additional validation of the arguments collected by argparse." 115 | return parser.parse_args() 116 | 117 | 118 | def getParser(): 119 | "Creates and returns the argparse parser object." 120 | parser = argparse.ArgumentParser( 121 | description=__description__, formatter_class=argparse.RawTextHelpFormatter 122 | ) 123 | parser.add_argument("input", help="Source volume.") 124 | parser.add_argument("output", help="Target volume.") 125 | parser.add_argument( 126 | "dimension", type=int, help="The dimension along which to discard the slices." 127 | ) 128 | parser.add_argument( 129 | "discard", 130 | type=int, 131 | help="How many slices to discard between each two slices which are kept.", 132 | ) 133 | parser.add_argument( 134 | "-v", dest="verbose", action="store_true", help="Display more information." 135 | ) 136 | parser.add_argument( 137 | "-d", dest="debug", action="store_true", help="Display debug information." 138 | ) 139 | parser.add_argument( 140 | "-f", 141 | dest="force", 142 | action="store_true", 143 | help="Silently override existing output images.", 144 | ) 145 | return parser 146 | 147 | 148 | if __name__ == "__main__": 149 | main() 150 | -------------------------------------------------------------------------------- /medpy/filter/binary.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Oskar Maier 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | # 16 | # author Oskar Maier 17 | # version r0.2.1 18 | # since 2013-10-14 19 | # status Release 20 | 21 | # build-in modules 22 | from operator import eq, ge, gt, le, lt, ne 23 | 24 | # third-party modules 25 | import numpy 26 | from scipy.ndimage import label 27 | 28 | # own modules 29 | 30 | 31 | # code 32 | def size_threshold(img, thr, comp="lt", structure=None): 33 | r""" 34 | Removes binary objects from an image identified by a size threshold. 35 | 36 | The unconnected binary objects in an image are identified and all removed 37 | whose size compares (e.g. less-than) to a supplied threshold value. 38 | 39 | The threshold ``thr`` can be any positive integer value. The comparison operator 40 | can be one of lt, le, gt, ge, ne or eq. The operators used are the functions of 41 | the same name supplied by the `operator` module of python. 42 | 43 | Parameters 44 | ---------- 45 | img : array_like 46 | An array containing connected objects. Will be cast to type `bool`. 47 | thr : int 48 | Integer defining the threshold size of the binary objects to remove. 49 | comp : {'lt', 'le', 'gt', 'ge', 'ne', 'eq'} 50 | The type of comparison to perform. Use e.g. 'lt' for less-than. 51 | structure : array of ints, optional 52 | A structuring element that defines feature connections. 53 | ``structure`` must be symmetric. If no structuring element is provided, 54 | one is automatically generated with a squared connectivity equal to 55 | one. That is, for a 2-D ``input`` array, the default structuring element 56 | is:: 57 | 58 | [[0,1,0], 59 | [1,1,1], 60 | [0,1,0]] 61 | 62 | Returns 63 | ------- 64 | binary_image : ndarray 65 | The supplied binary image with all objects removed that positively compare 66 | to the threshold ``thr`` using the comparison operator defined with ``comp``. 67 | 68 | Notes 69 | ----- 70 | If your voxel size is no isotrop i.e. of side-length 1 for all dimensions, simply 71 | divide the supplied threshold through the real voxel size. 72 | """ 73 | 74 | operators = {"lt": lt, "le": le, "gt": gt, "ge": ge, "eq": eq, "ne": ne} 75 | 76 | img = numpy.asarray(img).astype(numpy.bool_) 77 | if comp not in operators: 78 | raise ValueError("comp must be one of {}".format(list(operators.keys()))) 79 | comp = operators[comp] 80 | 81 | labeled_array, num_features = label(img, structure) 82 | for oidx in range(1, num_features + 1): 83 | omask = labeled_array == oidx 84 | if comp(numpy.count_nonzero(omask), thr): 85 | img[omask] = False 86 | 87 | return img 88 | 89 | 90 | def largest_connected_component(img, structure=None): 91 | r""" 92 | Select the largest connected binary component in an image. 93 | 94 | Treats all zero values in the input image as background and all others as foreground. 95 | The return value is an binary array of equal dimensions as the input array with TRUE 96 | values where the largest connected component is situated. 97 | 98 | Parameters 99 | ---------- 100 | img : array_like 101 | An array containing connected objects. Will be cast to type `bool`. 102 | structure : array_like 103 | A structuring element that defines the connectivity. Structure must be symmetric. 104 | If no structuring element is provided, one is automatically generated with a 105 | squared connectivity equal to one. 106 | 107 | Returns 108 | ------- 109 | binary_image : ndarray 110 | The supplied binary image with only the largest connected component remaining. 111 | """ 112 | labeled_array, num_features = label(img, structure) 113 | component_sizes = [ 114 | numpy.count_nonzero(labeled_array == label_idx) 115 | for label_idx in range(1, num_features + 1) 116 | ] 117 | largest_component_idx = numpy.argmax(component_sizes) + 1 118 | 119 | out = numpy.zeros(img.shape, numpy.bool_) 120 | out[labeled_array == largest_component_idx] = True 121 | return out 122 | 123 | 124 | def bounding_box(img): 125 | r""" 126 | Return the bounding box incorporating all non-zero values in the image. 127 | 128 | Parameters 129 | ---------- 130 | img : array_like 131 | An array containing non-zero objects. 132 | 133 | Returns 134 | ------- 135 | bbox : a list of slicer objects defining the bounding box 136 | """ 137 | locations = numpy.argwhere(img) 138 | mins = locations.min(0) 139 | maxs = locations.max(0) + 1 140 | return tuple([slice(x, y) for x, y in zip(mins, maxs)]) 141 | -------------------------------------------------------------------------------- /bin/medpy_resample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Resamples an image according to a supplied voxel spacing. 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | import argparse 23 | import logging 24 | 25 | # build-in modules 26 | import os 27 | 28 | # own modules 29 | from medpy.core import Logger 30 | from medpy.io import header, load, save 31 | from medpy.utilities import argparseu 32 | 33 | # third-party modules 34 | 35 | 36 | # path changes 37 | 38 | 39 | # information 40 | __author__ = "Oskar Maier" 41 | __version__ = "r0.1.1, 2013-07-08" 42 | __email__ = "oskar.maier@googlemail.com" 43 | __status__ = "Release" 44 | __description__ = """ 45 | Resamples an image according to a supplied voxel spacing. 46 | 47 | BSpline is used for interpolation. A order between 1 and 5 can be selected. 48 | 49 | Note that the pixel data type of the input image is respected, i.e. a integer 50 | input image leads to an integer output image etc. 51 | 52 | Copyright (C) 2013 Oskar Maier 53 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 54 | and you are welcome to redistribute it under certain conditions; see 55 | the LICENSE file or for details. 56 | """ 57 | 58 | 59 | # code 60 | def main(): 61 | parser = getParser() 62 | args = getArguments(parser) 63 | 64 | # prepare logger 65 | logger = Logger.getInstance() 66 | if args.debug: 67 | logger.setLevel(logging.DEBUG) 68 | elif args.verbose: 69 | logger.setLevel(logging.INFO) 70 | 71 | # loading input images 72 | img, hdr = load(args.input) 73 | 74 | # check spacing values 75 | if not len(args.spacing) == img.ndim: 76 | parser.error( 77 | "The image has {} dimensions, but {} spacing parameters have been supplied.".format( 78 | img.ndim, len(args.spacing) 79 | ) 80 | ) 81 | 82 | # check if output image exists 83 | if not args.force: 84 | if os.path.exists(args.output): 85 | parser.error("The output image {} already exists.".format(args.output)) 86 | 87 | logger.debug("target voxel spacing: {}".format(args.spacing)) 88 | 89 | # compute zoom values 90 | zoom_factors = [ 91 | old / float(new) 92 | for new, old in zip(args.spacing, header.get_pixel_spacing(hdr)) 93 | ] 94 | logger.debug("zoom-factors: {}".format(zoom_factors)) 95 | 96 | # zoom image 97 | img = scipy.ndimage.zoom(img, zoom_factors, order=args.order) 98 | logger.debug("new image shape: {}".format(img.shape)) 99 | 100 | # set new voxel spacing 101 | header.set_pixel_spacing(hdr, args.spacing) 102 | 103 | # saving the resulting image 104 | save(img, args.output, hdr, args.force) 105 | 106 | 107 | def getArguments(parser): 108 | "Provides additional validation of the arguments collected by argparse." 109 | args = parser.parse_args() 110 | if args.order < 0 or args.order > 5: 111 | parser.error("The order has to be a number between 0 and 5.") 112 | return args 113 | 114 | 115 | def getParser(): 116 | "Creates and returns the argparse parser object." 117 | parser = argparse.ArgumentParser(description=__description__) 118 | parser.add_argument("input", help="the input image") 119 | parser.add_argument("output", help="the output image") 120 | parser.add_argument( 121 | "spacing", 122 | type=argparseu.sequenceOfFloatsGt, 123 | help="the desired voxel spacing in colon-separated values, e.g. 1.2,1.2,5.0", 124 | ) 125 | parser.add_argument( 126 | "-o", 127 | "--order", 128 | type=int, 129 | default=2, 130 | dest="order", 131 | help="the bspline order, default is 2; means nearest neighbours; see also medpy_binary_resampling.py", 132 | ) 133 | 134 | # group = parser.add_mutually_exclusive_group(required=False) 135 | # group.add_argument('--binary', action='store_true', dest='binary', help='enforce binary output image') 136 | # group.add_argument('--float', action='store_true', dest='float', help='enforce floating point output image') 137 | 138 | parser.add_argument( 139 | "-v", "--verbose", dest="verbose", action="store_true", help="verbose output" 140 | ) 141 | parser.add_argument( 142 | "-d", dest="debug", action="store_true", help="Display debug information." 143 | ) 144 | parser.add_argument( 145 | "-f", 146 | "--force", 147 | dest="force", 148 | action="store_true", 149 | help="overwrite existing files", 150 | ) 151 | return parser 152 | 153 | 154 | if __name__ == "__main__": 155 | main() 156 | -------------------------------------------------------------------------------- /bin/medpy_morphology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Executes opening and closing morphological operations over the input image(s). 5 | 6 | Copyright (C) 2013 Oskar Maier 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | """ 21 | 22 | # build-in modules 23 | import argparse 24 | import logging 25 | 26 | # third-party modules 27 | import scipy.ndimage 28 | 29 | # own modules 30 | from medpy.core import Logger 31 | from medpy.io import load, save 32 | 33 | # path changes 34 | 35 | 36 | # information 37 | __author__ = "Oskar Maier" 38 | __version__ = "r2.0.0, 2011-12-13" 39 | __email__ = "oskar.maier@googlemail.com" 40 | __status__ = "Release" 41 | __description__ = """ 42 | Executes opening and closing morphological operations over the input image(s). 43 | 44 | Copyright (C) 2013 Oskar Maier 45 | This program comes with ABSOLUTELY NO WARRANTY; This is free software, 46 | and you are welcome to redistribute it under certain conditions; see 47 | the LICENSE file or for details. 48 | """ 49 | 50 | 51 | # code 52 | def main(): 53 | # parse cmd arguments 54 | parser = getParser() 55 | parser.parse_args() 56 | args = getArguments(parser) 57 | 58 | # prepare logger 59 | logger = Logger.getInstance() 60 | if args.debug: 61 | logger.setLevel(logging.DEBUG) 62 | elif args.verbose: 63 | logger.setLevel(logging.INFO) 64 | 65 | # load input image 66 | image_smoothed_data, image_header = load(args.input) 67 | 68 | # perform opening resp. closing 69 | # in 3D case: size 1 = 6-connectedness, 2 = 12-connectedness, 3 = 18-connectedness, etc. 70 | footprint = scipy.ndimage.generate_binary_structure( 71 | image_smoothed_data.ndim, args.size 72 | ) 73 | if "erosion" == args.type: 74 | logger.info("Applying erosion...") 75 | image_smoothed_data = scipy.ndimage.binary_erosion( 76 | image_smoothed_data, footprint, iterations=args.iterations 77 | ) 78 | elif "dilation" == args.type: 79 | logger.info("Applying dilation...") 80 | image_smoothed_data = scipy.ndimage.binary_dilation( 81 | image_smoothed_data, footprint, iterations=args.iterations 82 | ) 83 | elif "opening" == args.type: 84 | logger.info("Applying opening...") 85 | image_smoothed_data = scipy.ndimage.binary_opening( 86 | image_smoothed_data, footprint, iterations=args.iterations 87 | ) 88 | else: # closing 89 | logger.info("Applying closing...") 90 | image_smoothed_data = scipy.ndimage.binary_closing( 91 | image_smoothed_data, footprint, iterations=args.iterations 92 | ) 93 | 94 | # apply additional hole closing step 95 | logger.info("Closing holes...") 96 | image_smoothed_data = scipy.ndimage.binary_fill_holes(image_smoothed_data) 97 | 98 | # save resulting mas 99 | save(image_smoothed_data, args.output, image_header, args.force) 100 | 101 | logger.info("Successfully terminated.") 102 | 103 | 104 | def getArguments(parser): 105 | "Provides additional validation of the arguments collected by argparse." 106 | return parser.parse_args() 107 | 108 | 109 | def getParser(): 110 | "Creates and returns the argparse parser object." 111 | parser = argparse.ArgumentParser(description=__description__) 112 | parser.add_argument("input", help="Source volume.") 113 | parser.add_argument("output", help="Target volume.") 114 | parser.add_argument( 115 | "-t", 116 | "--type", 117 | dest="type", 118 | choices=["erosion", "dilation", "opening", "closing"], 119 | default="erosion", 120 | help="The type of the morphological operation.", 121 | ) 122 | parser.add_argument( 123 | "-i", 124 | "--iterations", 125 | dest="iterations", 126 | default=0, 127 | type=int, 128 | help="The number of iteration to execute. Supply a value of 1 or higher to restrict the effect of the morphological operation. Otherwise it is applied until saturation.", 129 | ) 130 | parser.add_argument( 131 | "-s", 132 | "--size", 133 | dest="size", 134 | default=3, 135 | type=int, 136 | help="Size of the closing element (>=1). The higher this value, the bigger the wholes that get closed (closing) resp. unconnected elements that are removed (opening). In the 3D case, 1 equals a 6-connectedness, 2 a 12-connectedness, 3 a 18-connectedness, etc.", 137 | ) 138 | parser.add_argument( 139 | "-v", dest="verbose", action="store_true", help="Display more information." 140 | ) 141 | parser.add_argument( 142 | "-d", dest="debug", action="store_true", help="Display debug information." 143 | ) 144 | parser.add_argument( 145 | "-f", 146 | dest="force", 147 | action="store_true", 148 | help="Silently override existing output images.", 149 | ) 150 | 151 | return parser 152 | 153 | 154 | if __name__ == "__main__": 155 | main() 156 | --------------------------------------------------------------------------------