├── 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 | [](https://pypi.python.org/pypi/MedPy/)
2 | [](https://anaconda.org/conda-forge/medpy)
3 | [](https://pypi.python.org/pypi/MedPy/)
4 | [](https://www.gnu.org/licenses/gpl-3.0)
5 | [](https://pepy.tech/project/medpy)
6 | [](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 | "| Original image | \n",
31 | "Region image | \n",
32 | "
\n",
33 | "
\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 | "| Original image | \n",
106 | "Region image | \n",
107 | "Region image (colored) | \n",
108 | "
\n",
109 | "
"
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 |
--------------------------------------------------------------------------------