├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-sdist-publish.yml │ ├── build.yml │ └── draft-pdf.yml ├── .gitignore ├── FastGeodis ├── __init__.py ├── _version.py ├── common.h ├── fastgeodis.cpp ├── fastgeodis.h ├── fastgeodis_cpu.cpp ├── fastgeodis_cuda.cu ├── geodis_fastmarch.cpp ├── geodis_pixelqueue.cpp └── geodis_toivanen.cpp ├── LICENSE ├── MANIFEST.in ├── README.md ├── data ├── ISIC_546.jpg ├── brain.png ├── brain_seg.png ├── brain_seg_noisy.png ├── img2d.png └── img3d.nii.gz ├── dependency └── fmm │ ├── .github │ └── workflows │ │ ├── cmake.yml │ │ └── cpplint.yml │ ├── .gitignore │ ├── CMakeLists.txt │ ├── CPPLINT.cfg │ ├── README.md │ ├── bindings │ └── python │ │ ├── CMakeLists.txt │ │ └── py-fast-marching-method.cpp │ ├── examples │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── minimal_example.cpp │ └── python │ │ └── py-fast-marching-minimal-example.py │ ├── img │ ├── fmm_readme.pdf │ ├── fmm_readme_concept.png │ ├── fmm_readme_dilation_bands.png │ ├── fmm_readme_eikonal_solvers.png │ ├── fmm_readme_input_values.png │ ├── fmm_readme_inside_outside.png │ └── fmm_readme_point_source_error.png │ ├── include │ └── thinks │ │ └── fast_marching_method │ │ └── fast_marching_method.hpp │ └── tests │ ├── CMakeLists.txt │ ├── eikonal_solvers_test.cpp │ ├── main.cpp │ ├── py-bindings-test.py │ ├── signed_arrival_time_test.cpp │ └── util.hpp ├── docs ├── .nojekyll ├── Makefile ├── make.bat ├── requirements-doc.txt └── source │ ├── acknowledgement.rst │ ├── api_docs.rst │ ├── citing.rst │ ├── conf.py │ ├── contributing.rst │ ├── experiment2d.csv │ ├── experiment3d.csv │ ├── getting_started.rst │ ├── index.rst │ ├── license.rst │ ├── methodology.rst │ ├── refs.bib │ └── usage_examples.rst ├── figures ├── 3d_axial.png ├── 3d_coronal.png ├── FastGeodis2D.png ├── FastGeodis3D.png ├── animation_pass2d.gif ├── experiment_2d.json ├── experiment_2d.png ├── experiment_2d_toivanen.png ├── experiment_3d.json ├── experiment_3d.png ├── experiment_3d_toivanen.png ├── fast_marching_compare_2d.png ├── fast_marching_compare_2d_jointhist.png ├── fast_marching_compare_3d.png ├── fast_marching_compare_3d_jointhist.png └── rasterscan_toivanen.png ├── paper ├── FastGeodis.png ├── paper.bib └── paper.md ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── samples ├── demo2d.ipynb ├── demo2d.py ├── demo2d_signed.ipynb ├── demo2d_signed.py ├── demo3d.ipynb ├── demo3d.py ├── demo3d_signed.ipynb ├── demo3d_signed.py ├── demoGSF2d_SmoothingSegExample.ipynb ├── simpledemo2d.ipynb ├── simpledemo2d.py ├── simpledemo2d_profile.py ├── simpledemo2d_signed.ipynb ├── simpledemo2d_signed.py ├── simpledemo3d.ipynb ├── simpledemo3d.py ├── simpledemo3d_profile.py ├── simpledemo3d_signed.ipynb ├── simpledemo3d_signed.py ├── test_speed_benchmark_geodistk.py └── test_speed_benchmark_toivanen.py ├── setup.py └── tests ├── __init__.py ├── test_fastgeodis.py ├── test_fastmarch.py ├── test_pixelqueue.py ├── test_toivanen.py └── utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/build-sdist-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload wheels 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | test_py: 7 | description: 'Publish package to test PY Repository' 8 | required: true 9 | default: 'false' 10 | main_py: 11 | description: 'Publish package to main PY Repository' 12 | required: true 13 | default: 'false' 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [windows-2019, macos-12, ubuntu-20.04] 21 | python-version: ['3.8', '3.9', '3.10', '3.11'] 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | pip install wheel setuptools ninja numpy 32 | pip install -r requirements-dev.txt 33 | 34 | - name: Build wheel 35 | run: python setup.py bdist_wheel 36 | 37 | - name: Install software 38 | run: pip install --find-links=${{github.workspace}}/dist/ FastGeodis 39 | 40 | - name: Run unittest 41 | run: python -m unittest 42 | 43 | build_sdist: 44 | runs-on: ubuntu-20.04 45 | steps: 46 | - uses: actions/checkout@v1 47 | - name: Set up Python 3.8 48 | uses: actions/setup-python@v2 49 | with: 50 | python-version: 3.8 51 | 52 | - name: Install dependencies 53 | run: | 54 | pip install wheel setuptools numpy 55 | pip install -r requirements-dev.txt 56 | 57 | - name: Build source dist 58 | run: python setup.py sdist 59 | 60 | - name: Upload Python Dist 61 | uses: actions/upload-artifact@v2 62 | with: 63 | name: dist 64 | path: dist/ 65 | if-no-files-found: error 66 | 67 | - name: Install software 68 | run: pip install ${{github.workspace}}/dist/FastGeodis*.tar.gz 69 | 70 | - name: Run unittest 71 | run: python -m unittest 72 | 73 | publish_pypi: 74 | runs-on: ubuntu-20.04 75 | needs: 76 | - build 77 | - build_sdist 78 | steps: 79 | - name: Download all the dists 80 | uses: actions/download-artifact@v2 81 | with: 82 | name: dist 83 | path: dist/ 84 | 85 | - name: Publish distribution to Test PyPI 86 | if: ${{ github.event.inputs.test_py == 'true' }} 87 | uses: pypa/gh-action-pypi-publish@v1.5.0 88 | with: 89 | user: __token__ 90 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 91 | repository_url: https://test.pypi.org/legacy/ 92 | 93 | - name: Publish distribution to PyPI 94 | if: ${{ github.event.inputs.main_py == 'true' }} 95 | uses: pypa/gh-action-pypi-publish@v1.5.0 96 | with: 97 | user: __token__ 98 | password: ${{ secrets.PYPI_API_TOKEN }} 99 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | # simple build workflow for sanity check wheel binary, source distribution compilation and installation on various platforms with p3.8 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | workflow_dispatch: 10 | inputs: 11 | save_artifacts: 12 | description: 'Save artifacts from build' 13 | required: true 14 | default: 'false' 15 | 16 | jobs: 17 | build_whl: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [windows-2019, macos-12, ubuntu-20.04] 22 | python-version: [3.8] 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | pip install wheel setuptools ninja numpy 33 | pip install -r requirements-dev.txt 34 | 35 | - name: Build wheel 36 | run: python setup.py bdist_wheel 37 | 38 | - name: Install software 39 | run: | 40 | pip install numpy # install to get rid of warning 41 | pip install --no-index --find-links=${{github.workspace}}/dist/ FastGeodis 42 | - name: Run unittests 43 | run: | 44 | python -m unittest 45 | 46 | - name: Upload Python Dist 47 | if: ${{ github.event.inputs.save_artifacts == 'true' }} 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: dist 51 | path: dist/ 52 | if-no-files-found: error 53 | 54 | build_sdist: 55 | runs-on: ubuntu-20.04 56 | steps: 57 | - uses: actions/checkout@v1 58 | - name: Set up Python 3.8 59 | uses: actions/setup-python@v2 60 | with: 61 | python-version: 3.8 62 | 63 | - name: Install dependencies 64 | run: | 65 | pip install wheel setuptools numpy 66 | pip install -r requirements-dev.txt 67 | 68 | - name: Build source dist 69 | run: python setup.py sdist 70 | 71 | - name: Install software 72 | run: | 73 | pip install numpy # install to get rid of warning 74 | pip install ${{github.workspace}}/dist/FastGeodis*.tar.gz 75 | - name: Run unittests 76 | run: | 77 | python -m unittest 78 | 79 | - name: Upload Python Dist 80 | if: ${{ github.event.inputs.save_artifacts == 'true' }} 81 | uses: actions/upload-artifact@v2 82 | with: 83 | name: dist 84 | path: dist/ 85 | if-no-files-found: error 86 | -------------------------------------------------------------------------------- /.github/workflows/draft-pdf.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | jobs: 5 | # paper: 6 | # runs-on: ubuntu-latest 7 | # name: Paper Draft 8 | # steps: 9 | # - name: Checkout 10 | # uses: actions/checkout@v2 11 | # - name: Build draft PDF 12 | # uses: openjournals/openjournals-draft-action@master 13 | # with: 14 | # journal: joss 15 | # # This should be the path to the paper within your repo. 16 | # paper-path: paper/paper.md 17 | # - name: Upload 18 | # uses: actions/upload-artifact@v1 19 | # with: 20 | # name: paper 21 | # # This is the output path where Pandoc will write the compiled 22 | # # PDF. Note, this should be the same directory as the input 23 | # # paper.md 24 | # path: paper/paper.pdf 25 | tests: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: TeX and PDF 30 | uses: docker://openjournals/paperdraft:latest 31 | with: 32 | args: '-k paper/paper.md' 33 | env: 34 | GIT_SHA: $GITHUB_SHA 35 | JOURNAL: joss 36 | - name: Upload 37 | uses: actions/upload-artifact@v1 38 | with: 39 | name: paper 40 | path: paper/ 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | .vscode/ -------------------------------------------------------------------------------- /FastGeodis/_version.py: -------------------------------------------------------------------------------- 1 | # following guidance from: https://stackoverflow.com/a/7071358 2 | __version__ = "1.0.5" -------------------------------------------------------------------------------- /FastGeodis/common.h: -------------------------------------------------------------------------------- 1 | // BSD 3-Clause License 2 | 3 | // Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | // All rights reserved. 5 | 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | 9 | // 1. Redistributions of source code must retain the above copyright notice, this 10 | // list of conditions and the following disclaimer. 11 | 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | 16 | // 3. Neither the name of the copyright holder nor the names of its 17 | // contributors may be used to endorse or promote products derived from 18 | // this software without specific prior written permission. 19 | 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | #pragma once 32 | #include 33 | #include 34 | 35 | void print_shape(const torch::Tensor &data) 36 | { 37 | auto num_dims = data.dim(); 38 | std::cout << "Shape: ("; 39 | for (int dim = 0; dim < num_dims; dim++) 40 | { 41 | std::cout << data.size(dim); 42 | if (dim != num_dims - 1) 43 | { 44 | std::cout << ", "; 45 | } 46 | else 47 | { 48 | std::cout << ")" << std::endl; 49 | } 50 | } 51 | } 52 | 53 | void check_spatial_shape_match(const torch::Tensor &in1, const torch::Tensor &in2, const int &dims) 54 | { 55 | if (in1.dim() != in2.dim()) 56 | { 57 | throw std::invalid_argument("dimensions of input tensors do not match " + \ 58 | std::to_string(in1.dim() - 2) + " vs " + std::to_string(in2.dim() - 2)); 59 | } 60 | for (int i = 0; i < dims; i++) 61 | { 62 | if (in1.size(2 + i) != in2.size(2 + i)) 63 | { 64 | std::cout << "Tensor1 "; 65 | print_shape(in1); 66 | std::cout << "Tensor2 "; 67 | print_shape(in2); 68 | throw std::invalid_argument("shapes of input tensors do not match"); 69 | } 70 | } 71 | } 72 | 73 | void check_cpu(const torch::Tensor &in) 74 | { 75 | if (in.is_cuda()) 76 | { 77 | throw std::invalid_argument("input is not on CPU device, try using data.to('cpu') on input"); 78 | } 79 | } 80 | 81 | void check_cuda(const torch::Tensor &in) 82 | { 83 | if (!in.is_cuda()) 84 | { 85 | throw std::invalid_argument("input is not on CUDA device, try using data.to('cuda') on input"); 86 | } 87 | } 88 | 89 | void check_single_batch(const torch::Tensor &in) 90 | { 91 | if (in.size(0) != 1) 92 | { 93 | throw std::invalid_argument("FastGeodis currently only supports single batch input."); 94 | } 95 | } 96 | 97 | void check_data_dim(const torch::Tensor &in, const int &dims) 98 | { 99 | // check input dimensions 100 | const int num_dims = in.dim(); 101 | if (num_dims != dims) 102 | { 103 | throw std::invalid_argument( 104 | "function only supports 2D spatial inputs, received " + std::to_string(num_dims - 2)); 105 | } 106 | } 107 | 108 | void check_input_dimensions(const torch::Tensor &image, const torch::Tensor &mask, const int &num_dims) 109 | { 110 | // check tensor dims 111 | check_data_dim(image, num_dims); 112 | check_data_dim(mask, num_dims); 113 | 114 | // check batch==1 115 | check_single_batch(image); 116 | check_single_batch(mask); 117 | 118 | // check spatial shapes match 119 | check_spatial_shape_match(image, mask, num_dims - 2); 120 | } 121 | -------------------------------------------------------------------------------- /FastGeodis/fastgeodis.h: -------------------------------------------------------------------------------- 1 | // BSD 3-Clause License 2 | 3 | // Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | // All rights reserved. 5 | 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | 9 | // 1. Redistributions of source code must retain the above copyright notice, this 10 | // list of conditions and the following disclaimer. 11 | 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | 16 | // 3. Neither the name of the copyright holder nor the names of its 17 | // contributors may be used to endorse or promote products derived from 18 | // this software without specific prior written permission. 19 | 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | #pragma once 32 | 33 | #include 34 | #include 35 | #include "common.h" 36 | 37 | #ifdef WITH_CUDA 38 | torch::Tensor generalised_geodesic2d_cuda( 39 | const torch::Tensor &image, 40 | const torch::Tensor &mask, 41 | const float &v, 42 | const float &l_grad, 43 | const float &l_eucl, 44 | const int &iterations); 45 | 46 | torch::Tensor generalised_geodesic3d_cuda( 47 | const torch::Tensor &image, 48 | const torch::Tensor &mask, 49 | std::vector spacing, 50 | const float &v, 51 | const float &l_grad, 52 | const float &l_eucl, 53 | const int &iterations); 54 | #endif 55 | 56 | torch::Tensor generalised_geodesic2d_cpu( 57 | const torch::Tensor &image, 58 | const torch::Tensor &mask, 59 | const float &v, 60 | const float &l_grad, 61 | const float &l_eucl, 62 | const int &iterations); 63 | 64 | torch::Tensor generalised_geodesic3d_cpu( 65 | const torch::Tensor &image, 66 | const torch::Tensor &mask, 67 | std::vector spacing, 68 | const float &v, 69 | const float &l_grad, 70 | const float &l_eucl, 71 | const int &iterations); 72 | 73 | torch::Tensor generalised_geodesic2d_toivanen_cpu( 74 | const torch::Tensor &image, 75 | const torch::Tensor &mask, 76 | const float &v, 77 | const float &l_grad, 78 | const float &l_eucl, 79 | const int &iterations); 80 | 81 | torch::Tensor generalised_geodesic3d_toivanen_cpu( 82 | const torch::Tensor &image, 83 | const torch::Tensor &mask, 84 | const std::vector &spacing, 85 | const float &v, 86 | const float &l_grad, 87 | const float &l_eucl, 88 | const int &iterations); 89 | 90 | torch::Tensor geodesic2d_pixelqueue_cpu( 91 | const torch::Tensor &image, 92 | const torch::Tensor &mask, 93 | const float &l_grad, 94 | const float &l_eucl); 95 | 96 | torch::Tensor geodesic3d_pixelqueue_cpu( 97 | const torch::Tensor &image, 98 | const torch::Tensor &mask, 99 | const std::vector &spacing, 100 | const float &l_grad, 101 | const float &l_eucl); 102 | 103 | torch::Tensor geodesic2d_fastmarch_cpu( 104 | const torch::Tensor &image, 105 | const torch::Tensor &mask, 106 | const float &l_grad, 107 | const float &l_eucl); 108 | 109 | torch::Tensor geodesic3d_fastmarch_cpu( 110 | const torch::Tensor &image, 111 | const torch::Tensor &mask, 112 | const std::vector &spacing, 113 | const float &l_grad, 114 | const float &l_eucl); 115 | 116 | 117 | torch::Tensor generalised_geodesic2d( 118 | const torch::Tensor &image, 119 | const torch::Tensor &mask, 120 | const float &v, 121 | const float &l_grad, 122 | const float &l_eucl, 123 | const int &iterations); 124 | 125 | torch::Tensor generalised_geodesic3d( 126 | const torch::Tensor &image, 127 | const torch::Tensor &mask, 128 | const std::vector &spacing, 129 | const float &v, 130 | const float &l_grad, 131 | const float &l_eucl, 132 | const int &iterations); 133 | 134 | torch::Tensor generalised_geodesic2d_toivanen( 135 | const torch::Tensor &image, 136 | const torch::Tensor &mask, 137 | const float &v, 138 | const float &l_grad, 139 | const float &l_eucl, 140 | const int &iterations); 141 | 142 | torch::Tensor generalised_geodesic3d_toivanen( 143 | const torch::Tensor &image, 144 | const torch::Tensor &mask, 145 | const std::vector &spacing, 146 | const float &v, 147 | const float &l_grad, 148 | const float &l_eucl, 149 | const int &iterations); 150 | 151 | torch::Tensor geodesic2d_pixelqueue( 152 | const torch::Tensor &image, 153 | const torch::Tensor &mask, 154 | const float &l_grad, 155 | const float &l_eucl); 156 | 157 | torch::Tensor geodesic3d_pixelqueue( 158 | const torch::Tensor &image, 159 | const torch::Tensor &mask, 160 | const std::vector &spacing, 161 | const float &l_grad, 162 | const float &l_eucl); 163 | 164 | torch::Tensor geodesic2d_fastmarch( 165 | const torch::Tensor &image, 166 | const torch::Tensor &mask, 167 | const float &l_grad, 168 | const float &l_eucl); 169 | 170 | torch::Tensor geodesic3d_fastmarch( 171 | const torch::Tensor &image, 172 | const torch::Tensor &mask, 173 | const std::vector &spacing, 174 | const float &l_grad, 175 | const float &l_eucl); 176 | 177 | 178 | torch::Tensor signed_generalised_geodesic2d( 179 | const torch::Tensor &image, 180 | const torch::Tensor &mask, 181 | const float &v, 182 | const float &l_grad, 183 | const float &l_eucl, 184 | const int &iterations); 185 | 186 | torch::Tensor signed_generalised_geodesic3d( 187 | const torch::Tensor &image, 188 | const torch::Tensor &mask, 189 | const std::vector &spacing, 190 | const float &v, 191 | const float &l_grad, 192 | const float &l_eucl, 193 | const int &iterations); 194 | 195 | torch::Tensor signed_generalised_geodesic2d_toivanen( 196 | const torch::Tensor &image, 197 | const torch::Tensor &mask, 198 | const float &v, 199 | const float &l_grad, 200 | const float &l_eucl, 201 | const int &iterations); 202 | 203 | torch::Tensor signed_generalised_geodesic3d_toivanen( 204 | const torch::Tensor &image, 205 | const torch::Tensor &mask, 206 | const std::vector &spacing, 207 | const float &v, 208 | const float &l_grad, 209 | const float &l_eucl, 210 | const int &iterations); 211 | 212 | torch::Tensor signed_geodesic2d_pixelqueue( 213 | const torch::Tensor &image, 214 | const torch::Tensor &mask, 215 | const float &l_grad, 216 | const float &l_eucl); 217 | 218 | torch::Tensor signed_geodesic3d_pixelqueue( 219 | const torch::Tensor &image, 220 | const torch::Tensor &mask, 221 | const std::vector &spacing, 222 | const float &l_grad, 223 | const float &l_eucl); 224 | 225 | torch::Tensor signed_geodesic2d_fastmarch( 226 | const torch::Tensor &image, 227 | const torch::Tensor &mask, 228 | const float &l_grad, 229 | const float &l_eucl); 230 | 231 | torch::Tensor signed_geodesic3d_fastmarch( 232 | const torch::Tensor &image, 233 | const torch::Tensor &mask, 234 | const std::vector &spacing, 235 | const float &l_grad, 236 | const float &l_eucl); 237 | 238 | torch::Tensor GSF2d( 239 | const torch::Tensor &image, 240 | const torch::Tensor &mask, 241 | const float &theta, 242 | const float &v, 243 | const float &lambda, 244 | const int &iterations); 245 | 246 | torch::Tensor GSF3d( 247 | const torch::Tensor &image, 248 | const torch::Tensor &mask, 249 | const float &theta, 250 | const std::vector &spacing, 251 | const float &v, 252 | const float &lambda, 253 | const int &iterations); 254 | 255 | torch::Tensor GSF2d_toivanen( 256 | const torch::Tensor &image, 257 | const torch::Tensor &mask, 258 | const float &theta, 259 | const float &v, 260 | const float &lambda, 261 | const int &iterations); 262 | 263 | torch::Tensor GSF3d_toivanen( 264 | const torch::Tensor &image, 265 | const torch::Tensor &mask, 266 | const float &theta, 267 | const std::vector &spacing, 268 | const float &v, 269 | const float &lambda, 270 | const int &iterations); 271 | 272 | torch::Tensor GSF2d_pixelqueue( 273 | const torch::Tensor &image, 274 | const torch::Tensor &mask, 275 | const float &theta, 276 | const float &lambda); 277 | 278 | torch::Tensor GSF3d_pixelqueue( 279 | const torch::Tensor &image, 280 | const torch::Tensor &mask, 281 | const float &theta, 282 | const std::vector &spacing, 283 | const float &lambda); 284 | 285 | torch::Tensor GSF2d_fastmarch( 286 | const torch::Tensor &image, 287 | const torch::Tensor &mask, 288 | const float &theta, 289 | const float &lambda); 290 | 291 | torch::Tensor GSF3d_fastmarch( 292 | const torch::Tensor &image, 293 | const torch::Tensor &mask, 294 | const float &theta, 295 | const std::vector &spacing, 296 | const float &lambda); -------------------------------------------------------------------------------- /FastGeodis/geodis_toivanen.cpp: -------------------------------------------------------------------------------- 1 | // BSD 3-Clause License 2 | 3 | // Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | // All rights reserved. 5 | 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | 9 | // 1. Redistributions of source code must retain the above copyright notice, this 10 | // list of conditions and the following disclaimer. 11 | 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | 16 | // 3. Neither the name of the copyright holder nor the names of its 17 | // contributors may be used to endorse or promote products derived from 18 | // this software without specific prior written permission. 19 | 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | #include 32 | #include 33 | // #include 34 | 35 | float l1distance_toivanen(const float &in1, const float &in2) 36 | { 37 | return std::abs(in1 - in2); 38 | } 39 | 40 | void geodesic2d_forback_toivanen_cpu( 41 | const torch::Tensor &image, 42 | torch::Tensor &distance, 43 | const float &l_grad, 44 | const float &l_eucl) 45 | { 46 | // batch, channel, height, width 47 | const int channel = image.size(1); 48 | const int height = image.size(2); 49 | const int width = image.size(3); 50 | 51 | auto image_ptr = image.accessor(); 52 | auto distance_ptr = distance.accessor(); 53 | 54 | // forward 55 | const int dh_f[4] = {-1, -1, -1, 0}; 56 | const int dw_f[4] = {-1, 0, 1, -1}; 57 | 58 | const float local_dist_f[] = {sqrt(float(2.)), float(1.), sqrt(float(2.)), float(1.)}; 59 | 60 | for (int h = 0; h < height; h++) 61 | { 62 | for (int w = 0; w < width; w++) 63 | { 64 | float l_dist, cur_dist; 65 | float new_dist = distance_ptr[0][0][h][w]; 66 | 67 | for (int ind = 0; ind < 4; ind++) 68 | { 69 | const int h_ind = h + dh_f[ind]; 70 | const int w_ind = w + dw_f[ind]; 71 | 72 | if (w_ind < 0 || w_ind >= width || h_ind < 0 || h_ind >= height) 73 | continue; 74 | 75 | l_dist = 0.0; 76 | if (channel == 1) 77 | { 78 | l_dist = l1distance_toivanen( 79 | image_ptr[0][0][h][w], 80 | image_ptr[0][0][h_ind][w_ind]); 81 | } 82 | else 83 | { 84 | for (int c_i = 0; c_i < channel; c_i++) 85 | { 86 | l_dist += l1distance_toivanen( 87 | image_ptr[0][c_i][h][w], 88 | image_ptr[0][c_i][h_ind][w_ind]); 89 | } 90 | } 91 | cur_dist = distance_ptr[0][0][h_ind][w_ind] + 92 | l_eucl * local_dist_f[ind] + 93 | l_grad * l_dist; 94 | 95 | new_dist = std::min(new_dist, cur_dist); 96 | } 97 | distance_ptr[0][0][h][w] = new_dist; 98 | } 99 | } 100 | 101 | // backward 102 | const int dh_b[4] = {0, 1, 1, 1}; 103 | const int dw_b[4] = {1, -1, 0, 1}; 104 | 105 | const float local_dist_b[] = {float(1.), sqrt(float(2.)), float(1.), sqrt(float(2.))}; 106 | 107 | for (int h = height - 1; h >= 0; h--) 108 | { 109 | for (int w = width - 1; w >= 0; w--) 110 | { 111 | float l_dist, cur_dist; 112 | float new_dist = distance_ptr[0][0][h][w]; 113 | 114 | for (int ind = 0; ind < 4; ind++) 115 | { 116 | const int h_ind = h + dh_b[ind]; 117 | const int w_ind = w + dw_b[ind]; 118 | 119 | if (w_ind < 0 || w_ind >= width || h_ind < 0 || h_ind >= height) 120 | continue; 121 | 122 | l_dist = 0; 123 | if (channel == 1) 124 | { 125 | l_dist = l1distance_toivanen( 126 | image_ptr[0][0][h][w], 127 | image_ptr[0][0][h_ind][w_ind]); 128 | } 129 | else 130 | { 131 | for (int c_i = 0; c_i < channel; c_i++) 132 | { 133 | l_dist += l1distance_toivanen( 134 | image_ptr[0][c_i][h][w], 135 | image_ptr[0][c_i][h_ind][w_ind]); 136 | } 137 | } 138 | cur_dist = distance_ptr[0][0][h_ind][w_ind] + 139 | l_eucl * local_dist_b[ind] + 140 | l_grad * l_dist; 141 | 142 | new_dist = std::min(new_dist, cur_dist); 143 | } 144 | distance_ptr[0][0][h][w] = new_dist; 145 | } 146 | } 147 | } 148 | 149 | torch::Tensor generalised_geodesic2d_toivanen_cpu( 150 | const torch::Tensor &image, 151 | const torch::Tensor &mask, 152 | const float &v, 153 | const float &l_grad, 154 | const float &l_eucl, 155 | const int &iterations) 156 | { 157 | torch::Tensor distance = v * mask.clone(); 158 | 159 | // iteratively run the distance transform 160 | for (int itr = 0; itr < iterations; itr++) 161 | { 162 | geodesic2d_forback_toivanen_cpu(image, distance, l_grad, l_eucl); 163 | } 164 | 165 | return distance; 166 | } 167 | 168 | void geodesic3d_forback_toivanen_cpu( 169 | const torch::Tensor &image, 170 | torch::Tensor &distance, 171 | const std::vector &spacing, 172 | const float &l_grad, 173 | const float &l_eucl) 174 | { 175 | // batch, channel, depth, height, width 176 | const int channel = image.size(1); 177 | const int depth = image.size(2); 178 | const int height = image.size(3); 179 | const int width = image.size(4); 180 | 181 | auto image_ptr = image.accessor(); 182 | auto distance_ptr = distance.accessor(); 183 | 184 | // distances for forward 185 | const int dz_f[13] = {-1, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1}; 186 | const int dh_f[13] = {-1, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, -1, 0}; 187 | const int dw_f[13] = {-1, 0, 1, -1, 0, -1, 0, 1, -1, -1, 0, 1, -1}; 188 | 189 | float local_dist_f[13]; 190 | for (int i = 0; i < 13; i++) 191 | { 192 | float ld = 0.0; 193 | if (dz_f[i] != 0) 194 | { 195 | ld += spacing[0] * spacing[0]; 196 | } 197 | 198 | if (dh_f[i] != 0) 199 | { 200 | ld += spacing[1] * spacing[1]; 201 | } 202 | 203 | if (dw_f[i] != 0) 204 | { 205 | ld += spacing[2] * spacing[2]; 206 | } 207 | 208 | local_dist_f[i] = sqrt(ld); 209 | } 210 | 211 | // distances for backward 212 | const int dz_b[13] = {-1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1, 1}; 213 | const int dh_b[13] = {0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1}; 214 | const int dw_b[13] = {1, -1, 0, 1, 1, -1, 0, 1, 0, 1, -1, 0, 1}; 215 | 216 | float local_dist_b[13]; 217 | for (int i = 0; i < 13; i++) 218 | { 219 | float ld = 0.0; 220 | if (dz_b[i] != 0) 221 | { 222 | ld += spacing[0] * spacing[0]; 223 | } 224 | 225 | if (dh_b[i] != 0) 226 | { 227 | ld += spacing[1] * spacing[1]; 228 | } 229 | 230 | if (dw_b[i] != 0) 231 | { 232 | ld += spacing[2] * spacing[2]; 233 | } 234 | 235 | local_dist_b[i] = sqrt(ld); 236 | } 237 | 238 | // front-back 239 | for (int z = 0; z < depth; z++) 240 | { 241 | for (int h = 0; h < height; h++) 242 | { 243 | for (int w = 0; w < width; w++) 244 | { 245 | float l_dist, cur_dist; 246 | float new_dist = distance_ptr[0][0][z][h][w]; 247 | 248 | for (int ind = 0; ind < 13; ind++) 249 | { 250 | const int z_ind = z + dz_f[ind]; 251 | const int h_ind = h + dh_f[ind]; 252 | const int w_ind = w + dw_f[ind]; 253 | 254 | if (z_ind < 0 || z_ind >= depth || w_ind < 0 || w_ind >= width || h_ind < 0 || h_ind >= height) 255 | continue; 256 | 257 | l_dist = 0.0; 258 | if (channel == 1) 259 | { 260 | l_dist = l1distance_toivanen( 261 | image_ptr[0][0][z][h][w], 262 | image_ptr[0][0][z_ind][h_ind][w_ind]); 263 | } 264 | else 265 | { 266 | for (int c_i = 0; c_i < channel; c_i++) 267 | { 268 | l_dist += l1distance_toivanen( 269 | image_ptr[0][c_i][z][h][w], 270 | image_ptr[0][c_i][z_ind][h_ind][w_ind]); 271 | } 272 | } 273 | cur_dist = distance_ptr[0][0][z_ind][h_ind][w_ind] + 274 | l_eucl * local_dist_f[ind] + 275 | l_grad * l_dist; 276 | 277 | new_dist = std::min(new_dist, cur_dist); 278 | } 279 | distance_ptr[0][0][z][h][w] = new_dist; 280 | } 281 | } 282 | } 283 | 284 | // backward 285 | for (int z = depth - 1; z >= 0; z--) 286 | { 287 | for (int h = height - 1; h >= 0; h--) 288 | { 289 | for (int w = width - 1; w >= 0; w--) 290 | { 291 | float l_dist, cur_dist; 292 | float new_dist = distance_ptr[0][0][z][h][w]; 293 | 294 | for (int ind = 0; ind < 13; ind++) 295 | { 296 | const int z_ind = z + dz_b[ind]; 297 | const int h_ind = h + dh_b[ind]; 298 | const int w_ind = w + dw_b[ind]; 299 | 300 | if (z_ind < 0 || z_ind >= depth || w_ind < 0 || w_ind >= width || h_ind < 0 || h_ind >= height) 301 | continue; 302 | 303 | l_dist = 0.0; 304 | if (channel == 1) 305 | { 306 | l_dist = l1distance_toivanen( 307 | image_ptr[0][0][z][h][w], 308 | image_ptr[0][0][z_ind][h_ind][w_ind]); 309 | } 310 | else 311 | { 312 | for (int c_i = 0; c_i < channel; c_i++) 313 | { 314 | l_dist += l1distance_toivanen( 315 | image_ptr[0][c_i][z][h][w], 316 | image_ptr[0][c_i][z_ind][h_ind][w_ind]); 317 | } 318 | } 319 | cur_dist = distance_ptr[0][0][z_ind][h_ind][w_ind] + 320 | l_eucl * local_dist_b[ind] + 321 | l_grad * l_dist; 322 | 323 | new_dist = std::min(new_dist, cur_dist); 324 | } 325 | distance_ptr[0][0][z][h][w] = new_dist; 326 | } 327 | } 328 | } 329 | } 330 | 331 | torch::Tensor generalised_geodesic3d_toivanen_cpu( 332 | const torch::Tensor &image, 333 | const torch::Tensor &mask, 334 | const std::vector &spacing, 335 | const float &v, 336 | const float &l_grad, 337 | const float &l_eucl, 338 | const int &iterations) 339 | { 340 | torch::Tensor distance = v * mask.clone(); 341 | 342 | // iteratively run the distance transform 343 | for (int itr = 0; itr < iterations; itr++) 344 | { 345 | geodesic3d_forback_toivanen_cpu(image, distance, spacing, l_grad, l_eucl); 346 | } 347 | 348 | return distance; 349 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include FastGeodis/*h 3 | include FastGeodis/*cpp 4 | include FastGeodis/*cu 5 | include FastGeodis/*py 6 | recursive-include dependency/fmm/include/ * 7 | -------------------------------------------------------------------------------- /data/ISIC_546.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/ISIC_546.jpg -------------------------------------------------------------------------------- /data/brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/brain.png -------------------------------------------------------------------------------- /data/brain_seg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/brain_seg.png -------------------------------------------------------------------------------- /data/brain_seg_noisy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/brain_seg_noisy.png -------------------------------------------------------------------------------- /data/img2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/img2d.png -------------------------------------------------------------------------------- /data/img3d.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/data/img3d.nii.gz -------------------------------------------------------------------------------- /dependency/fmm/.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 7 | BUILD_TYPE: RelWithDebInfo 8 | 9 | jobs: 10 | build: 11 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 12 | # You can convert this to a matrix build if you need cross-platform coverage. 13 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Install optional tools (clang-tidy) 20 | run: sudo apt-get install -y clang-tidy 21 | 22 | - name: Setup Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.10' 26 | 27 | - name: Install Python dependencies 28 | run: pip install numpy scipy 29 | 30 | - name: Configure CMake 31 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 32 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 33 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} . 34 | 35 | - name: Build 36 | # Build your program with the given configuration 37 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 38 | 39 | - name: Test 40 | working-directory: ${{github.workspace}}/build 41 | # Execute tests defined by the CMake configuration. 42 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 43 | run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure 44 | 45 | -------------------------------------------------------------------------------- /dependency/fmm/.github/workflows/cpplint.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action to run cpplint recursively on all pushes and pull requests 2 | # https://github.com/cpplint/GitHub-Action-for-cpplint 3 | 4 | name: cpplint 5 | on: [push, pull_request] 6 | jobs: 7 | cpplint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.x 14 | - run: pip install cpplint 15 | - run: cpplint --recursive . 16 | -------------------------------------------------------------------------------- /dependency/fmm/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | 19 | # Compiled Static libraries 20 | *.lai 21 | *.la 22 | *.a 23 | *.lib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | *.user 30 | test/CMakeLists.txt.user 31 | *.suo 32 | *.sqlite 33 | -------------------------------------------------------------------------------- /dependency/fmm/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(fast-marching-method) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | 7 | find_program(CLANG_TIDY_EXE NAMES clang-tidy PATHS /opt/homebrew/opt/llvm/bin/) 8 | if(NOT CLANG_TIDY_EXE) 9 | message(STATUS "clang-tidy not found. Skipping corresponding checks.") 10 | else() 11 | set(CMAKE_CXX_CLANG_TIDY 12 | ${CLANG_TIDY_EXE}; 13 | -header-filter=.*fast_marching_method*; 14 | -checks=-*,portability-*,bugprone-*,readability-,clang-analyzer-*,perforance-*;) 15 | message(STATUS "Found clang-tidy: ${CLANG_TIDY_EXE}.") 16 | endif() 17 | 18 | # Default to release build 19 | if(NOT CMAKE_BUILD_TYPE) 20 | #set(CMAKE_BUILD_TYPE Release) 21 | set(CMAKE_BUILD_TYPE RelWithDebInfo) 22 | #set(CMAKE_BUILD_TYPE Debug) 23 | endif() 24 | message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") 25 | 26 | if(MSVC) 27 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 28 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") 29 | set(CompilerFlags 30 | CMAKE_CXX_FLAGS 31 | CMAKE_CXX_FLAGS_DEBUG 32 | CMAKE_CXX_FLAGS_RELEASE 33 | CMAKE_C_FLAGS 34 | CMAKE_C_FLAGS_DEBUG 35 | CMAKE_C_FLAGS_RELEASE) 36 | foreach(CompilerFlag ${CompilerFlags}) 37 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 38 | endforeach() 39 | message(STATUS "CXX flags (release): ${CMAKE_CXX_FLAGS_RELEASE}") 40 | message(STATUS "CXX flags (debug): ${CMAKE_CXX_FLAGS_DEBUG}") 41 | endif() 42 | 43 | # Warnings Compiler Flags 44 | set(MyWarningFlags 45 | $<$:/W4> 46 | $<$>:-Wall -Wextra -Wpedantic -Wno-gnu-zero-variadic-macro-arguments> 47 | ) 48 | # message(STATUS "Warning flags: ${MyWarningFlags}") 49 | 50 | if(MSVC) 51 | string( REPLACE "/DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") 52 | else() 53 | string( REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") 54 | endif() 55 | 56 | 57 | enable_testing() 58 | add_subdirectory(tests) 59 | add_subdirectory(examples/cpp) 60 | add_subdirectory(bindings/python) -------------------------------------------------------------------------------- /dependency/fmm/CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | # Disable the unapproved c++11 header checks 2 | filter=-build/c++11 3 | 4 | # Allow for non-const reference parameters 5 | filter=-runtime/references 6 | 7 | # Disable "Missing username in TODO" warning 8 | filter=-readability/todo -------------------------------------------------------------------------------- /dependency/fmm/bindings/python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | FetchContent_Declare( 3 | pybind11 4 | GIT_REPOSITORY https://github.com/pybind/pybind11.git 5 | GIT_TAG v2.10.4 6 | ) 7 | FetchContent_MakeAvailable(pybind11) 8 | 9 | # A linked issue is generated at the moment 10 | # See https://github.com/pybind/pybind11/pull/4301 11 | pybind11_add_module(py_fast_marching_method py-fast-marching-method.cpp) -------------------------------------------------------------------------------- /dependency/fmm/bindings/python/py-fast-marching-method.cpp: -------------------------------------------------------------------------------- 1 | // Permission is hereby granted, free of charge, to any person obtaining a 2 | // copy of this software and associated documentation files (the "Software"), 3 | // to deal in the Software without restriction, including without limitation 4 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | // and/or sell copies of the Software, and to permit persons to whom the 6 | // Software is furnished to do so, subject to the following conditions: 7 | // 8 | // The above copyright notice and this permission notice shall be included in 9 | // all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include "../../include/thinks/fast_marching_method/fast_marching_method.hpp" 24 | 25 | namespace py = pybind11; 26 | namespace fmm = thinks::fast_marching_method; 27 | 28 | template 29 | py::array_t UniformSpeedSignedArrivalTime( 30 | std::array const& py_grid_size, 31 | std::vector> const& py_boundary_indices, 32 | std::vector const& boundary_times, 33 | std::array const& py_grid_spacing, T const uniform_speed) { 34 | std::array grid_size; 35 | std::reverse_copy(py_grid_size.begin(), py_grid_size.end(), 36 | grid_size.begin()); 37 | 38 | std::array grid_spacing; 39 | std::reverse_copy(py_grid_spacing.begin(), py_grid_spacing.end(), 40 | grid_spacing.begin()); 41 | 42 | std::vector> boundary_indices; 43 | boundary_indices.reserve(py_boundary_indices.size()); 44 | for (auto& py_boundary_index : py_boundary_indices) { 45 | std::array boundary_index; 46 | std::reverse_copy(py_boundary_index.begin(), py_boundary_index.end(), 47 | boundary_index.begin()); 48 | boundary_indices.push_back(boundary_index); 49 | } 50 | 51 | // auto eikonal_solver = fmm::UniformSpeedEikonalSolver 52 | // (grid_spacing, uniform_speed); 53 | 54 | auto eikonal_solver = fmm::HighAccuracyUniformSpeedEikonalSolver( 55 | grid_spacing, uniform_speed); 56 | 57 | // auto eikonal_solver = fmm::DistanceSolver(grid_spacing[0]); 58 | 59 | std::vector arrival_times = fmm::SignedArrivalTime( 60 | grid_size, boundary_indices, boundary_times, eikonal_solver); 61 | 62 | return py::array_t(py_grid_size, &arrival_times[0]); 63 | } 64 | 65 | template 66 | py::array_t VaryingSpeedSignedArrivalTime( 67 | std::array const& py_grid_size, 68 | std::vector> const& py_boundary_indices, 69 | std::vector const& boundary_times, 70 | std::array const& py_grid_spacing, 71 | py::array_t py_speed_buffer) { 72 | auto py_speed_buffer_flat = py_speed_buffer.reshape({py_speed_buffer.size()}); 73 | auto speed_buffer = py_speed_buffer_flat.template cast>(); 74 | 75 | std::array grid_size; 76 | std::reverse_copy(py_grid_size.begin(), py_grid_size.end(), 77 | grid_size.begin()); 78 | 79 | std::array grid_spacing; 80 | std::reverse_copy(py_grid_spacing.begin(), py_grid_spacing.end(), 81 | grid_spacing.begin()); 82 | 83 | std::vector> boundary_indices; 84 | boundary_indices.reserve(py_boundary_indices.size()); 85 | for (auto& py_boundary_index : py_boundary_indices) { 86 | std::array boundary_index; 87 | std::reverse_copy(py_boundary_index.begin(), py_boundary_index.end(), 88 | boundary_index.begin()); 89 | boundary_indices.push_back(boundary_index); 90 | } 91 | 92 | // auto eikonal_solver = fmm::VaryingSpeedEikonalSolver 93 | // (grid_spacing, grid_size, speed_buffer); 94 | 95 | auto eikonal_solver = fmm::HighAccuracyVaryingSpeedEikonalSolver( 96 | grid_spacing, grid_size, speed_buffer); 97 | 98 | std::vector arrival_times = fmm::SignedArrivalTime( 99 | grid_size, boundary_indices, boundary_times, eikonal_solver); 100 | 101 | return py::array_t(py_grid_size, &arrival_times[0]); 102 | } 103 | 104 | PYBIND11_MODULE(py_fast_marching_method, m) { 105 | m.doc() = R"pbdoc( 106 | Python bindings for the fast marching method 107 | ----------------------- 108 | .. currentmodule:: py_fast_marching_method 109 | .. autosummary:: 110 | :toctree: _generate 111 | uniform_speed_signed_arrival_time 112 | varying_speed_signed_arrival_time 113 | )pbdoc"; 114 | 115 | m.def("uniform_speed_signed_arrival_time", 116 | &UniformSpeedSignedArrivalTime, R"pbdoc( 117 | Signed arrival time under uniform speed 118 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 119 | )pbdoc"); 120 | 121 | m.def("uniform_speed_signed_arrival_time", 122 | &UniformSpeedSignedArrivalTime, R"pbdoc( 123 | Signed arrival time under uniform speed 124 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 125 | )pbdoc"); 126 | 127 | m.def("varying_speed_signed_arrival_time", 128 | &VaryingSpeedSignedArrivalTime, R"pbdoc( 129 | Signed arrival time under varying speed 130 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 131 | )pbdoc"); 132 | 133 | m.def("varying_speed_signed_arrival_time", 134 | &VaryingSpeedSignedArrivalTime, R"pbdoc( 135 | Signed arrival time under varying speed 136 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 137 | )pbdoc"); 138 | } 139 | -------------------------------------------------------------------------------- /dependency/fmm/examples/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(fast-marching-minimal-example 2 | minimal_example.cpp) 3 | 4 | # Warnings Compiler Flags 5 | target_compile_options(fast-marching-minimal-example PRIVATE ${MyWarningFlags}) -------------------------------------------------------------------------------- /dependency/fmm/examples/cpp/minimal_example.cpp: -------------------------------------------------------------------------------- 1 | // Permission is hereby granted, free of charge, to any person obtaining a 2 | // copy of this software and associated documentation files (the "Software"), 3 | // to deal in the Software without restriction, including without limitation 4 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | // and/or sell copies of the Software, and to permit persons to whom the 6 | // Software is furnished to do so, subject to the following conditions: 7 | // 8 | // The above copyright notice and this permission notice shall be included in 9 | // all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "../../include/thinks/fast_marching_method/fast_marching_method.hpp" 25 | 26 | int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { 27 | try { 28 | namespace fmm = thinks::fast_marching_method; 29 | 30 | auto grid_size = std::array{{16, 16}}; 31 | auto grid_spacing = std::array{{1.f / 16, 1.f / 16}}; 32 | auto uniform_speed = 1.f; 33 | 34 | std::cout << "Grid size: " << grid_size[0] << ", " << grid_size[1] 35 | << std::endl; 36 | std::cout << "Grid spacing: " << grid_spacing[0] << ", " << grid_spacing[1] 37 | << std::endl; 38 | std::cout << "Uniform speed: " << uniform_speed << std::endl; 39 | 40 | // Select points close to a true circle 41 | auto circle_boundary_indices = std::vector>{ 42 | {{5, 3}}, {{6, 3}}, {{7, 3}}, {{8, 3}}, {{9, 3}}, {{10, 3}}, 43 | {{4, 4}}, {{5, 4}}, {{10, 4}}, {{11, 4}}, {{3, 5}}, {{4, 5}}, 44 | {{11, 5}}, {{12, 5}}, {{3, 6}}, {{12, 6}}, {{3, 7}}, {{12, 7}}, 45 | {{3, 8}}, {{12, 8}}, {{3, 9}}, {{12, 9}}, {{3, 10}}, {{4, 10}}, 46 | {{11, 10}}, {{12, 10}}, {{4, 11}}, {{5, 11}}, {{10, 11}}, {{11, 11}}, 47 | {{5, 12}}, {{6, 12}}, {{7, 12}}, {{8, 12}}, {{9, 12}}, {{10, 12}}, 48 | }; 49 | 50 | auto num_seeds = circle_boundary_indices.size(); 51 | 52 | // Specify distances of such points to the true circle 53 | auto circle_boundary_distances = std::vector{ 54 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 55 | 0.0417385f, 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 56 | 0.0417385f, -0.0111773f, -0.0111773f, 0.0417385f, 0.0164635f, 57 | 0.0164635f, 0.0029808f, 0.0029808f, 0.0029808f, 0.0029808f, 58 | 0.0164635f, 0.0164635f, 0.0417385f, -0.0111773f, -0.0111773f, 59 | 0.0417385f, 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 60 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 61 | 0.0417385f}; 62 | 63 | // auto circle_boundary_distances = std::vector(num_seeds, 0.f); 64 | 65 | auto nan = std::numeric_limits::quiet_NaN(); 66 | auto initial_distances = 67 | std::vector(grid_size[0] * grid_size[1], nan); 68 | for (std::size_t i = 0; i < num_seeds; ++i) { 69 | auto idx = circle_boundary_indices[i]; 70 | initial_distances[idx[0] + idx[1] * grid_size[0]] = 71 | circle_boundary_distances[i]; 72 | } 73 | 74 | { 75 | std::cout << "Initial distance map (seeds):" << std::endl; 76 | std::size_t idx = 0; 77 | for (std::size_t j = 0; j < grid_size[1]; ++j) { 78 | for (std::size_t i = 0; i < grid_size[0]; ++i) { 79 | std::cout << std::setw(6) << std::fixed << std::setprecision(4) 80 | << initial_distances[idx++] << '\t'; 81 | } 82 | std::cout << std::endl; 83 | } 84 | } 85 | 86 | // Compute distance map 87 | auto arrival_times = fmm::SignedArrivalTime( 88 | grid_size, circle_boundary_indices, circle_boundary_distances, 89 | fmm::UniformSpeedEikonalSolver(grid_spacing, uniform_speed)); 90 | 91 | { 92 | std::cout << "Distance map:" << std::endl; 93 | std::size_t idx = 0; 94 | for (std::size_t j = 0; j < grid_size[1]; ++j) { 95 | for (std::size_t i = 0; i < grid_size[0]; ++i) { 96 | std::cout << std::setw(6) << std::fixed << std::setprecision(4) 97 | << arrival_times[idx++] << '\t'; 98 | } 99 | std::cout << std::endl; 100 | } 101 | } 102 | } catch (const std::exception& e) { 103 | // standard exceptions 104 | std::cout << "Caught std::exception in main: " << e.what() << std::endl; 105 | return EXIT_FAILURE; 106 | } catch (...) { 107 | // everything else 108 | std::cout << "Caught unknown exception in main" << std::endl; 109 | return EXIT_FAILURE; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /dependency/fmm/examples/python/py-fast-marching-minimal-example.py: -------------------------------------------------------------------------------- 1 | import py_fast_marching_method as fmm 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def signed_arrival_time_example(): 7 | grid_size = np.array([50, 100]) 8 | # grid_spacing = 1.0 / grid_size 9 | grid_spacing = np.ones(grid_size.shape) 10 | boundary_indices = np.array([[31, 75]]) 11 | boundary_times = np.array([0.0]) 12 | uniform_speed = 1.0 13 | 14 | arrival_times = fmm.uniform_speed_signed_arrival_time( 15 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 16 | ) 17 | 18 | print("Max dist:", np.max(arrival_times[:])) 19 | 20 | plt.imshow(arrival_times) 21 | plt.plot(boundary_indices[0,1], boundary_indices[0,0], "mo") 22 | plt.colorbar() 23 | plt.show() 24 | 25 | 26 | if __name__ == "__main__": 27 | signed_arrival_time_example() 28 | -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme.pdf -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_concept.png -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_dilation_bands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_dilation_bands.png -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_eikonal_solvers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_eikonal_solvers.png -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_input_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_input_values.png -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_inside_outside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_inside_outside.png -------------------------------------------------------------------------------- /dependency/fmm/img/fmm_readme_point_source_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/dependency/fmm/img/fmm_readme_point_source_error.png -------------------------------------------------------------------------------- /dependency/fmm/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | FetchContent_Declare( 3 | googletest 4 | GIT_REPOSITORY https://github.com/google/googletest.git 5 | GIT_TAG 750d67d809700ae8fca6d610f7b41b71aa161808 6 | SYSTEM 7 | ) 8 | # For Windows: Prevent overriding the parent project's compiler/linker settings 9 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 10 | FetchContent_MakeAvailable(googletest) 11 | 12 | set_target_properties(gtest PROPERTIES CXX_CLANG_TIDY "") 13 | set_target_properties(gtest_main PROPERTIES CXX_CLANG_TIDY "") 14 | set_target_properties(gmock PROPERTIES CXX_CLANG_TIDY "") 15 | set_target_properties(gmock_main PROPERTIES CXX_CLANG_TIDY "") 16 | 17 | add_executable(fast-marching-method-test 18 | main.cpp 19 | eikonal_solvers_test.cpp 20 | signed_arrival_time_test.cpp) 21 | 22 | target_link_libraries(fast-marching-method-test PRIVATE GTest::gtest GTest::gtest_main) 23 | 24 | # Warnings Compiler Flags 25 | target_compile_options(fast-marching-method-test PRIVATE ${MyWarningFlags}) 26 | 27 | add_test(NAME fast-marching-method-test COMMAND fast-marching-method-test) 28 | 29 | # Python bindings test 30 | add_test(NAME py-bindings-test 31 | COMMAND ${PYTHON_EXECUTABLE} py-bindings-test.py 32 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 33 | 34 | set_tests_properties(py-bindings-test PROPERTIES 35 | ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${CMAKE_BINARY_DIR}/bindings/python") -------------------------------------------------------------------------------- /dependency/fmm/tests/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #include 22 | 23 | int main(int argc, char* argv[]) { 24 | ::testing::InitGoogleTest(&argc, argv); 25 | 26 | return RUN_ALL_TESTS(); 27 | } 28 | -------------------------------------------------------------------------------- /dependency/fmm/tests/py-bindings-test.py: -------------------------------------------------------------------------------- 1 | import py_fast_marching_method as fmm 2 | import numpy as np 3 | from scipy import ndimage as ndi 4 | 5 | 6 | def test_uniform_2D(): 7 | grid_size = np.array([50, 101]) 8 | # grid_spacing = 1.0/grid_size 9 | grid_spacing = np.ones(grid_size.shape) 10 | boundary_indices = np.array([[20, 75]]) 11 | boundary_times = np.array([0.0]) 12 | uniform_speed = 1.0 13 | 14 | arrival_times = fmm.uniform_speed_signed_arrival_time( 15 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 16 | ) 17 | print("arrival_times\n", arrival_times) 18 | 19 | binary_input_grid = np.ones(grid_size) 20 | binary_input_grid[boundary_indices[:, 0], boundary_indices[:, 1]] = 0 21 | print("binary_input_grid\n", binary_input_grid) 22 | 23 | edt = ndi.distance_transform_edt(binary_input_grid) 24 | print("edt\n", edt) 25 | 26 | # FMM is not super accurate -> large tolerance 27 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.5) 28 | 29 | print("Done 2D") 30 | 31 | 32 | def test_uniform_3D(): 33 | grid_size = np.array([30, 76, 43]) 34 | # grid_spacing = 1.0/grid_size 35 | grid_spacing = np.ones(grid_size.shape) 36 | boundary_indices = np.array([[9, 50, 21]]) 37 | boundary_times = np.array([0.0]) 38 | uniform_speed = 1.0 39 | 40 | arrival_times = fmm.uniform_speed_signed_arrival_time( 41 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 42 | ) 43 | print("arrival_times\n", arrival_times) 44 | 45 | binary_input_grid = np.ones(grid_size) 46 | binary_input_grid[ 47 | boundary_indices[:, 0], boundary_indices[:, 1], boundary_indices[:, 2] 48 | ] = 0 49 | print("binary_input_grid\n", binary_input_grid) 50 | 51 | edt = ndi.distance_transform_edt(binary_input_grid) 52 | print("edt\n", edt) 53 | 54 | # FMM is not super accurate -> large tolerance 55 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.7) 56 | 57 | print("Done 3D") 58 | 59 | 60 | def test_varying_2D(): 61 | grid_size = np.array([5, 8]) 62 | # grid_spacing = 1.0/grid_size 63 | grid_spacing = np.ones(grid_size.shape) 64 | boundary_indices = np.array([[2, 5]]) 65 | boundary_times = np.array([0.0]) 66 | varying_speed = np.ones(grid_size) 67 | 68 | arrival_times = fmm.varying_speed_signed_arrival_time( 69 | grid_size, boundary_indices, boundary_times, grid_spacing, varying_speed 70 | ) 71 | print("arrival_times\n", arrival_times) 72 | 73 | binary_input_grid = np.ones(grid_size) 74 | binary_input_grid[boundary_indices[:, 0], boundary_indices[:, 1]] = 0 75 | print("binary_input_grid\n", binary_input_grid) 76 | 77 | edt = ndi.distance_transform_edt(binary_input_grid) 78 | print("edt\n", edt) 79 | 80 | # FMM is not super accurate -> large tolerance 81 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.5) 82 | 83 | print("Done 2D") 84 | 85 | 86 | if __name__ == "__main__": 87 | test_uniform_2D() 88 | test_uniform_3D() 89 | test_varying_2D() 90 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | HTMLCOPYDIR = . 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements-doc.txt: -------------------------------------------------------------------------------- 1 | sphinxemoji 2 | sphinxcontrib-bibtex 3 | torch 4 | -------------------------------------------------------------------------------- /docs/source/acknowledgement.rst: -------------------------------------------------------------------------------- 1 | ************************** 2 | Acknowledgement 3 | ************************** 4 | 5 | Development of **FastGeodis** was supported by the European Union's Horizon 2020 research and innovation programme under grant agreement No 101016131. 6 | 7 | We would also like to thank reviewers and editors for the `Journal of Open Source Software `_ for their valuable feedback and comments to further improve this package. 8 | 9 | -------------------------------------------------------------------------------- /docs/source/api_docs.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | API Documentation 3 | ********************* 4 | 5 | Modules and Functions 6 | ---------------------- 7 | This section provides the documentation of functions implemented in **FastGeodis** package. 8 | For usage examples, please see: :doc:`usage_examples` section of this documentation. 9 | 10 | .. automodule:: FastGeodis 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Unittests 17 | ----------- 18 | 19 | **FastGeodis** contains automated tests for checking correctness and functionality of the package. These are integrated as Github Workflows and are automatically run on multiple platforms anytime a push or pull request is made. These unittests can also be run locally as: 20 | :: 21 | 22 | pip install -r requirements-dev.txt 23 | python -m unittest 24 | 25 | The test script can be found under the `./tests `_ folder in the **FastGeodis** repository. For more information on unittest and instructions on how to run test files through the command line see the `unittest documentation `_. 26 | 27 | -------------------------------------------------------------------------------- /docs/source/citing.rst: -------------------------------------------------------------------------------- 1 | ************************** 2 | How to Cite FastGeodis 3 | ************************** 4 | 5 | When using **FastGeodis** in scientific publications, please cite the following paper: 6 | 7 | .. image:: https://joss.theoj.org/papers/d0b6e3daa4b22fec471691c6f1c60e2a/status.svg 8 | :target: https://joss.theoj.org/papers/d0b6e3daa4b22fec471691c6f1c60e2a 9 | 10 | **Asad, Muhammad, Reuben Dorent, and Tom Vercauteren. "FastGeodis: Fast Generalised Geodesic Distance Transform." Journal of Open Source Software (JOSS), 2022.** (paper link:https://doi.org/10.21105/joss.04532) 11 | 12 | Bibtex: 13 | :: 14 | 15 | @article{asad2022fastgeodis, 16 | doi = {10.21105/joss.04532}, 17 | url = {https://doi.org/10.21105/joss.04532}, 18 | year = {2022}, 19 | publisher = {The Open Journal}, 20 | volume = {7}, 21 | number = {79}, 22 | pages = {4532}, 23 | author = {Muhammad Asad and Reuben Dorent and Tom Vercauteren}, 24 | title = {FastGeodis: Fast Generalised Geodesic Distance Transform}, 25 | journal = {Journal of Open Source Software} 26 | } 27 | 28 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../../FastGeodis')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'FastGeodis' 21 | copyright = '2022, Muhammad Asad' 22 | author = 'Muhammad Asad' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.0.0' 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.napoleon', 35 | 'sphinx.ext.viewcode', 36 | 'sphinx.ext.mathjax', 37 | 'sphinx_rtd_theme', 38 | 'sphinxemoji.sphinxemoji', 39 | 'sphinxcontrib.bibtex', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | # This pattern also affects html_static_path and html_extra_path. 48 | exclude_patterns = [] 49 | 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ['_static'] 63 | 64 | 65 | # bibtex files 66 | bibtex_bibfiles = ['refs.bib'] -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Contributing 3 | ***************** 4 | 5 | 6 | Bugs |:bug:| 7 | ################################ 8 | If you found a bug in FastGeodis, please `open an issue request in GitHub. `_ Please be as descriptive as possible in your issue request. 9 | 10 | Feature suggestions |:bulb:| 11 | ################################ 12 | If you'd like to suggest a feature to be added to FastGeodis, please `open an issue request in GitHub. `_ and describe your feature appropriately. 13 | 14 | 15 | Contributing |:computer:| 16 | ################################ 17 | The following steps should be taken to contribute to FastGeodis: 18 | 19 | 1. Fork the FastGeodis repository. 20 | 2. Create a branch for any changes you are contributing and add your changes locally. 21 | 3. Ensure that unittests pass locally for you changes. 22 | 4. Commit your code to the branch in your fork. 23 | 5. Create a pull request that documents your additions/changes. 24 | 6. Add as much description for your proposed changes and fix any build issues within Github CI. -------------------------------------------------------------------------------- /docs/source/experiment2d.csv: -------------------------------------------------------------------------------- 1 | Device,CPU,CPU,GPU,CPU,GPU 2 | Spatial Size,GeodisTK,FastGeodis,FastGeodis,Speed-up,Speed-up 3 | 64 x 64,0.00401 s,0.00178 s,0.00245 s,2.3 x,1.6 x 4 | 128 x 128,0.01555 s,0.00279 s,0.00487 s,5.6 x,3.2 x 5 | 256 x 256,0.06412 s,0.00705 s,0.00961 s,9.1 x,6.7 x 6 | 512 x 512,0.25041 s,0.01873 s,0.01735 s,13.4 x,14.4 x 7 | 1024 x 1024,1.00589 s,0.04581 s,0.03461 s,22 x,29.1 x 8 | 2048 x 2048,4.01677 s,0.18565 s,0.07161 s,21.6 x,56.1 x 9 | -------------------------------------------------------------------------------- /docs/source/experiment3d.csv: -------------------------------------------------------------------------------- 1 | Device,CPU,CPU,GPU,CPU,GPU 2 | Spatial Size,GeodisTK,FastGeodis,FastGeodis,Speed-up,Speed-up 3 | 64 x 64 x64,0.13098 s,0.03933 s,0.00724 s,3.3 x,18.1 x 4 | 128 x 128 x 128,1.09478 s,0.30489 s,0.01783 s,3.6 x,61.4 x 5 | 256 x 256 x 256,8.60261 s,2.53498 s,0.12443 s,3.4 x,69.1 x 6 | 512 x 512 x 512,69.08086 s,22.24171 s,0.92998 s,3.1 x,74.3 x 7 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Getting Started 3 | ***************** 4 | 5 | About 6 | ######## 7 | **FastGeodis** provides efficient CPU (OpenMP) and GPU (CUDA) implementations of Generalised Geodesic Distance Transform in PyTorch for 2D and 3D input data based on parallelisable raster scan ideas from [1]. It includes methods for computing Geodesic, Euclidean distance transform and mixture of both. 8 | See :doc:`methodology` section for more details of the implemented algorithm. 9 | 10 | 1. Criminisi, Antonio, Toby Sharp, and Khan Siddiqui. "Interactive Geodesic Segmentation of n-Dimensional Medical Images on the Graphics Processor." Radiological Society of North America (RSNA), 2009. 11 | 12 | 13 | Python Version Support 14 | ################################### 15 | :code:`Python 3.6`, :code:`Python 3.7`, :code:`Python 3.8`, :code:`Python 3.9` 16 | 17 | 18 | Installation 19 | ################################### 20 | 21 | **FastGeodis** can be installed via pip by running the following from a terminal window: 22 | :: 23 | 24 | pip install FastGeodis 25 | 26 | or 27 | 28 | :: 29 | 30 | pip install git+https://github.com/masadcv/FastGeodis 31 | 32 | or (on conda environment with existing installation of PyTorch with CUDA) 33 | :: 34 | 35 | pip install FastGeodis --no-build-isolation 36 | 37 | 38 | Dependencies 39 | ################################### 40 | 41 | +------------+------------------+ 42 | | Dependency | Minimum Version | 43 | +============+==================+ 44 | |torch | 1.5.0 | 45 | +------------+------------------+ 46 | 47 | In addition, for compilation and execution on GPU, the **FastGeodis** package requires a CUDA installation compatible with installed PyTorch version. 48 | 49 | Optional Development Dependencies 50 | ################################### 51 | +-------------+------------------+ 52 | | Dependency | Minimum Version | 53 | +=============+==================+ 54 | |numpy | 1.19.2 | 55 | +-------------+------------------+ 56 | |matplotlib | 3.2.0 | 57 | +-------------+------------------+ 58 | |parameterized| 0.7.0 | 59 | +-------------+------------------+ 60 | |SimpleITK | 2.0.0 | 61 | +-------------+------------------+ 62 | 63 | 64 | Example Usage 65 | ################################### 66 | The following demonstrates a simple example showing FastGeodis usage: 67 | 68 | To compute Geodesic Distance Transform: 69 | :: 70 | 71 | device = "cuda" if torch.cuda.is_available() else "cpu" 72 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 73 | 74 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 75 | image_pt = image_pt.to(device) 76 | mask_pt = torch.ones_like(image_pt) 77 | mask_pt[..., 100, 100] = 0 78 | 79 | v = 1e10 80 | # lamb = 0.0 (Euclidean) or 1.0 (Geodesic) or (0.0, 1.0) (mixture) 81 | lamb = 1.0 82 | iterations = 2 83 | geodesic_dist = FastGeodis.generalised_geodesic2d( 84 | image_pt, mask_pt, v, lamb, iterations 85 | ) 86 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 87 | 88 | To compute Euclidean Distance Transform: 89 | :: 90 | 91 | device = "cuda" if torch.cuda.is_available() else "cpu" 92 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 93 | 94 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 95 | image_pt = image_pt.to(device) 96 | mask_pt = torch.ones_like(image_pt) 97 | mask_pt[..., 100, 100] = 0 98 | 99 | v = 1e10 100 | # lamb = 0.0 (Euclidean) or 1.0 (Geodesic) or (0.0, 1.0) (mixture) 101 | lamb = 0.0 102 | iterations = 2 103 | euclidean_dist = FastGeodis.generalised_geodesic2d( 104 | image_pt, mask_pt, v, lamb, iterations 105 | ) 106 | euclidean_dist = np.squeeze(euclidean_dist.cpu().numpy()) 107 | 108 | For more comprehensive usages examples, see: :doc:`usage_examples` 109 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. FastGeodis documentation master file, created by 2 | sphinx-quickstart on Fri Jun 24 19:31:02 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to FastGeodis's documentation! 7 | ====================================== 8 | 9 | Intro 10 | ############################## 11 | 12 | 13 | **FastGeodis** provides efficient CPU (OpenMP) and GPU (CUDA) implementations of Generalised Geodesic Distance Transform in PyTorch for 2D and 3D input data based on parallelisable raster scan ideas from [1]. It includes methods for computing Geodesic, Euclidean distance transform and mixture of both. 14 | 15 | .. table:: 16 | :align: center 17 | 18 | +--------------------------------------------+--------------------------------------------+ 19 | | **2D images:** 1 of 4 passes | **3D volumes:** 1 of 6 passes | 20 | +--------------------------------------------+--------------------------------------------+ 21 | | .. figure:: ../../figures/FastGeodis2D.png | .. figure:: ../../figures/FastGeodis3D.png | 22 | | :alt: 2D | :alt: 3D | 23 | | | | 24 | +--------------------------------------------+--------------------------------------------+ 25 | 26 | The above raster scan method can be parallelised for each row/plane on an available device (CPU or GPU). This leads to significant speed up as compared to existing non-parallelised raster scan implementations (e.g. https://github.com/taigw/GeodisTK). Python interface is provided (using PyTorch) for enabling its use in deep learning and image processing pipelines. 27 | 28 | See :doc:`methodology` section for more details of the implemented algorithm. 29 | 30 | 1. Criminisi, Antonio, Toby Sharp, and Khan Siddiqui. "Interactive Geodesic Segmentation of n-Dimensional Medical Images on the Graphics Processor." Radiological Society of North America (RSNA), 2009. 31 | \ 32 | 33 | 34 | Getting Started 35 | ################################### 36 | For information on getting started with using **FastGeodis**, including installation, dependencies, and other similar topics, see :doc:`getting_started` page. 37 | 38 | Table of Contents 39 | ################# 40 | Use table of contents below, or on the left panel to explore this documentation. 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | 45 | Introduction 46 | 47 | methodology 48 | 49 | getting_started 50 | 51 | usage_examples 52 | 53 | .. toctree:: 54 | :maxdepth: 2 55 | :hidden: 56 | :caption: Reference Documentation 57 | 58 | api_docs 59 | 60 | license 61 | 62 | acknowledgement 63 | 64 | .. toctree:: 65 | :maxdepth: 1 66 | :hidden: 67 | :caption: Getting Involved 68 | 69 | Source Code 70 | Contributing 71 | Citing 72 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | ************************** 2 | License 3 | ************************** 4 | 5 | BSD 3-Clause License 6 | 7 | Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | 3. Neither the name of the copyright holder nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /docs/source/methodology.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Methodology 3 | ***************** 4 | 5 | Generalised Geodesic Distance Transform 6 | ####################################### 7 | 8 | A distance transform maps each image pixel/voxel into its smallest distance to regions of interest :cite:p:`fabbri20082d`. It is a fundamental operator with relevant applications in computer vision, graphics, shape analysis, pattern recognition and computational geometry :cite:p:`fabbri20082d`. 9 | 10 | Generalised Geodesic distance transform combines spatial as well as gradient based distance transform and is defined as :cite:p:`criminisi2008geos`: 11 | 12 | .. math:: 13 | d(\mathbf{x}; I, \lambda) = \min \sum_{k=1}^{n} \Big[ \underbrace{\left( 1-\lambda \right) \left \| \mathbf{x}_k - \mathbf{x}_{k-1} \right \|_{L1}}_{\text{Euclidean Term}} + \underbrace{\lambda \left | I(\mathbf{x}_k) - I(\mathbf{x}_{k-1}) \right |}_{\text{Geodesic Term}} \Big] 14 | 15 | The ability to incorporate image gradients with spatial distances has enabled application of Geodesic distance transforms in a number of areas, including image editing and filtering :cite:p:`criminisi2008geos`. Methods from recent years have shown effectiveness in applying Geodesic distance transform to interactively annotate 3D medical imaging data :cite:p:`wang2018deepigeos,criminisi2008geos`, where it enables providing segmentation labels, i.e. voxel-wise labels, for different objects of interests. 16 | 17 | Statement of Need 18 | ################## 19 | 20 | Despite existing open-source implementation of distance transforms :cite:p:`tensorflow2015-whitepaper,eucildeantdimpl,geodistk`, open-source implementations of efficient Geodesic distance transform algorithms :cite:p:`criminisiinteractive,weber2008parallel` on CPU and GPU do not exist. However, efficient CPU :cite:p:`eucildeantdimpl` and GPU :cite:p:`tensorflow2015-whitepaper` implementations exist for Euclidean distance transform. To the best of our knowledge, **FastGeodis** is the first open-source implementation of efficient Geodesic distance transform :cite:p:`criminisiinteractive`, achieving up to 20x speed-up on CPU and up to 74x speed-up on GPU as compared to existing open-source libraries :cite:p:`geodistk`. It also provides efficient implementation of Euclidean distance transform. In addition, it is the first open-source implementation of generalised Geodesic distance transform and Geodesic Symmetric Filtering (GSF) proposed in :cite:p:`criminisi2008geos`. 21 | 22 | 23 | The ability to efficiently compute Geodesic and Euclidean distance transforms can significantly enhance distance transform applications especially for training deep learning models that utilise distance transforms :cite:p:`wang2018deepigeos`. It will improve prototyping, experimentation, and deployment of such methods, where efficient computation of distance transforms has been a limiting factor. In 3D medical imaging problems, efficient computation of distance transforms will lead to significant speed-ups, enabling online learning applications for better processing/labelling/inference from volumetric datasets :cite:p:`asad2022econet`. In addition, **FastGeodis** provides efficient implementation for both CPUs and GPUs hardware and hence will enable efficient use of a wide range of hardware devices. 24 | 25 | Implementation 26 | ################# 27 | **FastGeodis** implements an efficient distance transform algorithm from :cite:p:`criminisiinteractive`, which provides parallelisable raster scans to compute distance transform. The implementation consists of data propagation passes parallelised using threads for elements across a line (2D) or plane (3D). Figure below shows these data propagation passes, where each pass consists of computing distance values for next row (2D) or plane (3D) by utilising parallel threads and data from previous row/plane, hence resulting in propagating distance values along the direction of pass. For 2D data, four distance propagation passes are required, top-bottom, bottom-top, left-right and right-left, whereas for 3D data six passes are required, front-back, back-front, top-bottom, bottom-top, left-right and right-left. The algorithm can be applied to efficiently compute both Geodesic and Euclidean distance transforms. In addition to this, **FastGeodis** also provides non-parallelisable raster scan based distance transform method from :cite:p:`toivanen1996new`, which is implemented using single-thread CPU and used for comparison. 28 | 29 | .. table:: 30 | :align: center 31 | 32 | +--------------------------------------------+--------------------------------------------+ 33 | | **2D images:** 1 of 4 passes | **3D volumes:** 1 of 6 passes | 34 | +--------------------------------------------+--------------------------------------------+ 35 | | .. figure:: ../../figures/FastGeodis2D.png | .. figure:: ../../figures/FastGeodis3D.png | 36 | | :alt: 2D | :alt: 3D | 37 | | | | 38 | +--------------------------------------------+--------------------------------------------+ 39 | 40 | **FastGeodis** package is implemented using **PyTorch** :cite:p:`NEURIPS2019_9015` utilising OpenMP for CPU and CUDA for GPU parallelisation of the algorithm. It is accessible as a python package, that can be installed across different operating systems and devices. A comprehensive documentation and a range of examples are provided for understanding the usage of the package on 2D and 3D data using CPU or GPU. The provided examples include 2D/3D examples for Geodesic, Euclidean, Signed Geodesic distance transform as well as computing Geodesic symmetric filtering (GSF) that is essential first step in implementing interactive segmentation method from :cite:p:`criminisi2008geos`. 41 | 42 | In the following section, more details of each implemented algorithm is presented. 43 | 44 | 45 | Criminisi et al's Parallelisable Generalised Geodesic Distance Transform 46 | ************************************************************************ 47 | We implement both 2D and 3D parallelisable generalised Geodesic distance transform algorithms from :cite:p:`criminisiinteractive` on both CPU (OpenMP) and GPU (CUDA). The 2D algorithm works by computing distance propagation in one row at a time. This is a hard constraint because compute for each row is dependent on computed distances in previous rows from previous invocation. Consider an example of an image with 4 x 6 dimension. Then a successful full iteration of our method would involve going through the rows one by one as follows: 48 | 49 | .. image:: ../../figures/animation_pass2d.gif 50 | 51 | As can be seen, this involves top-down and left-right passes. How we implement left-right is by reusing top-down code and transposing the data instead. Please note that for each step, only the pixels highlighted in green color can be computed (as they have the data available from previous row). It is this row that we split into multiple threads using an underlying hardware (CPU or GPU). This parallelisation enables speed-up as compared to non-parallelisable CPU implementations, e.g. in :cite:p:`geodistk` which implements raster scan algorithm from :cite:p:`toivanen1996new`. 52 | 53 | Going beyond 2D images, we also implement the parallelisable Geodesic distance transform for 3D data. We provide both CPU (OpenMP) and GPU (CUDA) optimised implementations. Our 3D implementation operates on the same principle (we can process one plane at a time). However, in 3D case, since we have more data, we can utilise more compute on GPU and process a plane in parallel, however we still have the data dependency constraints that prevent us from processing all planes together. 54 | 55 | :cite:p:`weber2008parallel` presents a further optimised approach for computing Geodesic distance transforms on GPUs, however this method is protected by multiple patents and hence is not suitable for open-source implementation in **FastGeodis** package. 56 | 57 | 58 | Toivanen et al's Non-parallelisable Generalised Geodesic Distance Transform 59 | *************************************************************************** 60 | In addition to parallelisable algorithm from :cite:p:`criminisiinteractive`, we also implement non-parallelisable Geodesic distance transform method from :cite:p:`toivanen1996new` using CPU. This method is used for comparison of accuracy as well as execution of the parallelised Geodesic distance transform algorithm presented above. 61 | 62 | In this method, a 2D pass operates as a raster scan that is sequentially applied in two passes: forward and backward. Figure below, shows an overview of this method, as described in :cite:p:`criminisi2008geos`: 63 | 64 | 65 | .. figure:: ../../figures/rasterscan_toivanen.png 66 | 67 | Raster scan method from :cite:p:`toivanen1996new`. Figure from :cite:p:`criminisi2008geos`. 68 | 69 | For both forward and backward pass, an L shaped kernel is used in a single raster scan pass to propagate distance in forward and backward direction. In particular, this L shaped kernel along with the sequential pass limits this method to non-parallelisable CPU implementation, which we include in **FastGeodis** for comparison purposes. 70 | 71 | Sethian's Fast Marching-based Non-parallelisable Generalised Geodesic Distance Transform 72 | **************************************************************************************** 73 | In addition to the above methods, we also implement fast marching based non-parallelisable Geodesic distance transform method from :cite:p:`sethian1999fast` using CPU. This method is used for comparison of accuracy as well as execution of the parallelised Geodesic distance transform algorithm presented above. It serves as golden reference as this method provides accuracy distance transform calculation. While being accurate, it is computationally expensive taking orders of magnitude more time and compute. 74 | 75 | Performance Improvements 76 | ################################ 77 | FastGeodis (CPU/GPU) is compared with existing GeodisTK (https://github.com/taigw/GeodisTK) in terms of execution speed as well as accuracy. All our experiments were evaluated on Nvidia GeForce Titan X (12 GB) with 6-Core Intel Xeon E5-1650 CPU. We present our results below: 78 | 79 | Execution 80 | ******************* 81 | The following figures summarise execution speed comparison of **FastGeodis** with :cite:p:`geodistk`. 82 | 83 | .. table:: 84 | :align: center 85 | 86 | +--------------------------------------------+--------------------------------------------+ 87 | | **Execution Speed for 2D images** | **Execution Speed for 3D volumes** | 88 | +--------------------------------------------+--------------------------------------------+ 89 | | .. figure:: ../../figures/experiment_2d.png| .. figure:: ../../figures/experiment_3d.png| 90 | | :alt: 2D | :alt: 3D | 91 | | | | 92 | +--------------------------------------------+--------------------------------------------+ 93 | 94 | The above results are further summarised in tables below, along with calculated speed-ups for **FastGeodis** functions vs :cite:p:`geodistk`: 95 | 96 | .. csv-table:: Execution speed-up on 2D images 97 | :file: experiment2d.csv 98 | :widths: 10, 10, 10, 10, 10, 10 99 | :header-rows: 2 100 | 101 | 102 | .. csv-table:: Execution speed-up on 3D volumes 103 | :file: experiment3d.csv 104 | :widths: 10, 10, 10, 10, 10, 10 105 | :header-rows: 2 106 | 107 | It can be observed that for 2D images, **FastGeodis** leads to a speed-up of upto 20x on CPU and upto 55x on GPU. 108 | For 3D images, **FastGeodis** leads to a speed-up of upto 3x on CPU and upto 74x on GPU. 109 | 110 | Accuracy 111 | ******************* 112 | For accuracy, we use Fast Marching-based implementation as golden reference. These results are visualised below for visual comparison as well as quantitative comparison using joint histograms. 113 | 114 | 2D Image Data 115 | ================ 116 | 117 | .. figure:: ../../figures/fast_marching_compare_2d.png 118 | 119 | .. figure:: ../../figures/fast_marching_compare_2d_jointhist.png 120 | 121 | 3D Image Data 122 | ================== 123 | .. image:: ../../figures/fast_marching_compare_3d.png 124 | 125 | 126 | .. figure:: ../../figures/fast_marching_compare_3d_jointhist.png 127 | 128 | 129 | .. bibliography:: -------------------------------------------------------------------------------- /docs/source/refs.bib: -------------------------------------------------------------------------------- 1 | @article{fabbri20082d, 2 | title={2D Euclidean distance transform algorithms: A comparative survey}, 3 | author={Fabbri, Ricardo and Costa, Luciano Da F and Torelli, Julio C and Bruno, Odemir M}, 4 | journal={ACM Computing Surveys (CSUR)}, 5 | volume={40}, 6 | number={1}, 7 | pages={1--44}, 8 | year={2008}, 9 | publisher={ACM New York, NY, USA} 10 | } 11 | 12 | 13 | @article{wang2018deepigeos, 14 | title = {DeepIGeoS: a deep interactive geodesic framework for medical image segmentation}, 15 | author = {Wang, Guotai and Zuluaga, Maria A and Li, Wenqi and Pratt, Rosalind and Patel, Premal A and Aertsen, Michael and Doel, Tom and David, Anna L and Deprest, Jan and Ourselin, S{\'e}bastien and others}, 16 | journal = {IEEE transactions on pattern analysis and machine intelligence}, 17 | volume = {41}, 18 | number = {7}, 19 | pages = {1559--1572}, 20 | year = {2018}, 21 | publisher = {IEEE} 22 | } 23 | 24 | @inproceedings{criminisi2008geos, 25 | title = {Geos: Geodesic image segmentation}, 26 | author = {Criminisi, Antonio and Sharp, Toby and Blake, Andrew}, 27 | booktitle = {European Conference on Computer Vision}, 28 | pages = {99--112}, 29 | year = {2008}, 30 | organization = {Springer}, 31 | doi = {10.1007/978-3-540-88682-2_9} 32 | } 33 | 34 | @article{weber2008parallel, 35 | title = {Parallel algorithms for approximation of distance maps on parametric surfaces}, 36 | author = {Weber, Ofir and Devir, Yohai S and Bronstein, Alexander M and Bronstein, Michael M and Kimmel, Ron}, 37 | journal = {ACM Transactions on Graphics (TOG)}, 38 | volume = {27}, 39 | number = {4}, 40 | pages = {1--16}, 41 | year = {2008}, 42 | publisher = {ACM New York, NY, USA}, 43 | doi = {10.1145/1409625.1409626} 44 | } 45 | 46 | @misc{geodistk, 47 | author = {Wang, Guotai}, 48 | title = {GeodisTK: Geodesic Distance Transform Toolkit for 2D and 3D Images}, 49 | year = {2020}, 50 | publisher = {GitHub}, 51 | journal = {GitHub repository}, 52 | url = {https://github.com/taigw/GeodisTK} 53 | } 54 | 55 | @misc{eucildeantdimpl, 56 | author = {Seung-Lab, }, 57 | title = {Multi-Label Anisotropic 3D Euclidean Distance Transform (MLAEDT-3D)}, 58 | year = {2018}, 59 | publisher = {GitHub}, 60 | journal = {GitHub repository}, 61 | url = {https://github.com/seung-lab/euclidean-distance-transform-3d} 62 | } 63 | 64 | 65 | @article{asad2022econet, 66 | title = {ECONet: Efficient Convolutional Online Likelihood Network for Scribble-based Interactive Segmentation}, 67 | author = {Asad, Muhammad and Fidon, Lucas and Vercauteren, Tom}, 68 | journal = {arXiv preprint arXiv:2201.04584}, 69 | year = {2022} 70 | } 71 | 72 | @incollection{NEURIPS2019_9015, 73 | title = {PyTorch: An Imperative Style, High-Performance Deep Learning Library}, 74 | author = {Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and Kopf, Andreas and Yang, Edward and DeVito, Zachary and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith}, 75 | booktitle = {Advances in Neural Information Processing Systems 32}, 76 | editor = {H. Wallach and H. Larochelle and A. Beygelzimer and F. d\textquotesingle Alch\'{e}-Buc and E. Fox and R. Garnett}, 77 | pages = {8024--8035}, 78 | year = {2019}, 79 | publisher = {Curran Associates, Inc.}, 80 | url = {http://papers.neurips.cc/paper/9015-pytorch-an-imperative-style-high-performance-deep-learning-library.pdf} 81 | } 82 | 83 | @inproceedings{criminisiinteractive, 84 | author = {Criminisi, Antonio and Sharp, Toby and Siddiqui, Khan}, 85 | title = {Interactive Geodesic Segmentation of n-Dimensional Medical Images on the Graphics Processor}, 86 | booktitle = {Radiological Society of North America (RSNA)}, 87 | year = {2009}, 88 | month = {December}, 89 | abstract = {This paper presents a new, parallel segmentation algorithm which enables radiologists to separate a region of interest from 2D or 3D images accurately and efficiently. This, in turn, enables fast and accurate area/volume measurements as well as the extraction of statistics for the selected region. Our general purpose algorithm can be applied to any visible structure (e.g. a tumor or any structure) and it is driven by minimal and intuitive user interaction.}, 90 | url = {https://www.microsoft.com/en-us/research/publication/interactive-geodesic-segmentation-of-n-dimensional-medical-images-on-the-graphics-processor/}, 91 | edition = {Radiological Society of North America (RSNA)} 92 | } 93 | 94 | @article{felzenszwalb2012distance, 95 | title = {Distance transforms of sampled functions}, 96 | author = {Felzenszwalb, Pedro F and Huttenlocher, Daniel P}, 97 | journal = {Theory of computing}, 98 | volume = {8}, 99 | number = {1}, 100 | pages = {415--428}, 101 | year = {2012}, 102 | publisher = {Theory of Computing Exchange} 103 | } 104 | 105 | @article{toivanen1996new, 106 | title = {New geodosic distance transforms for gray-scale images}, 107 | author = {Toivanen, Pekka J}, 108 | journal = {Pattern Recognition Letters}, 109 | volume = {17}, 110 | number = {5}, 111 | pages = {437--450}, 112 | year = {1996}, 113 | publisher = {Elsevier}, 114 | doi = {10.1016/0167-8655(96)00010-4} 115 | } 116 | 117 | @misc{tensorflow2015-whitepaper, 118 | title = { {TensorFlow}: Large-Scale Machine Learning on Heterogeneous Systems}, 119 | url = {https://www.tensorflow.org/}, 120 | note = {Software available from tensorflow.org}, 121 | author = { 122 | Mart\'{i}n~Abadi and 123 | Ashish~Agarwal and 124 | Paul~Barham and 125 | Eugene~Brevdo and 126 | Zhifeng~Chen and 127 | Craig~Citro and 128 | Greg~S.~Corrado and 129 | Andy~Davis and 130 | Jeffrey~Dean and 131 | Matthieu~Devin and 132 | Sanjay~Ghemawat and 133 | Ian~Goodfellow and 134 | Andrew~Harp and 135 | Geoffrey~Irving and 136 | Michael~Isard and 137 | Yangqing Jia and 138 | Rafal~Jozefowicz and 139 | Lukasz~Kaiser and 140 | Manjunath~Kudlur and 141 | Josh~Levenberg and 142 | Dandelion~Man\'{e} and 143 | Rajat~Monga and 144 | Sherry~Moore and 145 | Derek~Murray and 146 | Chris~Olah and 147 | Mike~Schuster and 148 | Jonathon~Shlens and 149 | Benoit~Steiner and 150 | Ilya~Sutskever and 151 | Kunal~Talwar and 152 | Paul~Tucker and 153 | Vincent~Vanhoucke and 154 | Vijay~Vasudevan and 155 | Fernanda~Vi\'{e}gas and 156 | Oriol~Vinyals and 157 | Pete~Warden and 158 | Martin~Wattenberg and 159 | Martin~Wicke and 160 | Yuan~Yu and 161 | Xiaoqiang~Zheng}, 162 | year = {2015} 163 | } 164 | 165 | @article{sethian1999fast, 166 | title={Fast marching methods}, 167 | author={Sethian, James A}, 168 | journal={SIAM review}, 169 | volume={41}, 170 | number={2}, 171 | pages={199--235}, 172 | year={1999}, 173 | publisher={SIAM} 174 | } -------------------------------------------------------------------------------- /docs/source/usage_examples.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Usage Examples 3 | ***************** 4 | 5 | **FastGeodis** package includes a number of examples that demonstrate its usage for 2D and 3D data use-cases. Here we provide details of these examples and how to setup and run them. 6 | 7 | 8 | Simplest Examples 9 | ################################### 10 | 11 | For a given 2D image, **FastGeodis** can be used to get Geodesic distance transform as: 12 | :: 13 | 14 | device = "cpu" 15 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 16 | 17 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 18 | image_pt = image_pt.to(device) 19 | mask_pt = torch.ones_like(image_pt) 20 | mask_pt[..., 100, 100] = 0 21 | 22 | v = 1e10 23 | # lamb = 0.0 (Euclidean) or 1.0 (Geodesic) or (0.0, 1.0) (mixture) 24 | lamb = 1.0 25 | iterations = 2 26 | geodesic_dist = FastGeodis.generalised_geodesic2d( 27 | image_pt, mask_pt, v, lamb, iterations 28 | ) 29 | 30 | For a given 3D image volume, **FastGeodis** can be used to get Geodesic distance transform as: 31 | :: 32 | 33 | device = "cuda" if torch.cuda.is_available() else "cpu" 34 | 35 | image_sitk = sitk.ReadImage("data/img3d.nii.gz") 36 | image = sitk.GetArrayFromImage(image_sitk) 37 | spacing_raw = image_sitk.GetSpacing()[::-1] 38 | 39 | image = np.asarray(image, np.float32) 40 | mask = np.zeros_like(image, np.float32) 41 | mask[30, 120, 160] = 1 42 | 43 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 44 | mask_pt = torch.from_numpy(1 - mask.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 45 | 46 | image_pt = image_pt.to(device) 47 | mask_pt = mask_pt.to(device) 48 | 49 | v = 1e10 50 | iterations = 2 51 | # lamb = 0.0 (Euclidean) or 1.0 (Geodesic) or (0.0, 1.0) (mixture) 52 | lamb = 1.0 53 | geodesic_dist = FastGeodis.generalised_geodesic3d( 54 | image_pt, mask_pt, spacing, v, lamb, iterations 55 | ) 56 | 57 | To access complete examples, refer to Simple 2D/3D examples in table below. 58 | 59 | Note: the above example execute using CPU with :code:`device = "cpu"`. To change execution device to GPU use :code:`device="cuda"`. 60 | 61 | Available Examples 62 | ################################### 63 | 64 | A number of examples are provided, which can be found in `./samples `_ folder. 65 | The examples are provided as local python (.py) files, local Jupyter (.ipynb) notebooks and cloud Colab (.ipynb) notebooks. 66 | 67 | Running Examples Locally 68 | ------------------------ 69 | 70 | To run locally, clone github repository and navigate to `./samples `_ directory as: 71 | :: 72 | 73 | git clone https://github.com/masadcv/FastGeodis 74 | cd FastGeodis/samples 75 | 76 | and run an example as: 77 | :: 78 | 79 | python demo2d.py 80 | 81 | 82 | Running Examples in Colab 83 | ------------------------- 84 | 85 | To setup and run Colab notebooks in the cloud, follow the **Colab** links below. 86 | 87 | 88 | List of Example Scripts 89 | ----------------------- 90 | The table below gives an exhaustive list of all available demo examples. It includes relevant links to Python as well as Colab versions of the same demo. 91 | 92 | +------------------------------------------------+----------------------+----------------------------+ 93 | | Description | Python Link | Colab Link | 94 | +================================================+======================+============================+ 95 | | Simple 2D Geodesic & Euclidean Distance | SimpleDemo2dPy_ | SimpleDemo2dColab_ | 96 | +------------------------------------------------+----------------------+----------------------------+ 97 | | Simple Signed 2D Geodesic & Euclidean Distance | SimpleDemo2dSignedPy_| SimpleDemo2dSignedColab_ | 98 | +------------------------------------------------+----------------------+----------------------------+ 99 | | Simple 3D Geodesic & Euclidean Distance | SimpleDemo3dPy_ | SimpleDemo3dColab_ | 100 | +------------------------------------------------+----------------------+----------------------------+ 101 | | Simple Signed 3D Geodesic & Euclidean Distance | SimpleDemo3dSignedPy_| SimpleDemo3dSignedColab_ | 102 | +------------------------------------------------+----------------------+----------------------------+ 103 | | 2D Geodesic & Euclidean Distance | Demo2dPy_ | Demo2dColab_ | 104 | +------------------------------------------------+----------------------+----------------------------+ 105 | | 2D Signed Geodesic & Euclidean Distance | Demo2dSignedPy_ | Demo2dSignedColab_ | 106 | +------------------------------------------------+----------------------+----------------------------+ 107 | | 3D Geodesic & Euclidean Distance | Demo3dPy_ | Demo3dColab_ | 108 | +------------------------------------------------+----------------------+----------------------------+ 109 | | 3D Signed Geodesic & Euclidean Distance | Demo3dSignedPy_ | Demo3dSignedColab_ | 110 | +------------------------------------------------+----------------------+----------------------------+ 111 | | 2D 2D GSF Segmentation Smoothing | DemoGSF2dPy_ | DemoGSF2dColab_ | 112 | +------------------------------------------------+----------------------+----------------------------+ 113 | 114 | .. _SimpleDemo2dPy: https://github.com/masadcv/FastGeodis/blob/master/samples/simpledemo2d.py 115 | .. _SimpleDemo2dColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/simpledemo2d.ipynb 116 | .. _SimpleDemo2dSignedPy: https://github.com/masadcv/FastGeodis/blob/master/samples/simpledemo2d_signed.py 117 | .. _SimpleDemo2dSignedColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/simpledemo2d_signed.ipynb 118 | .. _SimpleDemo3dPy: https://github.com/masadcv/FastGeodis/blob/master/samples/simpledemo3d.py 119 | .. _SimpleDemo3dColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/simpledemo3d.ipynb 120 | .. _SimpleDemo3dSignedPy: https://github.com/masadcv/FastGeodis/blob/master/samples/simpledemo3d_signed.py 121 | .. _SimpleDemo3dSignedColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/simpledemo3d_signed.ipynb 122 | .. _Demo2dPy: https://github.com/masadcv/FastGeodis/blob/master/samples/demo2d.py 123 | .. _Demo2dSignedPy: https://github.com/masadcv/FastGeodis/blob/master/samples/demo2d_signed.py 124 | .. _Demo2dColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/demo2d.ipynb 125 | .. _Demo2dSignedColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/demo2d_signed.ipynb 126 | .. _Demo3dPy: https://github.com/masadcv/FastGeodis/blob/master/samples/demo3d.py 127 | .. _Demo3dSignedPy: https://github.com/masadcv/FastGeodis/blob/master/samples/demo3d_signed.py 128 | .. _Demo3dColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/demo3d.ipynb 129 | .. _Demo3dSignedColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/demo3d_signed.ipynb 130 | .. _DemoGSF2dPy: https://github.com/masadcv/FastGeodis/blob/master/samples/demoGSF2d_SmoothingSegExample.ipynb 131 | .. _DemoGSF2dColab: https://colab.research.google.com/github/masadcv/FastGeodis/blob/master/samples/demoGSF2d_SmoothingSegExample.ipynb 132 | -------------------------------------------------------------------------------- /figures/3d_axial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/3d_axial.png -------------------------------------------------------------------------------- /figures/3d_coronal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/3d_coronal.png -------------------------------------------------------------------------------- /figures/FastGeodis2D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/FastGeodis2D.png -------------------------------------------------------------------------------- /figures/FastGeodis3D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/FastGeodis3D.png -------------------------------------------------------------------------------- /figures/animation_pass2d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/animation_pass2d.gif -------------------------------------------------------------------------------- /figures/experiment_2d.json: -------------------------------------------------------------------------------- 1 | { 2 | "generalised_geodesic_distance_2d": [ 3 | 0.0040008783340454105, 4 | 0.015547347068786622, 5 | 0.06411871910095215, 6 | 0.2503993511199951, 7 | 1.0058921337127686, 8 | 4.0167663335800174 9 | ], 10 | "generalised_geodesic2d_raster_cpu": [ 11 | 0.0017780303955078126, 12 | 0.002790236473083496, 13 | 0.007051777839660644, 14 | 0.01872894763946533, 15 | 0.0458099365234375, 16 | 0.18565030097961427 17 | ], 18 | "generalised_geodesic2d_raster_gpu": [ 19 | 0.0024535417556762694, 20 | 0.00487065315246582, 21 | 0.009613299369812011, 22 | 0.01735057830810547, 23 | 0.03459699153900146, 24 | 0.07159698009490967 25 | ], 26 | "spatial_dim": [ 27 | 64, 28 | 128, 29 | 256, 30 | 512, 31 | 1024, 32 | 2048 33 | ] 34 | } -------------------------------------------------------------------------------- /figures/experiment_2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/experiment_2d.png -------------------------------------------------------------------------------- /figures/experiment_2d_toivanen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/experiment_2d_toivanen.png -------------------------------------------------------------------------------- /figures/experiment_3d.json: -------------------------------------------------------------------------------- 1 | { 2 | "generalised_geodesic_distance_3d": [ 3 | 0.13098487854003907, 4 | 1.094784927368164, 5 | 8.602601528167725, 6 | 69.08085768222809 7 | ], 8 | "generalised_geodesic3d_raster_cpu": [ 9 | 0.03932678699493408, 10 | 0.30489311218261717, 11 | 2.5349778652191164, 12 | 22.241712117195128 13 | ], 14 | "generalised_geodesic3d_raster_gpu": [ 15 | 0.007239127159118652, 16 | 0.01783447265625, 17 | 0.12442941665649414, 18 | 0.9299801826477051 19 | ], 20 | "spatial_dim": [ 21 | 64, 22 | 128, 23 | 256, 24 | 512 25 | ] 26 | } -------------------------------------------------------------------------------- /figures/experiment_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/experiment_3d.png -------------------------------------------------------------------------------- /figures/experiment_3d_toivanen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/experiment_3d_toivanen.png -------------------------------------------------------------------------------- /figures/fast_marching_compare_2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/fast_marching_compare_2d.png -------------------------------------------------------------------------------- /figures/fast_marching_compare_2d_jointhist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/fast_marching_compare_2d_jointhist.png -------------------------------------------------------------------------------- /figures/fast_marching_compare_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/fast_marching_compare_3d.png -------------------------------------------------------------------------------- /figures/fast_marching_compare_3d_jointhist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/fast_marching_compare_3d_jointhist.png -------------------------------------------------------------------------------- /figures/rasterscan_toivanen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/figures/rasterscan_toivanen.png -------------------------------------------------------------------------------- /paper/FastGeodis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masadcv/FastGeodis/0f6cc1e6e104bee56e6eb9dfdd2cfd29c21a1a3e/paper/FastGeodis.png -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{wang2018deepigeos, 2 | title = {DeepIGeoS: a deep interactive geodesic framework for medical image segmentation}, 3 | author = {Wang, Guotai and Zuluaga, Maria A and Li, Wenqi and Pratt, Rosalind and Patel, Premal A and Aertsen, Michael and Doel, Tom and David, Anna L and Deprest, Jan and Ourselin, S{\'e}bastien and others}, 4 | journal = {IEEE transactions on pattern analysis and machine intelligence}, 5 | volume = {41}, 6 | number = {7}, 7 | pages = {1559--1572}, 8 | year = {2018}, 9 | publisher = {IEEE} 10 | } 11 | 12 | @inproceedings{criminisi2008geos, 13 | title = {Geos: Geodesic image segmentation}, 14 | author = {Criminisi, Antonio and Sharp, Toby and Blake, Andrew}, 15 | booktitle = {European Conference on Computer Vision}, 16 | pages = {99--112}, 17 | year = {2008}, 18 | organization = {Springer}, 19 | doi = {10.1007/978-3-540-88682-2_9} 20 | } 21 | 22 | @article{weber2008parallel, 23 | title = {Parallel algorithms for approximation of distance maps on parametric surfaces}, 24 | author = {Weber, Ofir and Devir, Yohai S and Bronstein, Alexander M and Bronstein, Michael M and Kimmel, Ron}, 25 | journal = {ACM Transactions on Graphics (TOG)}, 26 | volume = {27}, 27 | number = {4}, 28 | pages = {1--16}, 29 | year = {2008}, 30 | publisher = {ACM New York, NY, USA}, 31 | doi = {10.1145/1409625.1409626} 32 | } 33 | 34 | @misc{geodistk, 35 | author = {Wang, Guotai}, 36 | title = {GeodisTK: Geodesic Distance Transform Toolkit for 2D and 3D Images}, 37 | year = {2020}, 38 | publisher = {GitHub}, 39 | journal = {GitHub repository}, 40 | url = {https://github.com/taigw/GeodisTK} 41 | } 42 | 43 | @misc{eucildeantdimpl, 44 | author = {Seung-Lab, }, 45 | title = {Multi-Label Anisotropic 3D {E}uclidean Distance Transform (MLAEDT-3D)}, 46 | year = {2018}, 47 | publisher = {GitHub}, 48 | journal = {GitHub repository}, 49 | url = {https://github.com/seung-lab/euclidean-distance-transform-3d} 50 | } 51 | 52 | 53 | @article{asad2022econet, 54 | title = {ECONet: Efficient Convolutional Online Likelihood Network for Scribble-based Interactive Segmentation}, 55 | author = {Asad, Muhammad and Fidon, Lucas and Vercauteren, Tom}, 56 | journal = {arXiv preprint arXiv:2201.04584}, 57 | year = {2022} 58 | } 59 | 60 | @incollection{NEURIPS2019_9015, 61 | title = {PyTorch: An Imperative Style, High-Performance Deep Learning Library}, 62 | author = {Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and Kopf, Andreas and Yang, Edward and DeVito, Zachary and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith}, 63 | booktitle = {Advances in Neural Information Processing Systems 32}, 64 | editor = {H. Wallach and H. Larochelle and A. Beygelzimer and F. d\textquotesingle Alch\'{e}-Buc and E. Fox and R. Garnett}, 65 | pages = {8024--8035}, 66 | year = {2019}, 67 | publisher = {Curran Associates, Inc.}, 68 | url = {http://papers.neurips.cc/paper/9015-pytorch-an-imperative-style-high-performance-deep-learning-library.pdf} 69 | } 70 | 71 | @inproceedings{criminisiinteractive, 72 | author = {Criminisi, Antonio and Sharp, Toby and Siddiqui, Khan}, 73 | title = {Interactive {G}eodesic Segmentation of n-Dimensional Medical Images on the Graphics Processor}, 74 | booktitle = {Radiological Society of North America (RSNA)}, 75 | year = {2009}, 76 | month = {December}, 77 | abstract = {This paper presents a new, parallel segmentation algorithm which enables radiologists to separate a region of interest from 2D or 3D images accurately and efficiently. This, in turn, enables fast and accurate area/volume measurements as well as the extraction of statistics for the selected region. Our general purpose algorithm can be applied to any visible structure (e.g. a tumor or any structure) and it is driven by minimal and intuitive user interaction.}, 78 | url = {https://www.microsoft.com/en-us/research/publication/interactive-geodesic-segmentation-of-n-dimensional-medical-images-on-the-graphics-processor/}, 79 | edition = {Radiological Society of North America (RSNA)} 80 | } 81 | 82 | @article{felzenszwalb2012distance, 83 | title = {Distance transforms of sampled functions}, 84 | author = {Felzenszwalb, Pedro F and Huttenlocher, Daniel P}, 85 | journal = {Theory of computing}, 86 | volume = {8}, 87 | number = {1}, 88 | pages = {415--428}, 89 | year = {2012}, 90 | publisher = {Theory of Computing Exchange} 91 | } 92 | 93 | @article{toivanen1996new, 94 | title = {New geodosic distance transforms for gray-scale images}, 95 | author = {Toivanen, Pekka J}, 96 | journal = {Pattern Recognition Letters}, 97 | volume = {17}, 98 | number = {5}, 99 | pages = {437--450}, 100 | year = {1996}, 101 | publisher = {Elsevier}, 102 | doi = {10.1016/0167-8655(96)00010-4} 103 | } 104 | 105 | @misc{tensorflow2015-whitepaper, 106 | title = { {TensorFlow}: Large-Scale Machine Learning on Heterogeneous Systems}, 107 | url = {https://www.tensorflow.org/}, 108 | note = {Software available from tensorflow.org}, 109 | author = { 110 | Mart\'{i}n~Abadi and 111 | Ashish~Agarwal and 112 | Paul~Barham and 113 | Eugene~Brevdo and 114 | Zhifeng~Chen and 115 | Craig~Citro and 116 | Greg~S.~Corrado and 117 | Andy~Davis and 118 | Jeffrey~Dean and 119 | Matthieu~Devin and 120 | Sanjay~Ghemawat and 121 | Ian~Goodfellow and 122 | Andrew~Harp and 123 | Geoffrey~Irving and 124 | Michael~Isard and 125 | Yangqing Jia and 126 | Rafal~Jozefowicz and 127 | Lukasz~Kaiser and 128 | Manjunath~Kudlur and 129 | Josh~Levenberg and 130 | Dandelion~Man\'{e} and 131 | Rajat~Monga and 132 | Sherry~Moore and 133 | Derek~Murray and 134 | Chris~Olah and 135 | Mike~Schuster and 136 | Jonathon~Shlens and 137 | Benoit~Steiner and 138 | Ilya~Sutskever and 139 | Kunal~Talwar and 140 | Paul~Tucker and 141 | Vincent~Vanhoucke and 142 | Vijay~Vasudevan and 143 | Fernanda~Vi\'{e}gas and 144 | Oriol~Vinyals and 145 | Pete~Warden and 146 | Martin~Wattenberg and 147 | Martin~Wicke and 148 | Yuan~Yu and 149 | Xiaoqiang~Zheng}, 150 | year = {2015} 151 | } 152 | 153 | @misc{bronstein2013parallel, 154 | title={Parallel approximation of distance maps (US Patent 8,373,716)}, 155 | author={Bronstein, Alexander and Bronstein, Michael and Devir, Yohai and Weber, Ofir and Kimmel, Ron}, 156 | year={2013}, 157 | month=feb # "~12", 158 | publisher={Google Patents}, 159 | note={US Patent 8,373,716} 160 | } 161 | 162 | @misc{bronstein2015parallel, 163 | title={Parallel approximation of distance maps (US Patent 8,982,142)}, 164 | author={Bronstein, Alexander and Bronstein, Michael and Kimmel, Ron and Devir, Yohai and Weber, Ofir}, 165 | year={2015}, 166 | month=mar # "~17", 167 | publisher={Google Patents}, 168 | note={US Patent 8,982,142} 169 | } 170 | 171 | @misc{bronstein2016parallel, 172 | title={Parallel approximation of distance maps (US Patent 9,489,708)}, 173 | author={Bronstein, Alexander and Bronstein, Michael and Kimmel, Ron and Devir, Yohai and Weber, Ofir}, 174 | year={2016}, 175 | month=nov # "~8", 176 | publisher={Google Patents}, 177 | note={US Patent 9,489,708} 178 | } 179 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'FastGeodis: Fast Generalised Geodesic Distance Transform' 3 | tags: 4 | - Python 5 | - PyTorch 6 | - Deep Learning 7 | - Medical Imaging 8 | - Distance Transform 9 | authors: 10 | - name: Muhammad Asad 11 | orcid: 0000-0002-3672-2414 12 | affiliation: 1 13 | corresponding: true 14 | - name: Reuben Dorent 15 | orcid: 0000-0002-7530-0644 16 | affiliation: 1 17 | - name: Tom Vercauteren 18 | orcid: 0000-0003-1794-0456 19 | affiliation: 1 20 | affiliations: 21 | - name: School of Biomedical Engineering & Imaging Sciences, King’s College London, UK 22 | index: 1 23 | date: 23 June 2022 24 | bibliography: paper.bib 25 | --- 26 | 27 | # Summary 28 | 29 | 30 | Geodesic and Euclidean distance transforms have been widely used in a number of applications where distance from a set of reference points is computed. Methods from recent years have shown effectiveness in applying the Geodesic distance transform to interactively annotate 3D medical imaging data [@wang2018deepigeos; @criminisi2008geos]. The Geodesic distance transform enables providing segmentation labels, i.e., voxel-wise labels, for different objects of interests. Despite existing methods for efficient computation of the Geodesic distance transform on GPU and CPU devices [@criminisiinteractive; @criminisi2008geos; @weber2008parallel; @toivanen1996new], an open-source implementation of such methods on the GPU does not exist. 31 | On the contrary, efficient methods for the computation of the Euclidean distance transform [@felzenszwalb2012distance] have open-source implementations [@tensorflow2015-whitepaper; @eucildeantdimpl]. Existing libraries, e.g., @geodistk, provide C++ implementations of the Geodesic distance transform; however, they lack efficient utilisation of the underlying hardware and hence result in significant computation time, especially when applying them on 3D medical imaging volumes. 32 | 33 | The `FastGeodis` package provides an efficient implementation for computing Geodesic and Euclidean distance transforms (or a mixture of both), targeting efficient utilisation of CPU and GPU hardware. In particular, it implements the paralellisable raster scan method from @criminisiinteractive, where elements in a row (2D) or plane (3D) can be computed with parallel threads. This package is able to handle 2D as well as 3D data, where it achieves up to a 20x speedup on a CPU and up to a 74x speedup on a GPU as compared to an existing open-source library [@geodistk] that uses a non-parallelisable single-thread CPU implementation. The performance speedups reported here were evaluated using 3D volume data on an Nvidia GeForce Titan X (12 GB) with a 6-Core Intel Xeon E5-1650 CPU. Further in-depth comparison of performance improvements is discussed in the `FastGeodis` \href{https://fastgeodis.readthedocs.io/}{documentation}. 34 | 35 | # Statement of need 36 | 37 | Despite existing open-source implementation of distance transforms [@tensorflow2015-whitepaper; @eucildeantdimpl; @geodistk], open-source implementations of efficient Geodesic distance transform algorithms [@criminisiinteractive; @weber2008parallel] on CPUs and GPUs do not exist. However, efficient CPU [@eucildeantdimpl] and GPU [@tensorflow2015-whitepaper] implementations exist for Euclidean distance transform. To the best of our knowledge, `FastGeodis` is the first open-source implementation of efficient the Geodesic distance transform [@criminisiinteractive], achieving up to a 20x speedup on a CPU and up to a 74x speedup on a GPU as compared to existing open-source libraries [@geodistk]. It also provides an efficient implementation of the Euclidean distance transform. In addition, it is the first open-source implementation of generalised Geodesic distance transform and Geodesic Symmetric Filtering (GSF) as proposed in @criminisi2008geos. Apart from a method from @criminisiinteractive, @weber2008parallel present a further optimised approach for computing Geodesic distance transforms on GPUs. However, this method is protected by multiple patents [@bronstein2013parallel; @bronstein2015parallel; @bronstein2016parallel] and hence is not suitable for open-source implementation in the **FastGeodis** package. 38 | 39 | 40 | The ability to efficiently compute Geodesic and Euclidean distance transforms can significantly enhance distance transform applications, especially for training deep learning models that utilise distance transforms [@wang2018deepigeos]. It will improve prototyping, experimentation, and deployment of such methods, where efficient computation of distance transforms has been a limiting factor. In 3D medical imaging problems, efficient computation of distance transforms will lead to significant speed-ups, enabling online learning applications for better processing/labelling/inference from volumetric datasets [@asad2022econet]. In addition, `FastGeodis` provides an efficient implementation for both CPUs and GPUs and hence will enable efficient use of a wide range of hardware devices. 41 | 42 | 43 | # Implementation 44 | 45 | 46 | `FastGeodis` implements an efficient distance transform algorithm from @criminisiinteractive, which provides parallelisable raster scans to compute distance transform. The implementation consists of data propagation passes that are parallelised using threads for elements across a line (2D) or plane (3D). \autoref{fig:hwpasses} shows these data propagation passes, where each pass consists of computing distance values for the next row (2D) or plane (3D) by utilising parallel threads and data from the previous row/plane, hence resulting in propagating distance values along the direction of the pass. For 2D data, four distance propagation passes are required, top-bottom, bottom-top, left-right and right-left, whereas for 3D data six passes are required, front-back, back-front, top-bottom, bottom-top, left-right and right-left. The algorithm can be applied to efficiently compute both Geodesic and Euclidean distance transforms. In addition to this, `FastGeodis` also provides the non-parallelisable raster scan based distance transform method from @toivanen1996new, which is implemented using a single CPU thread and used for comparison. 47 | 48 | 49 | The `FastGeodis` package is implemented using `PyTorch` [@NEURIPS2019_9015], utilising OpenMP for CPU- and CUDA for GPU-parallelisation of the algorithm. It is accessible as a Python package that can be installed across different operating systems and devices. Comprehensive documentation and a range of examples are provided for understanding the usage of the package on 2D and 3D data using CPUs or GPUs. Two- and three-dimensional examples are provided for Geodesic, Euclidean, and Signed Geodesic distance transforms as well as for computing Geodesic Symmetric Filtering (GSF), the essential first step in implementing the interactive segmentation method described in @criminisi2008geos. A further in-depth overview of the implemented algorithm, along with evaluation on common 2D/3D data input sizes, is provided in the `FastGeodis` \href{https://fastgeodis.readthedocs.io/}{documentation}. 50 | 51 | 52 | 53 | ![Raster scan data propagation passes in FastGeodis.\label{fig:hwpasses}](FastGeodis.png){ width=80% } 54 | 55 | # Acknowledgements 56 | 57 | This research was supported by the European Union’s Horizon 2020 research and innovation programme under grant agreement No 101016131. 58 | 59 | # References 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires=[ 3 | "setuptools", 4 | "torch>=1.5.0", 5 | "wheel" 6 | ] 7 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | numpy>=1.19.2 3 | matplotlib>=3.2.0 4 | parameterized>=0.7.0 5 | SimpleITK>=2.0 6 | ninja>=1.10.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch>=1.5.0 -------------------------------------------------------------------------------- /samples/demo2d.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import FastGeodis 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import torch 7 | from PIL import Image 8 | 9 | 10 | def evaluate_geodesic_distance2d(image, seed_pos): 11 | SHOW_JOINT_HIST = False 12 | # get image and create seed image 13 | input_image = np.asanyarray(image, np.float32) 14 | Seed = np.zeros((input_image.shape[0], input_image.shape[1]), np.float32) 15 | Seed[seed_pos[0]][seed_pos[1]] = 1 16 | 17 | # run and time each method 18 | iterations = 2 19 | v = 1e10 20 | lamb = 1.0 21 | 22 | if input_image.ndim == 3: 23 | input_image = np.moveaxis(input_image, -1, 0) 24 | else: 25 | input_image = np.expand_dims(input_image, 0) 26 | 27 | device = "cpu" 28 | input_image_pt = torch.from_numpy(input_image).unsqueeze_(0).to(device) 29 | seed_image_pt = ( 30 | torch.from_numpy(1 - Seed.astype(np.float32)) 31 | .unsqueeze_(0) 32 | .unsqueeze_(0) 33 | .to(device) 34 | ) 35 | 36 | tic = time.time() 37 | fastmarch_output = np.squeeze( 38 | FastGeodis.geodesic2d_fastmarch( 39 | input_image_pt, seed_image_pt, lamb 40 | ) 41 | .cpu() 42 | .numpy() 43 | ) 44 | fastmarch_time = time.time() - tic 45 | 46 | tic = time.time() 47 | toivanenraster_output = np.squeeze( 48 | FastGeodis.generalised_geodesic2d_toivanen( 49 | input_image_pt, seed_image_pt, v, lamb, iterations 50 | ) 51 | .cpu() 52 | .numpy() 53 | ) 54 | toivanenraster_time = time.time() - tic 55 | 56 | tic = time.time() 57 | fastraster_output_cpu = np.squeeze( 58 | FastGeodis.generalised_geodesic2d( 59 | input_image_pt, seed_image_pt, v, lamb, iterations 60 | ) 61 | .cpu() 62 | .numpy() 63 | ) 64 | fastraster_time_cpu = time.time() - tic 65 | 66 | device = "cuda" if torch.cuda.is_available() else None 67 | if device: 68 | input_image_pt = input_image_pt.to(device) 69 | seed_image_pt = seed_image_pt.to(device) 70 | 71 | tic = time.time() 72 | fastraster_output_gpu = np.squeeze( 73 | FastGeodis.generalised_geodesic2d( 74 | input_image_pt, seed_image_pt, v, lamb, iterations 75 | ) 76 | .cpu() 77 | .numpy() 78 | ) 79 | fastraster_time_gpu = time.time() - tic 80 | 81 | print("Runtimes:") 82 | print( 83 | "Fast Marching CPU: {:.6f} s \nToivanen's CPU raster: {:.6f} s \nFastGeodis CPU raster: {:.6f} s".format( 84 | fastmarch_time, toivanenraster_time, fastraster_time_cpu 85 | ) 86 | ) 87 | 88 | if device: 89 | print("FastGeodis GPU raster: {:.6f} s".format(fastraster_time_gpu)) 90 | 91 | plt.figure(figsize=(18, 6)) 92 | plt.subplot(2, 4, 1) 93 | plt.imshow(image, cmap="gray") 94 | plt.autoscale(False) 95 | plt.plot([seed_pos[0]], [seed_pos[1]], "ro") 96 | plt.axis("off") 97 | plt.title("(a) Input image") 98 | 99 | plt.subplot(2, 4, 5) 100 | plt.imshow(fastmarch_output) 101 | plt.axis("off") 102 | plt.title("(b) Fast Marching (cpu) | ({:.4f} s)".format(fastmarch_time)) 103 | 104 | 105 | plt.subplot(2, 4, 2) 106 | plt.imshow(toivanenraster_output) 107 | plt.axis("off") 108 | plt.title("(c) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 109 | 110 | plt.subplot(2, 4, 3) 111 | plt.imshow(fastraster_output_cpu) 112 | plt.axis("off") 113 | plt.title("(e) FastGeodis (cpu) | ({:.4f} s)".format(fastraster_time_cpu)) 114 | 115 | plt.subplot(2, 4, 6) 116 | plt.imshow(toivanenraster_output) 117 | plt.axis("off") 118 | plt.title("(d) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 119 | 120 | if device: 121 | plt.subplot(2, 4, 7) 122 | plt.imshow(fastraster_output_gpu) 123 | plt.axis("off") 124 | plt.title("(f) FastGeodis (gpu) | ({:.4f} s)".format(fastraster_time_gpu)) 125 | 126 | diff = ( 127 | abs(fastmarch_output - fastraster_output_cpu) 128 | / (fastmarch_output + 1e-7) 129 | * 100 130 | ) 131 | plt.subplot(2, 4, 4) 132 | plt.imshow(diff) 133 | plt.axis("off") 134 | plt.title( 135 | "(g) Fast Marching vs. FastGeodis (cpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 136 | np.max(diff), np.min(diff) 137 | ) 138 | ) 139 | 140 | if device: 141 | diff = ( 142 | abs(fastmarch_output - fastraster_output_gpu) 143 | / (fastmarch_output + 1e-7) 144 | * 100 145 | ) 146 | plt.subplot(2, 4, 8) 147 | plt.imshow(diff) 148 | plt.axis("off") 149 | plt.title( 150 | "(h) Fast Marching vs. FastGeodis (gpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 151 | np.max(diff), np.min(diff) 152 | ) 153 | ) 154 | 155 | # plt.colorbar() 156 | plt.show() 157 | 158 | if SHOW_JOINT_HIST: 159 | plt.figure(figsize=(12, 6)) 160 | plt.subplot(1, 3, 1) 161 | plt.title("Joint histogram\nFast Marching (cpu) vs. Toivanen's Raster (cpu)") 162 | plt.hist2d( 163 | fastmarch_output.flatten(), toivanenraster_output.flatten(), bins=50 164 | ) 165 | plt.xlabel("Fast Marching (cpu)") 166 | plt.ylabel("Toivanen's Raster (cpu)") 167 | 168 | plt.subplot(1, 3, 2) 169 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 170 | plt.hist2d( 171 | fastmarch_output.flatten(), fastraster_output_cpu.flatten(), bins=50 172 | ) 173 | plt.xlabel("Fast Marching (cpu)") 174 | plt.ylabel("FastGeodis (cpu)") 175 | # plt.gca().set_aspect("equal", adjustable="box") 176 | 177 | if device: 178 | plt.subplot(1, 3, 3) 179 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (gpu)") 180 | plt.hist2d( 181 | fastmarch_output.flatten(), 182 | fastraster_output_gpu.flatten(), 183 | bins=50, 184 | ) 185 | plt.xlabel("Fast Marching (cpu)") 186 | plt.ylabel("FastGeodis (gpu)") 187 | # plt.gca().set_aspect("equal", adjustable="box") 188 | 189 | plt.tight_layout() 190 | # plt.colorbar() 191 | plt.show() 192 | 193 | 194 | def demo_geodesic_distance2d(image): 195 | # make image bigger to check how much workload each method can take 196 | scale = 6 197 | scaled_image_size = [x * scale for x in image.size] 198 | image = image.resize(scaled_image_size) 199 | seed_position = [100 * scale, 100 * scale] 200 | evaluate_geodesic_distance2d(image, seed_position) 201 | 202 | 203 | if __name__ == "__main__": 204 | # "gray" or "color" 205 | example = "gray" 206 | 207 | if example == "gray": 208 | image = Image.open("data/img2d.png").convert("L") 209 | elif example == "color": 210 | image = Image.open("data/ISIC_546.jpg") 211 | 212 | demo_geodesic_distance2d(image) 213 | -------------------------------------------------------------------------------- /samples/demo2d_signed.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import FastGeodis 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import torch 7 | from PIL import Image 8 | 9 | 10 | def evaluate_geodesic_distance2d(image, seed_pos): 11 | SHOW_JOINT_HIST = False 12 | # get image and create seed image 13 | input_image = np.asanyarray(image, np.float32) 14 | Seed = np.zeros((input_image.shape[0], input_image.shape[1]), np.float32) 15 | Seed[seed_pos[0]][seed_pos[1]] = 1 16 | 17 | # run and time each method 18 | iterations = 2 19 | v = 1e10 20 | lamb = 1.0 21 | 22 | if input_image.ndim == 3: 23 | input_image = np.moveaxis(input_image, -1, 0) 24 | else: 25 | input_image = np.expand_dims(input_image, 0) 26 | 27 | device = "cpu" 28 | input_image_pt = torch.from_numpy(input_image).unsqueeze_(0).to(device) 29 | seed_image_pt = ( 30 | torch.from_numpy(1 - Seed.astype(np.float32)) 31 | .unsqueeze_(0) 32 | .unsqueeze_(0) 33 | .to(device) 34 | ) 35 | 36 | tic = time.time() 37 | fastmarch_output = np.squeeze( 38 | FastGeodis.signed_geodesic2d_fastmarch( 39 | input_image_pt, seed_image_pt, lamb 40 | ) 41 | .cpu() 42 | .numpy() 43 | ) 44 | fastmarch_time = time.time() - tic 45 | 46 | tic = time.time() 47 | toivanenraster_output = np.squeeze( 48 | FastGeodis.signed_generalised_geodesic2d_toivanen( 49 | input_image_pt, seed_image_pt, v, lamb, iterations 50 | ) 51 | .cpu() 52 | .numpy() 53 | ) 54 | toivanenraster_time = time.time() - tic 55 | 56 | tic = time.time() 57 | fastraster_output_cpu = np.squeeze( 58 | FastGeodis.signed_generalised_geodesic2d( 59 | input_image_pt, seed_image_pt, v, lamb, iterations 60 | ) 61 | .cpu() 62 | .numpy() 63 | ) 64 | fastraster_time_cpu = time.time() - tic 65 | 66 | device = "cuda" if torch.cuda.is_available() else None 67 | if device: 68 | input_image_pt = input_image_pt.to(device) 69 | seed_image_pt = seed_image_pt.to(device) 70 | 71 | tic = time.time() 72 | fastraster_output_gpu = np.squeeze( 73 | FastGeodis.signed_generalised_geodesic2d( 74 | input_image_pt, seed_image_pt, v, lamb, iterations 75 | ) 76 | .cpu() 77 | .numpy() 78 | ) 79 | fastraster_time_gpu = time.time() - tic 80 | 81 | print("Runtimes:") 82 | print( 83 | "Fast Marching CPU: {:.6f} s \nToivanen's CPU raster: {:.6f} s \nFastGeodis CPU raster: {:.6f} s".format( 84 | fastmarch_time, toivanenraster_time, fastraster_time_cpu 85 | ) 86 | ) 87 | 88 | if device: 89 | print("FastGeodis GPU raster: {:.6f} s".format(fastraster_time_gpu)) 90 | 91 | plt.figure(figsize=(18, 6)) 92 | plt.subplot(2, 4, 1) 93 | plt.imshow(image, cmap="gray") 94 | plt.autoscale(False) 95 | plt.plot([seed_pos[0]], [seed_pos[1]], "ro") 96 | plt.axis("off") 97 | plt.title("(a) Input image") 98 | 99 | plt.subplot(2, 4, 5) 100 | plt.imshow(fastmarch_output) 101 | plt.axis("off") 102 | plt.title("(b) Fast Marching (cpu) | ({:.4f} s)".format(fastmarch_time)) 103 | 104 | 105 | plt.subplot(2, 4, 2) 106 | plt.imshow(toivanenraster_output) 107 | plt.axis("off") 108 | plt.title("(c) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 109 | 110 | plt.subplot(2, 4, 3) 111 | plt.imshow(fastraster_output_cpu) 112 | plt.axis("off") 113 | plt.title("(e) FastGeodis (cpu) | ({:.4f} s)".format(fastraster_time_cpu)) 114 | 115 | plt.subplot(2, 4, 6) 116 | plt.imshow(toivanenraster_output) 117 | plt.axis("off") 118 | plt.title("(d) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 119 | 120 | if device: 121 | plt.subplot(2, 4, 7) 122 | plt.imshow(fastraster_output_gpu) 123 | plt.axis("off") 124 | plt.title("(f) FastGeodis (gpu) | ({:.4f} s)".format(fastraster_time_gpu)) 125 | 126 | diff = ( 127 | abs(fastmarch_output - fastraster_output_cpu) 128 | / (fastmarch_output + 1e-7) 129 | * 100 130 | ) 131 | plt.subplot(2, 4, 4) 132 | plt.imshow(diff) 133 | plt.axis("off") 134 | plt.title( 135 | "(g) Fast Marching vs. FastGeodis (cpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 136 | np.max(diff), np.min(diff) 137 | ) 138 | ) 139 | 140 | if device: 141 | diff = ( 142 | abs(fastmarch_output - fastraster_output_gpu) 143 | / (fastmarch_output + 1e-7) 144 | * 100 145 | ) 146 | plt.subplot(2, 4, 8) 147 | plt.imshow(diff) 148 | plt.axis("off") 149 | plt.title( 150 | "(h) Fast Marching vs. FastGeodis (gpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 151 | np.max(diff), np.min(diff) 152 | ) 153 | ) 154 | 155 | # plt.colorbar() 156 | plt.show() 157 | 158 | if SHOW_JOINT_HIST: 159 | plt.figure(figsize=(12, 6)) 160 | plt.subplot(1, 3, 1) 161 | plt.title("Joint histogram\nFast Marching (cpu) vs. Toivanen's Raster (cpu)") 162 | plt.hist2d( 163 | fastmarch_output.flatten(), toivanenraster_output.flatten(), bins=50 164 | ) 165 | plt.xlabel("Fast Marching (cpu)") 166 | plt.ylabel("Toivanen's Raster (cpu)") 167 | 168 | plt.subplot(1, 3, 2) 169 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 170 | plt.hist2d( 171 | fastmarch_output.flatten(), fastraster_output_cpu.flatten(), bins=50 172 | ) 173 | plt.xlabel("Fast Marching (cpu)") 174 | plt.ylabel("FastGeodis (cpu)") 175 | # plt.gca().set_aspect("equal", adjustable="box") 176 | 177 | if device: 178 | plt.subplot(1, 3, 3) 179 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (gpu)") 180 | plt.hist2d( 181 | fastmarch_output.flatten(), 182 | fastraster_output_gpu.flatten(), 183 | bins=50, 184 | ) 185 | plt.xlabel("Fast Marching (cpu)") 186 | plt.ylabel("FastGeodis (gpu)") 187 | # plt.gca().set_aspect("equal", adjustable="box") 188 | 189 | plt.tight_layout() 190 | # plt.colorbar() 191 | plt.show() 192 | 193 | 194 | def demo_geodesic_distance2d(image): 195 | # make image bigger to check how much workload each method can take 196 | scale = 6 197 | scaled_image_size = [x * scale for x in image.size] 198 | image = image.resize(scaled_image_size) 199 | seed_position = [100 * scale, 100 * scale] 200 | evaluate_geodesic_distance2d(image, seed_position) 201 | 202 | 203 | if __name__ == "__main__": 204 | # "gray" or "color" 205 | example = "gray" 206 | 207 | if example == "gray": 208 | image = Image.open("data/img2d.png").convert("L") 209 | elif example == "color": 210 | image = Image.open("data/ISIC_546.jpg") 211 | 212 | demo_geodesic_distance2d(image) 213 | -------------------------------------------------------------------------------- /samples/demo3d.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import time 4 | from functools import wraps 5 | 6 | import FastGeodis 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import SimpleITK as sitk 10 | import torch 11 | 12 | 13 | def demo_geodesic_distance3d(image_path, seed_pos): 14 | SHOW_JOINT_HIST = False 15 | image_folder = os.path.dirname(image_path) 16 | image_sitk = sitk.ReadImage(image_path) 17 | input_image = sitk.GetArrayFromImage(image_sitk) 18 | spacing_raw = image_sitk.GetSpacing() 19 | spacing = [spacing_raw[2], spacing_raw[1], spacing_raw[0]] 20 | 21 | input_image = np.asarray(input_image, np.float32) 22 | input_image = input_image[18:38, 63:183, 93:233] 23 | seed_image = np.zeros_like(input_image, np.uint8) 24 | seed_image[seed_pos[0]][seed_pos[1]][seed_pos[2]] = 1 25 | 26 | device = "cpu" 27 | input_image_pt = torch.from_numpy(input_image).unsqueeze_(0).unsqueeze_(0) 28 | seed_image_pt = ( 29 | torch.from_numpy(1 - seed_image.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 30 | ) 31 | input_image_pt = input_image_pt.to(device) 32 | seed_image_pt = seed_image_pt.to(device) 33 | 34 | tic = time.time() 35 | fastmarch_output = np.squeeze( 36 | FastGeodis.geodesic3d_fastmarch( 37 | input_image_pt, seed_image_pt, spacing, 1.0 38 | ) 39 | .cpu() 40 | .numpy() 41 | ) 42 | fastmarch_time = time.time() - tic 43 | 44 | tic = time.time() 45 | toivanenraster_output = np.squeeze( 46 | FastGeodis.generalised_geodesic3d_toivanen( 47 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 48 | ) 49 | .detach() 50 | .cpu() 51 | .numpy() 52 | ) 53 | toivanenraster_time = time.time() - tic 54 | 55 | tic = time.time() 56 | fastraster_output_cpu = np.squeeze( 57 | FastGeodis.generalised_geodesic3d( 58 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 59 | ) 60 | .detach() 61 | .cpu() 62 | .numpy() 63 | ) 64 | fastraster_time_cpu = time.time() - tic 65 | 66 | device = ( 67 | "cuda" if input_image_pt.shape[1] == 1 and torch.cuda.is_available() else None 68 | ) 69 | if device: 70 | input_image_pt = input_image_pt.to(device) 71 | seed_image_pt = seed_image_pt.to(device) 72 | tic = time.time() 73 | fastraster_output_gpu = np.squeeze( 74 | FastGeodis.generalised_geodesic3d( 75 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 76 | ) 77 | .detach() 78 | .cpu() 79 | .numpy() 80 | ) 81 | fastraster_time_gpu = time.time() - tic 82 | 83 | print( 84 | "Fast Marching CPU: {:.6} s \nToivanen's CPU raster: {:.6f} s \nFastGeodis CPU raster: {:.6f} s".format( 85 | fastmarch_time, toivanenraster_time, fastraster_time_cpu 86 | ) 87 | ) 88 | if device: 89 | print("FastGeodis GPU raster: {:.6f} s".format(fastraster_time_gpu)) 90 | 91 | img_toivanenraster_output = sitk.GetImageFromArray(toivanenraster_output) 92 | img_toivanenraster_output.SetSpacing(spacing_raw) 93 | sitk.WriteImage( 94 | img_toivanenraster_output, os.path.join(image_folder, "image3d_dis2.nii.gz") 95 | ) 96 | 97 | img_d3 = sitk.GetImageFromArray(fastraster_output_cpu) 98 | img_d3.SetSpacing(spacing_raw) 99 | sitk.WriteImage(img_d3, os.path.join(image_folder, "image3d_dis3.nii.gz")) 100 | 101 | input_image_sub = sitk.GetImageFromArray(input_image) 102 | input_image_sub.SetSpacing(spacing_raw) 103 | sitk.WriteImage(input_image_sub, os.path.join(image_folder, "image3d_sub.nii.gz")) 104 | 105 | input_image = input_image * 255 / input_image.max() 106 | input_image = np.asarray(input_image, np.uint8) 107 | 108 | image_slice = input_image[10] 109 | fastmarch_output_slice = fastmarch_output[10] 110 | toivanenraster_output_slice = toivanenraster_output[10] 111 | fastraster_output_cpu_slice = fastraster_output_cpu[10] 112 | if device: 113 | fastraster_output_gpu_slice = fastraster_output_gpu[10] 114 | 115 | plt.figure(figsize=(18, 6)) 116 | plt.subplot(2, 5, 1) 117 | plt.imshow(image_slice, cmap="gray") 118 | plt.autoscale(False) 119 | plt.plot([70], [60], "ro") 120 | plt.axis("off") 121 | plt.title("(a) Input image") 122 | 123 | plt.subplot(2, 4, 5) 124 | plt.imshow(fastmarch_output_slice) 125 | plt.axis("off") 126 | plt.title("(b) Fast Marching (cpu) | ({:.4f} s)".format(fastmarch_time)) 127 | 128 | plt.subplot(2, 4, 2) 129 | plt.imshow(toivanenraster_output_slice) 130 | plt.axis("off") 131 | plt.title("(c) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 132 | 133 | plt.subplot(2, 4, 3) 134 | plt.imshow(fastraster_output_cpu_slice) 135 | plt.axis("off") 136 | plt.title("(e) FastGeodis (cpu) | ({:.4f} s)".format(fastraster_time_cpu)) 137 | 138 | plt.subplot(2, 4, 6) 139 | plt.imshow(toivanenraster_output_slice) 140 | plt.axis("off") 141 | plt.title("(d) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 142 | 143 | if device: 144 | plt.subplot(2, 4, 7) 145 | plt.imshow(fastraster_output_gpu_slice) 146 | plt.axis("off") 147 | plt.title("(f) FastGeodis (gpu) | ({:.4f} s)".format(fastraster_time_gpu)) 148 | 149 | diff = ( 150 | abs(fastmarch_output - fastraster_output_cpu) 151 | / (fastmarch_output + 1e-7) 152 | * 100 153 | ) 154 | diff_vol = fastmarch_output - fastraster_output_cpu 155 | diff_slice = diff_vol[10] 156 | plt.subplot(2, 4, 4) 157 | plt.imshow(diff_slice) 158 | plt.axis("off") 159 | plt.title( 160 | "(g) Fast Marching vs. FastGeodis (cpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 161 | np.max(diff), np.min(diff) 162 | ) 163 | ) 164 | 165 | if device: 166 | diff = ( 167 | abs(fastmarch_output - fastraster_output_gpu) 168 | / (fastmarch_output + 1e-7) 169 | * 100 170 | ) 171 | diff_vol = fastmarch_output - fastraster_output_gpu 172 | diff_slice = diff_vol[10] 173 | plt.subplot(2, 4, 8) 174 | plt.imshow(diff_slice) 175 | plt.axis("off") 176 | plt.title( 177 | "(h) Fast Marching vs. FastGeodis (gpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 178 | np.max(diff), np.min(diff) 179 | ) 180 | ) 181 | 182 | plt.show() 183 | 184 | if SHOW_JOINT_HIST: 185 | plt.figure(figsize=(12, 6)) 186 | plt.subplot(1, 3, 1) 187 | plt.title("Joint histogram\nFast Marching vs. Toivanen's Raster (cpu)") 188 | plt.hist2d( 189 | fastmarch_output.flatten(), toivanenraster_output.flatten(), bins=50 190 | ) 191 | plt.xlabel("Fast Marching (cpu)") 192 | plt.ylabel("Toivanen's Raster (cpu)") 193 | 194 | plt.subplot(1, 3, 2) 195 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 196 | plt.hist2d( 197 | fastmarch_output.flatten(), fastraster_output_cpu.flatten(), bins=50 198 | ) 199 | plt.xlabel("Fast Marching (cpu)") 200 | plt.ylabel("FastGeodis (cpu)") 201 | 202 | if device: 203 | plt.subplot(1, 3, 3) 204 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 205 | plt.hist2d( 206 | fastmarch_output.flatten(), 207 | fastraster_output_gpu.flatten(), 208 | bins=50, 209 | ) 210 | plt.xlabel("Fast Marching (cpu)") 211 | plt.ylabel("FastGeodis (gpu)") 212 | # plt.gca().set_aspect("equal", adjustable="box") 213 | 214 | plt.tight_layout() 215 | plt.show() 216 | 217 | 218 | if __name__ == "__main__": 219 | demo_geodesic_distance3d("data/img3d.nii.gz", [10, 60, 70]) 220 | -------------------------------------------------------------------------------- /samples/demo3d_signed.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import time 4 | from functools import wraps 5 | 6 | import FastGeodis 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import SimpleITK as sitk 10 | import torch 11 | 12 | 13 | def demo_geodesic_distance3d(image_path, seed_pos): 14 | SHOW_JOINT_HIST = False 15 | image_folder = os.path.dirname(image_path) 16 | image_sitk = sitk.ReadImage(image_path) 17 | input_image = sitk.GetArrayFromImage(image_sitk) 18 | spacing_raw = image_sitk.GetSpacing() 19 | spacing = [spacing_raw[2], spacing_raw[1], spacing_raw[0]] 20 | 21 | input_image = np.asarray(input_image, np.float32) 22 | input_image = input_image[18:38, 63:183, 93:233] 23 | seed_image = np.zeros_like(input_image, np.uint8) 24 | seed_image[seed_pos[0]][seed_pos[1]][seed_pos[2]] = 1 25 | 26 | device = "cpu" 27 | input_image_pt = torch.from_numpy(input_image).unsqueeze_(0).unsqueeze_(0) 28 | seed_image_pt = ( 29 | torch.from_numpy(1 - seed_image.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 30 | ) 31 | input_image_pt = input_image_pt.to(device) 32 | seed_image_pt = seed_image_pt.to(device) 33 | 34 | tic = time.time() 35 | fastmarch_output = np.squeeze( 36 | FastGeodis.signed_geodesic3d_fastmarch( 37 | input_image_pt, seed_image_pt, spacing, 1.0 38 | ) 39 | .cpu() 40 | .numpy() 41 | ) 42 | fastmarch_time = time.time() - tic 43 | 44 | tic = time.time() 45 | toivanenraster_output = np.squeeze( 46 | FastGeodis.signed_generalised_geodesic3d_toivanen( 47 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 48 | ) 49 | .detach() 50 | .cpu() 51 | .numpy() 52 | ) 53 | toivanenraster_time = time.time() - tic 54 | 55 | tic = time.time() 56 | fastraster_output_cpu = np.squeeze( 57 | FastGeodis.signed_generalised_geodesic3d( 58 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 59 | ) 60 | .detach() 61 | .cpu() 62 | .numpy() 63 | ) 64 | fastraster_time_cpu = time.time() - tic 65 | 66 | device = ( 67 | "cuda" if input_image_pt.shape[1] == 1 and torch.cuda.is_available() else None 68 | ) 69 | if device: 70 | input_image_pt = input_image_pt.to(device) 71 | seed_image_pt = seed_image_pt.to(device) 72 | tic = time.time() 73 | fastraster_output_gpu = np.squeeze( 74 | FastGeodis.signed_generalised_geodesic3d( 75 | input_image_pt, seed_image_pt, spacing, 1e10, 1.0, 4 76 | ) 77 | .detach() 78 | .cpu() 79 | .numpy() 80 | ) 81 | fastraster_time_gpu = time.time() - tic 82 | 83 | print( 84 | "Fast Marching CPU: {:.6} s \nToivanen's CPU raster: {:.6f} s \nFastGeodis CPU raster: {:.6f} s".format( 85 | fastmarch_time, toivanenraster_time, fastraster_time_cpu 86 | ) 87 | ) 88 | if device: 89 | print("FastGeodis GPU raster: {:.6f} s".format(fastraster_time_gpu)) 90 | 91 | img_toivanenraster_output = sitk.GetImageFromArray(toivanenraster_output) 92 | img_toivanenraster_output.SetSpacing(spacing_raw) 93 | sitk.WriteImage( 94 | img_toivanenraster_output, os.path.join(image_folder, "image3d_dis2.nii.gz") 95 | ) 96 | 97 | img_d3 = sitk.GetImageFromArray(fastraster_output_cpu) 98 | img_d3.SetSpacing(spacing_raw) 99 | sitk.WriteImage(img_d3, os.path.join(image_folder, "image3d_dis3.nii.gz")) 100 | 101 | input_image_sub = sitk.GetImageFromArray(input_image) 102 | input_image_sub.SetSpacing(spacing_raw) 103 | sitk.WriteImage(input_image_sub, os.path.join(image_folder, "image3d_sub.nii.gz")) 104 | 105 | input_image = input_image * 255 / input_image.max() 106 | input_image = np.asarray(input_image, np.uint8) 107 | 108 | image_slice = input_image[10] 109 | fastmarch_output_slice = fastmarch_output[10] 110 | toivanenraster_output_slice = toivanenraster_output[10] 111 | fastraster_output_cpu_slice = fastraster_output_cpu[10] 112 | if device: 113 | fastraster_output_gpu_slice = fastraster_output_gpu[10] 114 | 115 | plt.figure(figsize=(18, 6)) 116 | plt.subplot(2, 5, 1) 117 | plt.imshow(image_slice, cmap="gray") 118 | plt.autoscale(False) 119 | plt.plot([70], [60], "ro") 120 | plt.axis("off") 121 | plt.title("(a) Input image") 122 | 123 | plt.subplot(2, 4, 5) 124 | plt.imshow(fastmarch_output_slice) 125 | plt.axis("off") 126 | plt.title("(b) Fast Marching (cpu) | ({:.4f} s)".format(fastmarch_time)) 127 | 128 | plt.subplot(2, 4, 2) 129 | plt.imshow(toivanenraster_output_slice) 130 | plt.axis("off") 131 | plt.title("(c) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 132 | 133 | plt.subplot(2, 4, 3) 134 | plt.imshow(fastraster_output_cpu_slice) 135 | plt.axis("off") 136 | plt.title("(e) FastGeodis (cpu) | ({:.4f} s)".format(fastraster_time_cpu)) 137 | 138 | plt.subplot(2, 4, 6) 139 | plt.imshow(toivanenraster_output_slice) 140 | plt.axis("off") 141 | plt.title("(d) Toivanen's Raster (cpu) | ({:.4f} s)".format(toivanenraster_time)) 142 | 143 | if device: 144 | plt.subplot(2, 4, 7) 145 | plt.imshow(fastraster_output_gpu_slice) 146 | plt.axis("off") 147 | plt.title("(f) FastGeodis (gpu) | ({:.4f} s)".format(fastraster_time_gpu)) 148 | 149 | diff = ( 150 | abs(fastmarch_output - fastraster_output_cpu) 151 | / (fastmarch_output + 1e-7) 152 | * 100 153 | ) 154 | diff_vol = fastmarch_output - fastraster_output_cpu 155 | diff_slice = diff_vol[10] 156 | plt.subplot(2, 4, 4) 157 | plt.imshow(diff_slice) 158 | plt.axis("off") 159 | plt.title( 160 | "(g) Fast Marching vs. FastGeodis (cpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 161 | np.max(diff), np.min(diff) 162 | ) 163 | ) 164 | 165 | if device: 166 | diff = ( 167 | abs(fastmarch_output - fastraster_output_gpu) 168 | / (fastmarch_output + 1e-7) 169 | * 100 170 | ) 171 | diff_vol = fastmarch_output - fastraster_output_gpu 172 | diff_slice = diff_vol[10] 173 | plt.subplot(2, 4, 8) 174 | plt.imshow(diff_slice) 175 | plt.axis("off") 176 | plt.title( 177 | "(h) Fast Marching vs. FastGeodis (gpu)\ndiff: max: {:.4f} | min: {:.4f}".format( 178 | np.max(diff), np.min(diff) 179 | ) 180 | ) 181 | 182 | plt.show() 183 | 184 | if SHOW_JOINT_HIST: 185 | plt.figure(figsize=(12, 6)) 186 | plt.subplot(1, 3, 1) 187 | plt.title("Joint histogram\nFast Marching vs. Toivanen's Raster (cpu)") 188 | plt.hist2d( 189 | fastmarch_output.flatten(), toivanenraster_output.flatten(), bins=50 190 | ) 191 | plt.xlabel("Fast Marching (cpu)") 192 | plt.ylabel("Toivanen's Raster (cpu)") 193 | 194 | plt.subplot(1, 3, 2) 195 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 196 | plt.hist2d( 197 | fastmarch_output.flatten(), fastraster_output_cpu.flatten(), bins=50 198 | ) 199 | plt.xlabel("Fast Marching (cpu)") 200 | plt.ylabel("FastGeodis (cpu)") 201 | 202 | if device: 203 | plt.subplot(1, 3, 3) 204 | plt.title("Joint histogram\nFast Marching (cpu) vs. FastGeodis (cpu)") 205 | plt.hist2d( 206 | fastmarch_output.flatten(), 207 | fastraster_output_gpu.flatten(), 208 | bins=50, 209 | ) 210 | plt.xlabel("Fast Marching (cpu)") 211 | plt.ylabel("FastGeodis (gpu)") 212 | # plt.gca().set_aspect("equal", adjustable="box") 213 | 214 | plt.tight_layout() 215 | plt.show() 216 | 217 | 218 | if __name__ == "__main__": 219 | demo_geodesic_distance3d("data/img3d.nii.gz", [10, 60, 70]) 220 | -------------------------------------------------------------------------------- /samples/simpledemo2d.py: -------------------------------------------------------------------------------- 1 | import FastGeodis 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | import torch 5 | from PIL import Image 6 | 7 | # FastGeodis Method 8 | 9 | device = "cuda" if torch.cuda.is_available() else "cpu" 10 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 11 | 12 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 13 | image_pt = image_pt.to(device) 14 | mask_pt = torch.ones_like(image_pt) 15 | mask_pt[..., 100, 100] = 0 16 | 17 | v = 1e10 18 | iterations = 2 19 | 20 | lamb = 1.0 # <-- Geodesic distance transform 21 | geodesic_dist = FastGeodis.generalised_geodesic2d( 22 | image_pt, mask_pt, v, lamb, iterations 23 | ) 24 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 25 | 26 | lamb = 0.0 # <-- Euclidean distance transform 27 | euclidean_dist = FastGeodis.generalised_geodesic2d( 28 | image_pt, mask_pt, v, lamb, iterations 29 | ) 30 | euclidean_dist = np.squeeze(euclidean_dist.cpu().numpy()) 31 | 32 | plt.figure(figsize=(12, 4)) 33 | plt.subplot(1, 3, 1) 34 | plt.imshow(image) 35 | 36 | plt.subplot(1, 3, 2) 37 | plt.imshow(geodesic_dist) 38 | plt.plot(100, 100, "mo") 39 | 40 | plt.subplot(1, 3, 3) 41 | plt.imshow(euclidean_dist) 42 | plt.plot(100, 100, "mo") 43 | 44 | plt.show() 45 | 46 | # Toivanen's Raster Method 47 | 48 | # Toivanen's method only support CPU 49 | image_pt = image_pt.to("cpu") 50 | mask_pt = mask_pt.to("cpu") 51 | 52 | lamb = 1.0 # <-- Geodesic distance transform 53 | geodesic_dist_toivanen = FastGeodis.generalised_geodesic2d_toivanen( 54 | image_pt, mask_pt, v, lamb, iterations 55 | ) 56 | geodesic_dist_toivanen = np.squeeze(geodesic_dist_toivanen.cpu().numpy()) 57 | 58 | lamb = 0.0 # <-- Euclidean distance transform 59 | euclidean_dist_toivanen = FastGeodis.generalised_geodesic2d_toivanen( 60 | image_pt, mask_pt, v, lamb, iterations 61 | ) 62 | euclidean_dist_toivanen = np.squeeze(euclidean_dist_toivanen.cpu().numpy()) 63 | 64 | plt.figure(figsize=(12, 4)) 65 | plt.subplot(1, 3, 1) 66 | plt.imshow(image) 67 | 68 | plt.subplot(1, 3, 2) 69 | plt.imshow(geodesic_dist_toivanen) 70 | plt.plot(100, 100, "mo") 71 | 72 | plt.subplot(1, 3, 3) 73 | plt.imshow(euclidean_dist_toivanen) 74 | plt.plot(100, 100, "mo") 75 | 76 | plt.show() 77 | 78 | 79 | # Fast Marching Method 80 | 81 | # Fast Marching method only support CPU 82 | image_pt = image_pt.to("cpu") 83 | mask_pt = mask_pt.to("cpu") 84 | 85 | lamb = 1.0 # <-- Geodesic distance transform 86 | geodesic_dist_fastmarch = FastGeodis.geodesic2d_fastmarch( 87 | image_pt, mask_pt, lamb 88 | ) 89 | geodesic_dist_fastmarch = np.squeeze(geodesic_dist_fastmarch.cpu().numpy()) 90 | 91 | lamb = 0.0 # <-- Euclidean distance transform 92 | euclidean_dist_fastmarch = FastGeodis.geodesic2d_fastmarch( 93 | image_pt, mask_pt, lamb 94 | ) 95 | euclidean_dist_fastmarch = np.squeeze(euclidean_dist_fastmarch.cpu().numpy()) 96 | 97 | plt.figure(figsize=(12, 4)) 98 | plt.subplot(1, 3, 1) 99 | plt.imshow(image) 100 | 101 | plt.subplot(1, 3, 2) 102 | plt.imshow(geodesic_dist_fastmarch) 103 | plt.plot(100, 100, "mo") 104 | 105 | plt.subplot(1, 3, 3) 106 | plt.imshow(euclidean_dist_fastmarch) 107 | plt.plot(100, 100, "mo") 108 | 109 | plt.show() 110 | -------------------------------------------------------------------------------- /samples/simpledemo2d_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | import numpy as np 3 | import torch 4 | from PIL import Image 5 | 6 | import FastGeodis 7 | 8 | device = "cuda" if torch.cuda.is_available() else "cpu" 9 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 10 | 11 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 12 | image_pt = image_pt.to(device) 13 | mask_pt = torch.ones_like(image_pt) 14 | mask_pt[..., 100, 100] = 0 15 | 16 | v = 1e10 17 | iterations = 2 18 | 19 | lamb = 1.0 # <-- Geodesic distance transform 20 | geodesic_dist = FastGeodis.generalised_geodesic2d( 21 | image_pt, mask_pt, v, lamb, iterations 22 | ) 23 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 24 | 25 | print(geodesic_dist) 26 | -------------------------------------------------------------------------------- /samples/simpledemo2d_signed.py: -------------------------------------------------------------------------------- 1 | import FastGeodis 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | import torch 5 | from PIL import Image 6 | 7 | device = "cuda" if torch.cuda.is_available() else "cpu" 8 | image = np.asarray(Image.open("data/img2d.png"), np.float32) 9 | 10 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 11 | image_pt = image_pt.to(device) 12 | mask_pt = torch.ones_like(image_pt) 13 | mask_pt[..., 100, 100] = 0 14 | 15 | v = 1e10 16 | iterations = 2 17 | 18 | lamb = 1.0 # <-- Geodesic distance transform 19 | geodesic_dist = FastGeodis.signed_generalised_geodesic2d( 20 | image_pt, mask_pt, v, lamb, iterations 21 | ) 22 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 23 | 24 | lamb = 0.0 # <-- Euclidean distance transform 25 | euclidean_dist = FastGeodis.signed_generalised_geodesic2d( 26 | image_pt, mask_pt, v, lamb, iterations 27 | ) 28 | euclidean_dist = np.squeeze(euclidean_dist.cpu().numpy()) 29 | 30 | plt.figure(figsize=(12, 4)) 31 | plt.subplot(1, 3, 1) 32 | plt.imshow(image) 33 | 34 | plt.subplot(1, 3, 2) 35 | plt.imshow(geodesic_dist) 36 | plt.plot(100, 100, "mo") 37 | 38 | plt.subplot(1, 3, 3) 39 | plt.imshow(euclidean_dist) 40 | plt.plot(100, 100, "mo") 41 | 42 | plt.show() 43 | 44 | # min/max for signed distances 45 | print(geodesic_dist.min()) 46 | print(geodesic_dist.max()) 47 | print(euclidean_dist.min()) 48 | print(euclidean_dist.max()) 49 | 50 | 51 | # Toivanen's Raster Method 52 | 53 | # Toivanen's method only support CPU 54 | image_pt = image_pt.to("cpu") 55 | mask_pt = mask_pt.to("cpu") 56 | 57 | lamb = 1.0 # <-- Geodesic distance transform 58 | geodesic_dist_toivanen = FastGeodis.signed_generalised_geodesic2d_toivanen( 59 | image_pt, mask_pt, v, lamb, iterations 60 | ) 61 | geodesic_dist_toivanen = np.squeeze(geodesic_dist_toivanen.cpu().numpy()) 62 | 63 | lamb = 0.0 # <-- Euclidean distance transform 64 | euclidean_dist_toivanen = FastGeodis.signed_generalised_geodesic2d_toivanen( 65 | image_pt, mask_pt, v, lamb, iterations 66 | ) 67 | euclidean_dist_toivanen = np.squeeze(euclidean_dist_toivanen.cpu().numpy()) 68 | 69 | plt.figure(figsize=(12, 4)) 70 | plt.subplot(1, 3, 1) 71 | plt.imshow(image) 72 | 73 | plt.subplot(1, 3, 2) 74 | plt.imshow(geodesic_dist_toivanen) 75 | plt.plot(100, 100, "mo") 76 | 77 | plt.subplot(1, 3, 3) 78 | plt.imshow(euclidean_dist_toivanen) 79 | plt.plot(100, 100, "mo") 80 | 81 | plt.show() 82 | 83 | # min/max for signed distances 84 | print(geodesic_dist_toivanen.min()) 85 | print(geodesic_dist_toivanen.max()) 86 | 87 | print(euclidean_dist_toivanen.min()) 88 | print(euclidean_dist_toivanen.max()) 89 | 90 | 91 | # Fast Marching Method 92 | 93 | # Fast Marching method only support CPU 94 | image_pt = image_pt.to("cpu") 95 | mask_pt = mask_pt.to("cpu") 96 | 97 | lamb = 1.0 # <-- Geodesic distance transform 98 | geodesic_dist_fastmarch = FastGeodis.signed_geodesic2d_fastmarch( 99 | image_pt, mask_pt, lamb 100 | ) 101 | geodesic_dist_fastmarch = np.squeeze(geodesic_dist_fastmarch.cpu().numpy()) 102 | 103 | lamb = 0.0 # <-- Euclidean distance transform 104 | euclidean_dist_fastmarch = FastGeodis.signed_geodesic2d_fastmarch( 105 | image_pt, mask_pt, lamb 106 | ) 107 | euclidean_dist_fastmarch = np.squeeze(euclidean_dist_fastmarch.cpu().numpy()) 108 | 109 | plt.figure(figsize=(12, 4)) 110 | plt.subplot(1, 3, 1) 111 | plt.imshow(image) 112 | 113 | plt.subplot(1, 3, 2) 114 | plt.imshow(geodesic_dist_fastmarch) 115 | plt.plot(100, 100, "mo") 116 | 117 | plt.subplot(1, 3, 3) 118 | plt.imshow(euclidean_dist_fastmarch) 119 | plt.plot(100, 100, "mo") 120 | 121 | plt.show() 122 | -------------------------------------------------------------------------------- /samples/simpledemo3d.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import FastGeodis 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import SimpleITK as sitk 7 | import torch 8 | from PIL import Image 9 | 10 | # FastGeodis Method 11 | 12 | device = "cuda" if torch.cuda.is_available() else "cpu" 13 | 14 | image_path = "data/img3d.nii.gz" 15 | seed_pos = [10, 60, 70] 16 | image_folder = os.path.dirname(image_path) 17 | image_sitk = sitk.ReadImage(image_path) 18 | image = sitk.GetArrayFromImage(image_sitk) 19 | spacing_raw = image_sitk.GetSpacing() 20 | spacing = [spacing_raw[2], spacing_raw[1], spacing_raw[0]] 21 | 22 | image = np.asarray(image, np.float32) 23 | image = image[18:38, 63:183, 93:233] 24 | mask = np.zeros_like(image, np.uint8) 25 | mask[seed_pos[0]][seed_pos[1]][seed_pos[2]] = 1 26 | 27 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 28 | mask_pt = torch.from_numpy(1 - mask.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 29 | image_pt = image_pt.to(device) 30 | mask_pt = mask_pt.to(device) 31 | 32 | v = 1e10 33 | iterations = 2 34 | 35 | lamb = 1.0 # <-- Geodesic distance transform 36 | geodesic_dist = FastGeodis.generalised_geodesic3d( 37 | image_pt, mask_pt, spacing, v, lamb, iterations 38 | ) 39 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 40 | 41 | lamb = 0.0 # <-- Euclidean distance transform 42 | euclidean_dist = FastGeodis.generalised_geodesic3d( 43 | image_pt, mask_pt, spacing, v, lamb, iterations 44 | ) 45 | euclidean_dist = np.squeeze(euclidean_dist.cpu().numpy()) 46 | 47 | plt.figure(figsize=(12, 4)) 48 | plt.subplot(1, 3, 1) 49 | plt.imshow(image[10]) 50 | plt.plot(seed_pos[2], seed_pos[1], "ro") 51 | 52 | 53 | plt.subplot(1, 3, 2) 54 | plt.imshow(geodesic_dist[10]) 55 | plt.plot(seed_pos[2], seed_pos[1], "ro") 56 | 57 | plt.subplot(1, 3, 3) 58 | plt.imshow(euclidean_dist[10]) 59 | plt.plot(seed_pos[2], seed_pos[1], "ro") 60 | 61 | plt.show() 62 | 63 | # Toivanen's Raster Method 64 | 65 | # Toivanen's method only support CPU 66 | image_pt = image_pt.to("cpu") 67 | mask_pt = mask_pt.to("cpu") 68 | 69 | lamb = 1.0 # <-- Geodesic distance transform 70 | geodesic_dist_toivanen = FastGeodis.generalised_geodesic3d_toivanen( 71 | image_pt, mask_pt, spacing, v, lamb, iterations 72 | ) 73 | geodesic_dist_toivanen = np.squeeze(geodesic_dist_toivanen.cpu().numpy()) 74 | 75 | lamb = 0.0 # <-- Euclidean distance transform 76 | euclidean_dist_toivanen = FastGeodis.generalised_geodesic3d_toivanen( 77 | image_pt, mask_pt, spacing, v, lamb, iterations 78 | ) 79 | euclidean_dist_toivanen = np.squeeze(euclidean_dist_toivanen.cpu().numpy()) 80 | 81 | plt.figure(figsize=(12, 4)) 82 | plt.subplot(1, 3, 1) 83 | plt.imshow(image[10]) 84 | plt.plot(seed_pos[2], seed_pos[1], "ro") 85 | 86 | plt.subplot(1, 3, 2) 87 | plt.imshow(geodesic_dist_toivanen[10]) 88 | plt.plot(seed_pos[2], seed_pos[1], "ro") 89 | 90 | plt.subplot(1, 3, 3) 91 | plt.imshow(euclidean_dist_toivanen[10]) 92 | plt.plot(seed_pos[2], seed_pos[1], "ro") 93 | 94 | plt.show() 95 | 96 | # Fast Marching Method 97 | 98 | # Fast Marching method only support CPU 99 | image_pt = image_pt.to("cpu") 100 | mask_pt = mask_pt.to("cpu") 101 | 102 | lamb = 1.0 # <-- Geodesic distance transform 103 | geodesic_dist_fastmarch = FastGeodis.geodesic3d_fastmarch( 104 | image_pt, mask_pt, spacing, lamb 105 | ) 106 | geodesic_dist_fastmarch = np.squeeze(geodesic_dist_fastmarch.cpu().numpy()) 107 | 108 | lamb = 0.0 # <-- Euclidean distance transform 109 | euclidean_dist_fastmarch = FastGeodis.geodesic3d_fastmarch( 110 | image_pt, mask_pt, spacing, lamb 111 | ) 112 | euclidean_dist_fastmarch = np.squeeze(euclidean_dist_fastmarch.cpu().numpy()) 113 | 114 | plt.figure(figsize=(12, 4)) 115 | plt.subplot(1, 3, 1) 116 | plt.imshow(image[10]) 117 | plt.plot(seed_pos[2], seed_pos[1], "ro") 118 | 119 | plt.subplot(1, 3, 2) 120 | plt.imshow(geodesic_dist_fastmarch[10]) 121 | plt.plot(seed_pos[2], seed_pos[1], "ro") 122 | 123 | plt.subplot(1, 3, 3) 124 | plt.imshow(euclidean_dist_fastmarch[10]) 125 | plt.plot(seed_pos[2], seed_pos[1], "ro") 126 | 127 | plt.show() 128 | -------------------------------------------------------------------------------- /samples/simpledemo3d_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env /usr/bin/python3 2 | import os 3 | 4 | import FastGeodis 5 | import numpy as np 6 | import SimpleITK as sitk 7 | import torch 8 | from PIL import Image 9 | 10 | # FastGeodis Method 11 | 12 | device = "cuda" if torch.cuda.is_available() else "cpu" 13 | 14 | image_path = "data/img3d.nii.gz" 15 | seed_pos = [10, 60, 70] 16 | image_folder = os.path.dirname(image_path) 17 | image_sitk = sitk.ReadImage(image_path) 18 | image = sitk.GetArrayFromImage(image_sitk) 19 | spacing_raw = image_sitk.GetSpacing() 20 | spacing = [spacing_raw[2], spacing_raw[1], spacing_raw[0]] 21 | 22 | image = np.asarray(image, np.float32) 23 | image = image[18:38, 63:183, 93:233] 24 | mask = np.zeros_like(image, np.uint8) 25 | mask[seed_pos[0]][seed_pos[1]][seed_pos[2]] = 1 26 | 27 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 28 | mask_pt = torch.from_numpy(1 - mask.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 29 | image_pt = image_pt.to(device) 30 | mask_pt = mask_pt.to(device) 31 | 32 | v = 1e10 33 | iterations = 2 34 | 35 | lamb = 1.0 # <-- Geodesic distance transform 36 | geodesic_dist = FastGeodis.generalised_geodesic3d( 37 | image_pt, mask_pt, spacing, v, lamb, iterations 38 | ) 39 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 40 | 41 | print(geodesic_dist) 42 | -------------------------------------------------------------------------------- /samples/simpledemo3d_signed.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import FastGeodis 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import SimpleITK as sitk 7 | import torch 8 | from PIL import Image 9 | 10 | # FastGeodis Method 11 | 12 | device = "cuda" if torch.cuda.is_available() else "cpu" 13 | 14 | image_path = "data/img3d.nii.gz" 15 | seed_pos = [10, 60, 70] 16 | image_folder = os.path.dirname(image_path) 17 | image_sitk = sitk.ReadImage(image_path) 18 | image = sitk.GetArrayFromImage(image_sitk) 19 | spacing_raw = image_sitk.GetSpacing() 20 | spacing = [spacing_raw[2], spacing_raw[1], spacing_raw[0]] 21 | 22 | image = np.asarray(image, np.float32) 23 | image = image[18:38, 63:183, 93:233] 24 | mask = np.zeros_like(image, np.uint8) 25 | mask[seed_pos[0]][seed_pos[1]][seed_pos[2]] = 1 26 | 27 | image_pt = torch.from_numpy(image).unsqueeze_(0).unsqueeze_(0) 28 | mask_pt = torch.from_numpy(1 - mask.astype(np.float32)).unsqueeze_(0).unsqueeze_(0) 29 | image_pt = image_pt.to(device) 30 | mask_pt = mask_pt.to(device) 31 | 32 | v = 1e10 33 | iterations = 2 34 | 35 | lamb = 1.0 # <-- Geodesic distance transform 36 | geodesic_dist = FastGeodis.signed_generalised_geodesic3d( 37 | image_pt, mask_pt, spacing, v, lamb, iterations 38 | ) 39 | geodesic_dist = np.squeeze(geodesic_dist.cpu().numpy()) 40 | 41 | lamb = 0.0 # <-- Euclidean distance transform 42 | euclidean_dist = FastGeodis.signed_generalised_geodesic3d( 43 | image_pt, mask_pt, spacing, v, lamb, iterations 44 | ) 45 | euclidean_dist = np.squeeze(euclidean_dist.cpu().numpy()) 46 | 47 | plt.figure(figsize=(12, 4)) 48 | plt.subplot(1, 3, 1) 49 | plt.imshow(image[10]) 50 | plt.plot(seed_pos[2], seed_pos[1], "ro") 51 | 52 | 53 | plt.subplot(1, 3, 2) 54 | plt.imshow(geodesic_dist[10]) 55 | plt.plot(seed_pos[2], seed_pos[1], "ro") 56 | 57 | plt.subplot(1, 3, 3) 58 | plt.imshow(euclidean_dist[10]) 59 | plt.plot(seed_pos[2], seed_pos[1], "ro") 60 | 61 | plt.show() 62 | 63 | # Toivanen's Raster Method 64 | 65 | # Toivanen's method only support CPU 66 | image_pt = image_pt.to("cpu") 67 | mask_pt = mask_pt.to("cpu") 68 | 69 | lamb = 1.0 # <-- Geodesic distance transform 70 | geodesic_dist_toivanen = FastGeodis.signed_generalised_geodesic3d_toivanen( 71 | image_pt, mask_pt, spacing, v, lamb, iterations 72 | ) 73 | geodesic_dist_toivanen = np.squeeze(geodesic_dist_toivanen.cpu().numpy()) 74 | 75 | lamb = 0.0 # <-- Euclidean distance transform 76 | euclidean_dist_toivanen = FastGeodis.signed_generalised_geodesic3d_toivanen( 77 | image_pt, mask_pt, spacing, v, lamb, iterations 78 | ) 79 | euclidean_dist_toivanen = np.squeeze(euclidean_dist_toivanen.cpu().numpy()) 80 | 81 | plt.figure(figsize=(12, 4)) 82 | plt.subplot(1, 3, 1) 83 | plt.imshow(image[10]) 84 | plt.plot(seed_pos[2], seed_pos[1], "ro") 85 | 86 | plt.subplot(1, 3, 2) 87 | plt.imshow(geodesic_dist_toivanen[10]) 88 | plt.plot(seed_pos[2], seed_pos[1], "ro") 89 | 90 | plt.subplot(1, 3, 3) 91 | plt.imshow(euclidean_dist_toivanen[10]) 92 | plt.plot(seed_pos[2], seed_pos[1], "ro") 93 | 94 | plt.show() 95 | 96 | 97 | # Fast Marching Method 98 | 99 | # Fast Marching method only support CPU 100 | image_pt = image_pt.to("cpu") 101 | mask_pt = mask_pt.to("cpu") 102 | 103 | lamb = 1.0 # <-- Geodesic distance transform 104 | geodesic_dist_fastmarch = FastGeodis.signed_geodesic3d_fastmarch( 105 | image_pt, mask_pt, spacing, lamb 106 | ) 107 | geodesic_dist_fastmarch = np.squeeze(geodesic_dist_fastmarch.cpu().numpy()) 108 | 109 | lamb = 0.0 # <-- Euclidean distance transform 110 | euclidean_dist_fastmarch = FastGeodis.signed_geodesic3d_fastmarch( 111 | image_pt, mask_pt, spacing, lamb 112 | ) 113 | euclidean_dist_fastmarch = np.squeeze(euclidean_dist_fastmarch.cpu().numpy()) 114 | 115 | plt.figure(figsize=(12, 4)) 116 | plt.subplot(1, 3, 1) 117 | plt.imshow(image[10]) 118 | plt.plot(seed_pos[2], seed_pos[1], "ro") 119 | 120 | plt.subplot(1, 3, 2) 121 | plt.imshow(geodesic_dist_fastmarch[10]) 122 | plt.plot(seed_pos[2], seed_pos[1], "ro") 123 | 124 | plt.subplot(1, 3, 3) 125 | plt.imshow(euclidean_dist_fastmarch[10]) 126 | plt.plot(seed_pos[2], seed_pos[1], "ro") 127 | 128 | plt.show() 129 | -------------------------------------------------------------------------------- /samples/test_speed_benchmark_geodistk.py: -------------------------------------------------------------------------------- 1 | import json 2 | import GeodisTK 3 | import numpy as np 4 | import torch 5 | import matplotlib.pyplot as plt 6 | from functools import wraps 7 | import SimpleITK as sitk 8 | import FastGeodis 9 | import time 10 | import os 11 | 12 | def timing(f): 13 | @wraps(f) 14 | def wrap(*args, **kw): 15 | ts = time.time() 16 | result = f(*args, **kw) 17 | te = time.time() 18 | print("func:%r took: %2.4f sec" % (f.__name__, te - ts)) 19 | return result 20 | 21 | return wrap 22 | 23 | @timing 24 | def generalised_geodesic_distance_2d(I, S, v, lamb, iter): 25 | return GeodisTK.geodesic2d_raster_scan(I, 1-S.astype(np.uint8), lamb, iter) 26 | 27 | @timing 28 | def generalised_geodesic2d_raster_cpu(I, S, v, lamb, iter): 29 | return FastGeodis.generalised_geodesic2d(I, S, v, lamb, iter) 30 | 31 | @timing 32 | def generalised_geodesic2d_raster_gpu(I, S, v, lamb, iter): 33 | return FastGeodis.generalised_geodesic2d(I, S, v, lamb, iter) 34 | 35 | @timing 36 | def generalised_geodesic_distance_3d(I, S, spacing, v, lamb, iter): 37 | return GeodisTK.geodesic3d_raster_scan(I, 1-S.astype(np.uint8), spacing, lamb, iter) 38 | 39 | @timing 40 | def generalised_geodesic3d_raster_cpu(I, S, spacing, v, lamb, iter): 41 | return FastGeodis.generalised_geodesic3d(I, S, spacing, v, lamb, iter) 42 | 43 | @timing 44 | def generalised_geodesic3d_raster_gpu(I, S, spacing, v, lamb, iter): 45 | return FastGeodis.generalised_geodesic3d(I, S, spacing, v, lamb, iter) 46 | 47 | func_to_test_2d = [generalised_geodesic_distance_2d, generalised_geodesic2d_raster_cpu, generalised_geodesic2d_raster_gpu] 48 | func_to_test_3d = [generalised_geodesic_distance_3d, generalised_geodesic3d_raster_cpu, generalised_geodesic3d_raster_gpu] 49 | 50 | def test2d(): 51 | num_runs = 10 52 | 53 | sizes_to_test = [64, 64*2, 64*(2**2), 64*(2**3), 64*(2**4), 64*(2**5)]#, 64*(2**6)] 54 | print(sizes_to_test) 55 | time_taken_dict = dict() 56 | for func in func_to_test_2d: 57 | time_taken_dict[func.__name__] = [] 58 | for size in sizes_to_test: 59 | image = torch.rand((1, 1, size, size)) 60 | seed = torch.ones((1, 1, size, size)) 61 | seed[:, :, size//2, size//2] = 0.0 62 | 63 | if 'gpu' in func.__name__: 64 | image = image.to('cuda').contiguous() 65 | seed = seed.to('cuda').contiguous() 66 | 67 | tic = time.time() 68 | for i in range(num_runs): 69 | if 'cpu' in func.__name__: 70 | func(image, seed, 10000, 1.0, 2) 71 | elif 'gpu' in func.__name__ and torch.cuda.is_available(): 72 | func(image, seed, 10000, 1.0, 2) 73 | else: 74 | func(np.squeeze(image.cpu().numpy()), np.squeeze(seed.cpu().numpy()), 10000, 1.0, 2) 75 | 76 | time_taken_dict[func.__name__].append((time.time() - tic)/num_runs) 77 | print() 78 | 79 | return sizes_to_test, time_taken_dict 80 | 81 | def test3d(): 82 | num_runs = 10 83 | spacing = [1.0, 1.0, 1.0] 84 | 85 | sizes_to_test = [64*(2**0), 64*(2**1), 64*(2**2), 64*(2**3)]#, 64*(2**4)]#, 64*(2**5), 64*(2**6)] 86 | print(sizes_to_test) 87 | time_taken_dict = dict() 88 | for func in func_to_test_3d: 89 | time_taken_dict[func.__name__] = [] 90 | for size in sizes_to_test: 91 | image = torch.rand((1, 1, size, size, size)) 92 | seed = torch.ones((1, 1, size, size, size)) 93 | seed[:, :, size//2, size//2, size//2] = 0.0 94 | 95 | if 'gpu' in func.__name__: 96 | image = image.to('cuda').contiguous() 97 | seed = seed.to('cuda').contiguous() 98 | 99 | tic = time.time() 100 | for i in range(num_runs): 101 | if 'cpu' in func.__name__: 102 | func(image, seed, spacing, 10000, 1.0, 2) 103 | elif 'gpu' in func.__name__ and torch.cuda.is_available(): 104 | func(image, seed, spacing, 10000, 1.0, 2) 105 | else: 106 | func(np.squeeze(image.cpu().numpy()), np.squeeze(seed.cpu().numpy()), spacing, 10000, 1.0, 2) 107 | 108 | time_taken_dict[func.__name__].append((time.time() - tic)/num_runs) 109 | print() 110 | 111 | return sizes_to_test, time_taken_dict 112 | 113 | def save_plot(sizes, time_taken_dict, figname): 114 | plt.figure() 115 | plt.grid() 116 | for key in time_taken_dict.keys(): 117 | if 'cpu' in key: 118 | plt.plot(sizes, time_taken_dict[key], 'm-o', label='FastGeodis (cpu)') 119 | elif 'gpu' in key: 120 | plt.plot(sizes, time_taken_dict[key], 'g-o', label='FastGeodis (gpu)') 121 | else: 122 | plt.plot(sizes, time_taken_dict[key], 'r-o', label='GeodisTK') 123 | plt.legend() 124 | plt.xticks(sizes, [str(s) for s in sizes], rotation=45) 125 | plt.title(figname) 126 | plt.xlabel('Spatial size') 127 | plt.ylabel('Execution time (seconds)') 128 | plt.tight_layout() 129 | plt.savefig(os.path.join('figures', figname + '.png')) 130 | time_taken_dict['spatial_dim'] = sizes 131 | with open(os.path.join('figures', figname + '.json'), 'w') as fp: 132 | json.dump(time_taken_dict, fp, indent=4) 133 | 134 | 135 | 136 | if __name__ == "__main__": 137 | sizes, ttdict = test2d() 138 | save_plot(sizes, ttdict, 'experiment_2d') 139 | 140 | sizes, ttdict = test3d() 141 | save_plot(sizes, ttdict, 'experiment_3d') -------------------------------------------------------------------------------- /samples/test_speed_benchmark_toivanen.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | from functools import wraps 5 | 6 | import FastGeodis 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import SimpleITK as sitk 10 | import torch 11 | 12 | 13 | def timing(f): 14 | @wraps(f) 15 | def wrap(*args, **kw): 16 | ts = time.time() 17 | result = f(*args, **kw) 18 | te = time.time() 19 | print("func:%r took: %2.4f sec" % (f.__name__, te - ts)) 20 | return result 21 | 22 | return wrap 23 | 24 | 25 | @timing 26 | def generalised_geodesic_distance_2d_cpu(I, S, v, lamb, iter): 27 | return FastGeodis.generalised_geodesic2d_toivanen(I, S, v, lamb, iter) 28 | 29 | 30 | @timing 31 | def generalised_geodesic2d_raster_cpu(I, S, v, lamb, iter): 32 | return FastGeodis.generalised_geodesic2d(I, S, v, lamb, iter) 33 | 34 | 35 | @timing 36 | def generalised_geodesic2d_raster_gpu(I, S, v, lamb, iter): 37 | return FastGeodis.generalised_geodesic2d(I, S, v, lamb, iter) 38 | 39 | 40 | @timing 41 | def generalised_geodesic_distance_3d_cpu(I, S, spacing, v, lamb, iter): 42 | return FastGeodis.generalised_geodesic3d_toivanen(I, S, spacing, v, lamb, iter) 43 | 44 | 45 | @timing 46 | def generalised_geodesic3d_raster_cpu(I, S, spacing, v, lamb, iter): 47 | return FastGeodis.generalised_geodesic3d(I, S, spacing, v, lamb, iter) 48 | 49 | 50 | @timing 51 | def generalised_geodesic3d_raster_gpu(I, S, spacing, v, lamb, iter): 52 | return FastGeodis.generalised_geodesic3d(I, S, spacing, v, lamb, iter) 53 | 54 | 55 | func_to_test_2d = [ 56 | generalised_geodesic_distance_2d_cpu, 57 | generalised_geodesic2d_raster_cpu, 58 | generalised_geodesic2d_raster_gpu, 59 | ] 60 | func_to_test_3d = [ 61 | generalised_geodesic_distance_3d_cpu, 62 | generalised_geodesic3d_raster_cpu, 63 | generalised_geodesic3d_raster_gpu, 64 | ] 65 | 66 | 67 | def test2d(): 68 | num_runs = 5 69 | 70 | sizes_to_test = [ 71 | 64, 72 | 64 * 2, 73 | 64 * (2 ** 2), 74 | 64 * (2 ** 3), 75 | 64 * (2 ** 4), 76 | 64 * (2 ** 5), 77 | ] # , 64*(2**6)] 78 | print(sizes_to_test) 79 | time_taken_dict = dict() 80 | for func in func_to_test_2d: 81 | time_taken_dict[func.__name__] = [] 82 | for size in sizes_to_test: 83 | image = torch.rand((1, 1, size, size)) 84 | seed = torch.ones((1, 1, size, size)) 85 | seed[:, :, size // 2, size // 2] = 0.0 86 | 87 | if "gpu" in func.__name__: 88 | image = image.to("cuda").contiguous() 89 | seed = seed.to("cuda").contiguous() 90 | 91 | tic = time.time() 92 | for i in range(num_runs): 93 | if "cpu" in func.__name__: 94 | func(image, seed, 10000, 1.0, 2) 95 | elif "gpu" in func.__name__ and torch.cuda.is_available(): 96 | func(image, seed, 10000, 1.0, 2) 97 | else: 98 | func( 99 | np.squeeze(image.cpu().numpy()), 100 | np.squeeze(seed.cpu().numpy()), 101 | 10000, 102 | 1.0, 103 | 2, 104 | ) 105 | 106 | time_taken_dict[func.__name__].append((time.time() - tic) / num_runs) 107 | print() 108 | 109 | return sizes_to_test, time_taken_dict 110 | 111 | 112 | def test3d(): 113 | num_runs = 2 114 | spacing = [1.0, 1.0, 1.0] 115 | 116 | sizes_to_test = [ 117 | 64 * (2 ** 0), 118 | 64 * (2 ** 1), 119 | 64 * (2 ** 2), 120 | 64 * (2 ** 3), 121 | ] # , 64*(2**4)]#, 64*(2**5), 64*(2**6)] 122 | print(sizes_to_test) 123 | time_taken_dict = dict() 124 | for func in func_to_test_3d: 125 | time_taken_dict[func.__name__] = [] 126 | for size in sizes_to_test: 127 | image = torch.rand((1, 1, size, size, size)) 128 | seed = torch.ones((1, 1, size, size, size)) 129 | seed[:, :, size // 2, size // 2, size // 2] = 0.0 130 | 131 | if "gpu" in func.__name__: 132 | image = image.to("cuda").contiguous() 133 | seed = seed.to("cuda").contiguous() 134 | 135 | tic = time.time() 136 | for i in range(num_runs): 137 | if "cpu" in func.__name__: 138 | func(image, seed, spacing, 10000, 1.0, 2) 139 | elif "gpu" in func.__name__ and torch.cuda.is_available(): 140 | func(image, seed, spacing, 10000, 1.0, 2) 141 | else: 142 | func( 143 | np.squeeze(image.cpu().numpy()), 144 | np.squeeze(seed.cpu().numpy()), 145 | spacing, 146 | 10000, 147 | 1.0, 148 | 2, 149 | ) 150 | 151 | time_taken_dict[func.__name__].append((time.time() - tic) / num_runs) 152 | print() 153 | 154 | return sizes_to_test, time_taken_dict 155 | 156 | 157 | def save_plot(sizes, time_taken_dict, figname): 158 | plt.figure() 159 | plt.grid() 160 | for key in time_taken_dict.keys(): 161 | if "cpu" in key and "raster" in key: 162 | plt.plot(sizes, time_taken_dict[key], "m-o", label="FastGeodis (cpu)") 163 | elif "gpu" in key and "raster" in key: 164 | plt.plot(sizes, time_taken_dict[key], "g-o", label="FastGeodis (gpu)") 165 | else: 166 | plt.plot(sizes, time_taken_dict[key], "r-o", label="Toivanen (cpu)") 167 | plt.legend() 168 | plt.xticks(sizes, [str(s) for s in sizes], rotation=45) 169 | plt.title(figname) 170 | plt.xlabel("Spatial size") 171 | plt.ylabel("Execution time (seconds)") 172 | plt.tight_layout() 173 | plt.savefig(os.path.join("figures", figname + ".png")) 174 | time_taken_dict['spatial_dim'] = sizes 175 | with open(os.path.join('figures', figname + '.json'), 'w') as fp: 176 | json.dump(time_taken_dict, fp, indent=4) 177 | 178 | 179 | if __name__ == "__main__": 180 | sizes, ttdict = test2d() 181 | save_plot(sizes, ttdict, "experiment_2d_toivanen") 182 | 183 | sizes, ttdict = test3d() 184 | save_plot(sizes, ttdict, "experiment_3d_toivanen") 185 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from gettext import install 2 | import glob 3 | import os 4 | import re 5 | import sys 6 | import warnings 7 | 8 | import pkg_resources 9 | from setuptools import find_packages, setup 10 | 11 | FORCE_CUDA = os.getenv("FORCE_CUDA", "0") == "1" 12 | 13 | BUILD_CPP = BUILD_CUDA = False 14 | TORCH_VERSION = 0 15 | try: 16 | import torch 17 | 18 | print(f"setup.py with torch {torch.__version__}") 19 | from torch.utils.cpp_extension import BuildExtension, CppExtension 20 | 21 | BUILD_CPP = True 22 | from torch.utils.cpp_extension import CUDA_HOME, CUDAExtension 23 | 24 | BUILD_CUDA = (CUDA_HOME is not None) if torch.cuda.is_available() else FORCE_CUDA 25 | 26 | _pt_version = pkg_resources.parse_version(torch.__version__)._version.release 27 | if _pt_version is None or len(_pt_version) < 3: 28 | raise AssertionError("unknown torch version") 29 | TORCH_VERSION = ( 30 | int(_pt_version[0]) * 10000 + int(_pt_version[1]) * 100 + int(_pt_version[2]) 31 | ) 32 | except (ImportError, TypeError, AssertionError, AttributeError) as e: 33 | warnings.warn(f"extension build skipped: {e}") 34 | finally: 35 | print( 36 | f"BUILD_CPP={BUILD_CPP}, BUILD_CUDA={BUILD_CUDA}, TORCH_VERSION={TORCH_VERSION}." 37 | ) 38 | 39 | 40 | def torch_parallel_backend(): 41 | try: 42 | match = re.search( 43 | "^ATen parallel backend: (?P.*)$", 44 | torch._C._parallel_info(), 45 | re.MULTILINE, 46 | ) 47 | if match is None: 48 | return None 49 | backend = match.group("backend") 50 | if backend == "OpenMP": 51 | return "AT_PARALLEL_OPENMP" 52 | if backend == "native thread pool": 53 | return "AT_PARALLEL_NATIVE" 54 | if backend == "native thread pool and TBB": 55 | return "AT_PARALLEL_NATIVE_TBB" 56 | except (NameError, AttributeError): # no torch or no binaries 57 | warnings.warn("Could not determine torch parallel_info.") 58 | return None 59 | 60 | 61 | def omp_flags(): 62 | if sys.platform == "win32": 63 | return ["/openmp"] 64 | if sys.platform == "darwin": 65 | # https://stackoverflow.com/questions/37362414/ 66 | # return ["-fopenmp=libiomp5"] 67 | return [] 68 | return ["-fopenmp"] 69 | 70 | 71 | def get_extensions(): 72 | # this_dir = os.path.dirname(os.path.abspath(__file__)) 73 | # ext_dir = os.path.join(this_dir, "src") 74 | ext_dir = "FastGeodis" 75 | include_dirs = [ext_dir] 76 | 77 | source_cpu = glob.glob(os.path.join(ext_dir, "**", "*.cpp"), recursive=True) 78 | source_cuda = glob.glob(os.path.join(ext_dir, "**", "*.cu"), recursive=True) 79 | 80 | extension = None 81 | define_macros = [(f"{torch_parallel_backend()}", 1)] 82 | extra_compile_args = {} 83 | extra_link_args = [] 84 | sources = source_cpu 85 | if BUILD_CPP: 86 | extension = CppExtension 87 | extra_compile_args.setdefault("cxx", []) 88 | if torch_parallel_backend() == "AT_PARALLEL_OPENMP": 89 | extra_compile_args["cxx"] += omp_flags() 90 | extra_link_args = omp_flags() 91 | if BUILD_CUDA: 92 | extension = CUDAExtension 93 | sources += source_cuda 94 | define_macros += [("WITH_CUDA", None)] 95 | extra_compile_args = {"cxx": [], "nvcc": []} 96 | if torch_parallel_backend() == "AT_PARALLEL_OPENMP": 97 | extra_compile_args["cxx"] += omp_flags() 98 | if extension is None or not sources: 99 | return [] # compile nothing 100 | 101 | # compile release 102 | extra_compile_args["cxx"] += ["-g0"] 103 | 104 | ext_modules = [ 105 | extension( 106 | name="FastGeodisCpp", 107 | sources=sources, 108 | include_dirs=include_dirs, 109 | define_macros=define_macros, 110 | extra_compile_args=extra_compile_args, 111 | extra_link_args=extra_link_args, 112 | ) 113 | ] 114 | return ext_modules 115 | 116 | def get_version(): 117 | # following guidance from: https://stackoverflow.com/a/7071358 118 | VERSIONFILE="FastGeodis/_version.py" 119 | verstrline = open(VERSIONFILE, "rt").read() 120 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" 121 | mo = re.search(VSRE, verstrline, re.M) 122 | if mo: 123 | verstr = mo.group(1) 124 | else: 125 | raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) 126 | return verstr 127 | 128 | # get current version 129 | version = get_version() 130 | print(f"FastGeodis building version: {version}") 131 | 132 | with open("README.md", "r") as fh: 133 | long_description = fh.read() 134 | 135 | with open("requirements.txt", "r") as fp: 136 | install_requires = fp.read().splitlines() 137 | 138 | # add dependencies folder in include path 139 | dep_dir = os.path.join(".", "dependency") 140 | 141 | setup( 142 | name="FastGeodis", 143 | version=version, 144 | description="Fast Implementation of Generalised Geodesic Distance Transform for CPU (OpenMP) and GPU (CUDA)", 145 | long_description=long_description, 146 | long_description_content_type="text/markdown", 147 | url="https://github.com/masadcv/FastGeodis", 148 | author="Muhammad Asad", 149 | author_email="masadcv@gmail.com", 150 | license="BSD-3-Clause License", 151 | classifiers=[ 152 | "License :: OSI Approved :: BSD License", 153 | "Programming Language :: Python :: 3", 154 | ], 155 | install_requires=install_requires, 156 | cmdclass={ 157 | "build_ext": BuildExtension 158 | }, # .with_options(no_python_abi_suffix=True)}, 159 | packages=find_packages(exclude=("data", "docs", "examples", "scripts", "tests")), 160 | zip_safe=False, 161 | ext_modules=get_extensions(), 162 | include_dirs=[dep_dir], 163 | ) 164 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | # Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | # All rights reserved. 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/test_fastmarch.py: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | # Copyright (c) 2021, Muhammad Asad (masadcv@gmail.com) 4 | # All rights reserved. 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import math 32 | import unittest 33 | 34 | import numpy as np 35 | import torch 36 | from parameterized import parameterized 37 | 38 | from .utils import * 39 | 40 | # set deterministic seed 41 | torch.manual_seed(15) 42 | np.random.seed(15) 43 | 44 | 45 | class TestFastMarch(unittest.TestCase): 46 | @parameterized.expand(CONF_ALL_CPU) 47 | @run_cuda_if_available 48 | def test_ill_shape(self, device, num_dims, base_dim): 49 | print(device) 50 | print(num_dims) 51 | 52 | # start with a good shape for image and mask 53 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 54 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 55 | 56 | geodis_func = get_fastmarch_func(num_dims=num_dims) 57 | 58 | # batch != 1 - unsupported 59 | image_shape_mod = image_shape.copy() 60 | mask_shape_mod = mask_shape.copy() 61 | with self.assertRaises(ValueError): 62 | mask_shape_mod[0] = 2 63 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 64 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 65 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 66 | 67 | with self.assertRaises(ValueError): 68 | image_shape_mod[0] = 2 69 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 70 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 71 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 72 | 73 | # spatial shape mismatch - unsupported 74 | image_shape_mod = image_shape.copy() 75 | mask_shape_mod = mask_shape.copy() 76 | with self.assertRaises(ValueError): 77 | image_shape_mod[-1] = 12 78 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 79 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 80 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 81 | 82 | # 3D shape for 2D functions - unsupported 83 | if num_dims == 2: 84 | image_shape_mod = image_shape.copy() 85 | mask_shape_mod = mask_shape.copy() 86 | with self.assertRaises(ValueError): 87 | image_shape_mod += [16] 88 | mask_shape_mod += [16] 89 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 90 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 91 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 92 | 93 | @parameterized.expand(CONF_ALL_CPU) 94 | @run_cuda_if_available 95 | def test_correct_shape(self, device, num_dims, base_dim): 96 | print(device) 97 | print(num_dims) 98 | 99 | # start with a good shape for image and mask 100 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 101 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 102 | 103 | geodis_func = get_fastmarch_func(num_dims=num_dims) 104 | 105 | image_shape_mod = image_shape.copy() 106 | mask_shape_mod = mask_shape.copy() 107 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 108 | mask = torch.ones(mask_shape_mod, dtype=torch.float32).to(device) 109 | indices = tuple([0] * len(mask_shape_mod)) 110 | mask[indices] = 0 111 | 112 | # should work without any errors 113 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 114 | 115 | @parameterized.expand(CONF_ALL_CPU) 116 | @run_cuda_if_available 117 | def test_euclidean_dist_output(self, device, num_dims, base_dim): 118 | """ 119 | Explanation: 120 | 121 | Taking euclidean distance with x==0 below: 122 | x------------- 123 | | | 124 | | | 125 | | | 126 | | | 127 | | | 128 | | | 129 | -------------- 130 | 131 | The max distance in euclidean output will be approx equal to 132 | distance to furthest corner (x->o) along diagonal shown below: 133 | x------------- 134 | | \ | 135 | | \ | 136 | | \ | 137 | | \ | 138 | | \ | 139 | | \| 140 | -------------o 141 | """ 142 | 143 | print(device) 144 | print(num_dims) 145 | 146 | # start with a good shape for image and mask 147 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 148 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 149 | 150 | geodis_func = get_fastmarch_func(num_dims=num_dims) 151 | 152 | # device mismatch for input - unsupported 153 | image = torch.ones(image_shape, dtype=torch.float32).to(device) 154 | mask = torch.ones(mask_shape, dtype=torch.float32).to(device) 155 | mask[0, 0, 0, 0] = 0 156 | 157 | geodesic_dist = geodis_func(image, mask, 0.0, 2) 158 | pred_max_dist = geodesic_dist.cpu().numpy().max() 159 | exp_max_dist = math.sqrt(num_dims * (base_dim**2)) 160 | tolerance = 10 if num_dims == 2 else 100 # more tol needed for 3d approx 161 | 162 | check = exp_max_dist - tolerance < pred_max_dist < exp_max_dist + tolerance 163 | self.assertTrue(check) 164 | 165 | @parameterized.expand(CONF_3D_CPU) 166 | @run_cuda_if_available 167 | def test_ill_spacing(self, device, num_dims, base_dim): 168 | print(device) 169 | print(num_dims) 170 | 171 | # start with a good shape for image and mask 172 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 173 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 174 | 175 | # device mismatch for input - unsupported 176 | image = torch.zeros(image_shape, dtype=torch.float32).to(device) 177 | mask = torch.zeros(mask_shape, dtype=torch.float32).to(device) 178 | 179 | spacing = [1.0, 1.0] 180 | geodis_func = get_fastmarch_func(num_dims=num_dims, spacing=spacing) 181 | 182 | with self.assertRaises(ValueError): 183 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 184 | 185 | 186 | class TestFastMarchSigned(unittest.TestCase): 187 | @parameterized.expand(CONF_ALL_CPU) 188 | def test_ill_shape(self, device, num_dims, base_dim): 189 | print(device) 190 | print(num_dims) 191 | 192 | # start with a good shape for image and mask 193 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 194 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 195 | 196 | geodis_func = get_signed_fastmarch_func(num_dims=num_dims) 197 | 198 | # batch != 1 - unsupported 199 | image_shape_mod = image_shape.copy() 200 | mask_shape_mod = mask_shape.copy() 201 | with self.assertRaises(ValueError): 202 | mask_shape_mod[0] = 2 203 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 204 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 205 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 206 | 207 | with self.assertRaises(ValueError): 208 | image_shape_mod[0] = 2 209 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 210 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 211 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 212 | 213 | # spatial shape mismatch - unsupported 214 | image_shape_mod = image_shape.copy() 215 | mask_shape_mod = mask_shape.copy() 216 | with self.assertRaises(ValueError): 217 | image_shape_mod[-1] = 12 218 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 219 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 220 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 221 | 222 | # 3D shape for 2D functions - unsupported 223 | if num_dims == 2: 224 | image_shape_mod = image_shape.copy() 225 | mask_shape_mod = mask_shape.copy() 226 | with self.assertRaises(ValueError): 227 | image_shape_mod += [16] 228 | mask_shape_mod += [16] 229 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 230 | mask = torch.rand(mask_shape_mod, dtype=torch.float32).to(device) 231 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 232 | 233 | @parameterized.expand(CONF_ALL_CPU) 234 | def test_correct_shape(self, device, num_dims, base_dim): 235 | print(device) 236 | print(num_dims) 237 | 238 | # start with a good shape for image and mask 239 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 240 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 241 | 242 | geodis_func = get_signed_fastmarch_func(num_dims=num_dims) 243 | 244 | image_shape_mod = image_shape.copy() 245 | mask_shape_mod = mask_shape.copy() 246 | image = torch.ones(image_shape_mod, dtype=torch.float32).to(device) 247 | mask = torch.ones(mask_shape_mod, dtype=torch.float32).to(device) 248 | indices = tuple([0] * len(mask_shape_mod)) 249 | mask[indices] = 0 250 | 251 | # should work without any errors 252 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 253 | 254 | @parameterized.expand(CONF_3D_CPU) 255 | def test_ill_spacing(self, device, num_dims, base_dim): 256 | print(device) 257 | print(num_dims) 258 | 259 | # start with a good shape for image and mask 260 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 261 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 262 | 263 | # device mismatch for input - unsupported 264 | image = torch.zeros(image_shape, dtype=torch.float32).to(device) 265 | mask = torch.ones(mask_shape, dtype=torch.float32).to(device) 266 | indices = tuple([0] * len(mask_shape)) 267 | mask[indices] = 0 268 | 269 | spacing = [1.0, 1.0] 270 | geodis_func = get_signed_fastmarch_func(num_dims=num_dims, spacing=spacing) 271 | 272 | with self.assertRaises(ValueError): 273 | geodesic_dist = geodis_func(image, mask, 1.0, 2) 274 | 275 | 276 | class TestGSFFastMarch(unittest.TestCase): 277 | @parameterized.expand(CONF_ALL_CPU_FM) 278 | @run_cuda_if_available 279 | def test_correct_shape(self, device, num_dims, base_dim): 280 | print(device) 281 | print(num_dims) 282 | 283 | # start with a good shape for image and mask 284 | image_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 285 | mask_shape = get_simple_shape(base_dim=base_dim, num_dims=num_dims) 286 | 287 | geodis_func = get_GSF_fastmarch_func(num_dims=num_dims) 288 | 289 | image_shape_mod = image_shape.copy() 290 | mask_shape_mod = mask_shape.copy() 291 | image = torch.rand(image_shape_mod, dtype=torch.float32).to(device) 292 | mask = torch.ones(mask_shape_mod, dtype=torch.float32).to(device) 293 | indices = tuple([0] * len(mask_shape_mod)) 294 | mask[indices] = 0 295 | 296 | # should work without any errors 297 | geodesic_dist = geodis_func(image, mask, 0.0, 1.0, 2) 298 | 299 | 300 | if __name__ == "__main__": 301 | unittest.main() 302 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from functools import partial, wraps 2 | import unittest 3 | import torch 4 | 5 | try: 6 | import FastGeodis 7 | except: 8 | print( 9 | "Unable to load FastGeodis for unittests\nMake sure to install using: python setup.py install" 10 | ) 11 | exit() 12 | 13 | DEVICES_TO_RUN = ["cpu", "cuda"] 14 | CONF_2D_CPU = [("cpu", 2, bas) for bas in [16, 32, 64]] 15 | CONF_2D_CUDA = [("cuda", 2, bas) for bas in [16, 32, 64]] 16 | CONF_2D = CONF_2D_CPU + CONF_2D_CUDA 17 | 18 | CONF_3D_CPU = [("cpu", 3, bas) for bas in [16, 32, 64]] 19 | CONF_3D_CUDA = [("cuda", 3, bas) for bas in [16, 32, 64]] 20 | CONF_3D = CONF_3D_CPU + CONF_3D_CUDA 21 | CONF_ALL = CONF_2D + CONF_3D 22 | 23 | CONF_ALL_CPU = CONF_2D_CPU + CONF_3D_CPU 24 | 25 | # Fast March is compute intensive, so limit 2d and 3d cases to run with it 26 | CONF_2D_CPU_FM = [CONF_2D_CPU[0]] 27 | CONF_3D_CPU_FM = [CONF_3D_CPU[0]] 28 | CONF_ALL_CPU_FM = CONF_2D_CPU_FM + CONF_3D_CPU_FM 29 | 30 | 31 | 32 | def skip_if_no_cuda(obj): 33 | return unittest.skipUnless(torch.cuda.is_available(), "Skipping CUDA-based tests")( 34 | obj 35 | ) 36 | 37 | 38 | def run_cuda_if_available(fn): 39 | @wraps(fn) 40 | def wrapper(*args, **kwargs): 41 | if args[1] == "cuda": 42 | if torch.cuda.is_available(): 43 | return fn(*args, **kwargs) 44 | else: 45 | raise unittest.SkipTest("skipping as cuda device not found") 46 | else: 47 | return fn(*args, **kwargs) 48 | 49 | return wrapper 50 | 51 | 52 | def fastgeodis_generalised_geodesic_distance_2d(image, softmask, v, lamb, iter): 53 | return FastGeodis.generalised_geodesic2d(image, softmask, v, lamb, iter) 54 | 55 | 56 | def fastgeodis_generalised_geodesic_distance_3d( 57 | image, softmask, v, lamb, iter, spacing 58 | ): 59 | return FastGeodis.generalised_geodesic3d(image, softmask, spacing, v, lamb, iter) 60 | 61 | 62 | def fastgeodis_signed_generalised_geodesic_distance_2d(image, softmask, v, lamb, iter): 63 | return FastGeodis.signed_generalised_geodesic2d(image, softmask, v, lamb, iter) 64 | 65 | 66 | def fastgeodis_signed_generalised_geodesic_distance_3d( 67 | image, softmask, v, lamb, iter, spacing 68 | ): 69 | return FastGeodis.signed_generalised_geodesic3d( 70 | image, softmask, spacing, v, lamb, iter 71 | ) 72 | 73 | 74 | def toivanen_signed_generalised_geodesic_distance_2d(image, softmask, v, lamb, iter): 75 | return FastGeodis.signed_generalised_geodesic2d_toivanen( 76 | image, softmask, v, lamb, iter 77 | ) 78 | 79 | 80 | def toivanen_signed_generalised_geodesic_distance_3d( 81 | image, softmask, v, lamb, iter, spacing 82 | ): 83 | return FastGeodis.signed_generalised_geodesic3d_toivanen( 84 | image, softmask, spacing, v, lamb, iter 85 | ) 86 | 87 | 88 | def pixelqueue_signed_generalised_geodesic_distance_2d(image, softmask, lamb, iter): 89 | return FastGeodis.signed_geodesic2d_pixelqueue(image, softmask, lamb) 90 | 91 | 92 | def pixelqueue_signed_generalised_geodesic_distance_3d( 93 | image, softmask, lamb, iter, spacing 94 | ): 95 | return FastGeodis.signed_geodesic3d_pixelqueue( 96 | image, softmask, spacing, lamb 97 | ) 98 | 99 | def fastmarch_signed_generalised_geodesic_distance_2d(image, softmask, lamb, iter): 100 | return FastGeodis.signed_geodesic2d_fastmarch(image, softmask, lamb) 101 | 102 | 103 | def fastmarch_signed_generalised_geodesic_distance_3d( 104 | image, softmask, lamb, iter, spacing 105 | ): 106 | return FastGeodis.signed_geodesic3d_fastmarch( 107 | image, softmask, spacing, lamb 108 | ) 109 | 110 | def toivanen_generalised_geodesic_distance_2d(image, softmask, v, lamb, iter): 111 | return FastGeodis.generalised_geodesic2d_toivanen(image, softmask, v, lamb, iter) 112 | 113 | 114 | def toivanen_generalised_geodesic_distance_3d(image, softmask, v, lamb, iter, spacing): 115 | return FastGeodis.generalised_geodesic3d_toivanen( 116 | image, softmask, spacing, v, lamb, iter 117 | ) 118 | 119 | 120 | def pixelqueue_geodesic_distance_2d(image, softmask, lamb, iter): 121 | return FastGeodis.geodesic2d_pixelqueue(image, softmask, lamb) 122 | 123 | 124 | def pixelqueue_geodesic_distance_3d(image, softmask, lamb, iter, spacing): 125 | return FastGeodis.geodesic3d_pixelqueue( 126 | image, softmask, spacing, lamb 127 | ) 128 | 129 | def fastmarch_geodesic_distance_2d(image, softmask, lamb, iter): 130 | return FastGeodis.geodesic2d_fastmarch(image, softmask, lamb) 131 | 132 | 133 | def fastmarch_geodesic_distance_3d(image, softmask, lamb, iter, spacing): 134 | return FastGeodis.geodesic3d_fastmarch( 135 | image, softmask, spacing, lamb 136 | ) 137 | 138 | 139 | def fastgeodis_GSF_2d(image, softmask, theta, v, lamb, iter): 140 | return FastGeodis.GSF2d(image, softmask, theta, v, lamb, iter) 141 | 142 | 143 | def fastgeodis_GSF_3d(image, softmask, theta, v, lamb, iter, spacing): 144 | return FastGeodis.GSF3d(image, softmask, theta, spacing, v, lamb, iter) 145 | 146 | 147 | def toivanen_GSF_2d(image, softmask, theta, v, lamb, iter): 148 | return FastGeodis.GSF2d_toivanen(image, softmask, theta, v, lamb, iter) 149 | 150 | 151 | def toivanen_GSF_3d(image, softmask, theta, v, lamb, iter, spacing): 152 | return FastGeodis.GSF3d_toivanen(image, softmask, theta, spacing, v, lamb, iter) 153 | 154 | 155 | def pixelqueue_GSF_2d(image, softmask, theta, lamb, iter): 156 | return FastGeodis.GSF2d_pixelqueue(image, softmask, theta, lamb) 157 | 158 | 159 | def pixelqueue_GSF_3d(image, softmask, theta, lamb, iter, spacing): 160 | return FastGeodis.GSF3d_pixelqueue(image, softmask, theta, spacing, lamb) 161 | 162 | def fastmarch_GSF_2d(image, softmask, theta, lamb, iter): 163 | return FastGeodis.GSF2d_fastmarch(image, softmask, theta, lamb) 164 | 165 | 166 | def fastmarch_GSF_3d(image, softmask, theta, lamb, iter, spacing): 167 | return FastGeodis.GSF3d_fastmarch(image, softmask, theta, spacing, lamb) 168 | 169 | 170 | def get_simple_shape(base_dim, num_dims): 171 | return [1, 1] + [ 172 | base_dim, 173 | ] * num_dims 174 | 175 | 176 | def get_fastgeodis_func(num_dims, spacing=[1.0, 1.0, 1.0]): 177 | if num_dims == 2: 178 | return fastgeodis_generalised_geodesic_distance_2d 179 | elif num_dims == 3: 180 | return partial(fastgeodis_generalised_geodesic_distance_3d, spacing=spacing) 181 | else: 182 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 183 | 184 | 185 | def get_signed_fastgeodis_func(num_dims, spacing=[1.0, 1.0, 1.0]): 186 | if num_dims == 2: 187 | return fastgeodis_signed_generalised_geodesic_distance_2d 188 | elif num_dims == 3: 189 | return partial( 190 | fastgeodis_signed_generalised_geodesic_distance_3d, spacing=spacing 191 | ) 192 | else: 193 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 194 | 195 | 196 | def get_toivanen_func(num_dims, spacing=[1.0, 1.0, 1.0]): 197 | if num_dims == 2: 198 | return toivanen_generalised_geodesic_distance_2d 199 | elif num_dims == 3: 200 | return partial(toivanen_generalised_geodesic_distance_3d, spacing=spacing) 201 | else: 202 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 203 | 204 | 205 | def get_signed_toivanen_func(num_dims, spacing=[1.0, 1.0, 1.0]): 206 | if num_dims == 2: 207 | return toivanen_signed_generalised_geodesic_distance_2d 208 | elif num_dims == 3: 209 | return partial( 210 | toivanen_signed_generalised_geodesic_distance_3d, spacing=spacing 211 | ) 212 | else: 213 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 214 | 215 | 216 | def get_pixelqueue_func(num_dims, spacing=[1.0, 1.0, 1.0]): 217 | if num_dims == 2: 218 | return pixelqueue_geodesic_distance_2d 219 | elif num_dims == 3: 220 | return partial(pixelqueue_geodesic_distance_3d, spacing=spacing) 221 | else: 222 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 223 | 224 | 225 | def get_signed_pixelqueue_func(num_dims, spacing=[1.0, 1.0, 1.0]): 226 | if num_dims == 2: 227 | return pixelqueue_signed_generalised_geodesic_distance_2d 228 | elif num_dims == 3: 229 | return partial( 230 | pixelqueue_signed_generalised_geodesic_distance_3d, spacing=spacing 231 | ) 232 | else: 233 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 234 | 235 | def get_fastmarch_func(num_dims, spacing=[1.0, 1.0, 1.0]): 236 | if num_dims == 2: 237 | return fastmarch_geodesic_distance_2d 238 | elif num_dims == 3: 239 | return partial(fastmarch_geodesic_distance_3d, spacing=spacing) 240 | else: 241 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 242 | 243 | 244 | def get_signed_fastmarch_func(num_dims, spacing=[1.0, 1.0, 1.0]): 245 | if num_dims == 2: 246 | return fastmarch_signed_generalised_geodesic_distance_2d 247 | elif num_dims == 3: 248 | return partial( 249 | fastmarch_signed_generalised_geodesic_distance_3d, spacing=spacing 250 | ) 251 | else: 252 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 253 | 254 | 255 | def get_GSF_func(num_dims, spacing=[1.0, 1.0, 1.0]): 256 | if num_dims == 2: 257 | return fastgeodis_GSF_2d 258 | elif num_dims == 3: 259 | return partial(fastgeodis_GSF_3d, spacing=spacing) 260 | else: 261 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 262 | 263 | 264 | def get_GSF_toivanen_func(num_dims, spacing=[1.0, 1.0, 1.0]): 265 | if num_dims == 2: 266 | return toivanen_GSF_2d 267 | elif num_dims == 3: 268 | return partial(toivanen_GSF_3d, spacing=spacing) 269 | else: 270 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 271 | 272 | 273 | def get_GSF_pixelqueue_func(num_dims, spacing=[1.0, 1.0, 1.0]): 274 | if num_dims == 2: 275 | return pixelqueue_GSF_2d 276 | elif num_dims == 3: 277 | return partial(pixelqueue_GSF_3d, spacing=spacing) 278 | else: 279 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 280 | 281 | 282 | 283 | def get_GSF_fastmarch_func(num_dims, spacing=[1.0, 1.0, 1.0]): 284 | if num_dims == 2: 285 | return fastmarch_GSF_2d 286 | elif num_dims == 3: 287 | return partial(fastmarch_GSF_3d, spacing=spacing) 288 | else: 289 | raise ValueError("Unsupported num_dims received: {}".format(num_dims)) 290 | 291 | --------------------------------------------------------------------------------