├── .github └── workflows │ ├── check.yml │ └── deploy_pypi.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── README.md ├── apple.rst ├── conf.py ├── dev_notes │ └── _Back_3D_tilted.sphere_points_from_angles_and_tilt.pdf ├── ex2d.rst ├── ex3d.rst ├── extensions │ ├── fancy_include.py │ └── github_changelog.py ├── index.rst ├── odtbrain.bib ├── processing.rst ├── recon_2d.rst ├── recon_3d.rst ├── requirements.txt ├── sec_changelog.rst ├── sec_code_reference.rst ├── sec_examples.rst ├── sec_introduction.rst └── zsec_bib.rst ├── examples ├── backprop_from_fdtd_2d.jpg ├── backprop_from_fdtd_2d.py ├── backprop_from_fdtd_3d.jpg ├── backprop_from_fdtd_3d.py ├── backprop_from_fdtd_3d_tilted.jpg ├── backprop_from_fdtd_3d_tilted.py ├── backprop_from_fdtd_3d_tilted2.jpg ├── backprop_from_fdtd_3d_tilted2.py ├── backprop_from_mie_2d_cylinder_offcenter.jpg ├── backprop_from_mie_2d_cylinder_offcenter.py ├── backprop_from_mie_2d_incomplete_coverage.jpg ├── backprop_from_mie_2d_incomplete_coverage.py ├── backprop_from_mie_2d_weights_angles.jpg ├── backprop_from_mie_2d_weights_angles.py ├── backprop_from_mie_3d_sphere.jpg ├── backprop_from_mie_3d_sphere.py ├── backprop_from_qlsi_3d_hl60.jpg ├── backprop_from_qlsi_3d_hl60.py ├── backprop_from_rytov_3d_phantom_apple.jpg ├── backprop_from_rytov_3d_phantom_apple.py ├── data │ ├── fdtd_2d_sino_A100_R13.zip │ ├── fdtd_3d_sino_A180_R6.500.tar.lzma │ ├── fdtd_3d_sino_A220_R6.500_tiltyz0.2.tar.lzma │ ├── mie_2d_noncentered_cylinder_A250_R2.zip │ ├── mie_3d_sphere_field.zip │ └── qlsi_3d_hl60-cell_A140.tar.lzma ├── example_helper.py ├── generate_example_images.py └── requirements.txt ├── misc ├── Readme.md ├── meep_phantom_2d.cpp └── meep_phantom_3d.cpp ├── odtbrain ├── __init__.py ├── _alg2d_bpp.py ├── _alg2d_fmp.py ├── _alg2d_int.py ├── _alg3d_bpp.py ├── _alg3d_bppt.py ├── _prepare_sino.py ├── _translate_ri.py ├── apple.py ├── util.py └── warn.py ├── pyproject.toml └── tests ├── README.md ├── common_methods.py ├── data ├── alg2d_bpp__test_2d_backprop_full.zip ├── alg2d_bpp__test_2d_backprop_phase.zip ├── alg2d_fmp__test_2d_fmap.zip ├── alg2d_int__test_2d_integrate.zip ├── alg3d_bpp__test_3d_backprop_nopadreal.zip ├── alg3d_bpp__test_3d_backprop_phase.zip ├── alg3d_bpp__test_3d_mprotate.zip ├── apple__test_correct_reproduce.zip ├── processing__test_odt_to_ri.zip ├── processing__test_opt_to_ri.zip ├── processing__test_sino_radon.zip └── processing__test_sino_rytov.zip ├── requirements.txt ├── test_alg2d_bpp.py ├── test_alg2d_fmp.py ├── test_alg2d_int.py ├── test_alg3d_bpp.py ├── test_alg3d_bppt.py ├── test_angle_weights.py ├── test_apple.py ├── test_copy.py ├── test_counters.py ├── test_processing.py ├── test_rotation_matrices.py ├── test_save_memory.py ├── test_spherecoords_from_angles_and_axis.py ├── test_util.py └── test_weighting.py /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ['3.9', '3.10'] 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | cache: 'pip' 24 | check-latest: true 25 | - name: Install fftw3 libs (Linux) 26 | if: runner.os == 'Linux' 27 | run: | 28 | sudo apt-get install -y libfftw3-dev libfftw3-double3 29 | - name: Install fftw3 libs (macOS) 30 | if: runner.os == 'macOS' 31 | run: | 32 | brew install --overwrite fftw 33 | - name: Install dependencies 34 | run: | 35 | # prerequisites 36 | python -m pip install --upgrade pip wheel 37 | python -m pip install coverage flake8 38 | # install dependencies 39 | pip install -e . 40 | pip install -r tests/requirements.txt 41 | # show installed packages 42 | pip freeze 43 | - name: Test with pytest 44 | run: | 45 | coverage run --source=odtbrain -m pytest tests 46 | - name: Upload test artifacts 47 | if: (runner.os == 'Linux' && matrix.python-version == '3.10') 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: Test_artifacts 51 | path: | 52 | ./*.zip 53 | 54 | - name: Lint with flake8 55 | run: | 56 | flake8 --exclude _version.py . 57 | - name: Upload coverage to Codecov 58 | uses: codecov/codecov-action@v3 59 | -------------------------------------------------------------------------------- /.github/workflows/deploy_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build_sdist_wheel: 10 | name: Build source distribution 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@main 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Build sdist 18 | run: pipx run build --sdist --wheel 19 | 20 | - name: publish 21 | env: 22 | TWINE_USERNAME: __token__ 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PWD }} 24 | run: | 25 | pipx install twine 26 | twine upload --skip-existing dist/* 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | 60 | *.md~ 61 | *.txt~ 62 | *.py~ 63 | *.in~ 64 | *.yml~ 65 | *.rst~ 66 | 67 | _version_save.py 68 | _version.py 69 | 70 | # extracted test files 71 | tests/data/*__*.txt 72 | *.tar 73 | .env 74 | .pytest_cache 75 | *bib.bak 76 | *bib.sav 77 | 78 | .idea 79 | 80 | # used by pyenv-virtualenv: 81 | .python-version -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | formats: 3 | - pdf 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.11" 8 | jobs: 9 | post_checkout: 10 | - git fetch --unshallow || true 11 | sphinx: 12 | configuration: docs/conf.py 13 | python: 14 | install: 15 | - requirements: docs/requirements.txt 16 | - method: pip 17 | path: . 18 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.4.12 2 | - docs: update outdated 2D FDTD example image 3 | 0.4.11 4 | - docs: clarify reconstruction distance in 2D example 5 | 0.4.10 6 | - maintenance release 7 | 0.4.9 8 | - maintenance release 9 | 0.4.8 10 | - fix: support numpy 2 (#23) 11 | - setup: bumpy scikit-image to 0.21.0 (API) 12 | - setup: migrate to pyproject.toml 13 | - fix: cut-off radius too small in fmap_2d algorithm (#19) 14 | 0.4.7 15 | - maintenance release 16 | 0.4.6 17 | - fix: handle more special cases when computing weights for 18 | projections 19 | - docs: update example scripts and images 20 | 0.4.5 21 | - ci: fix build pipeline 22 | 0.4.4 23 | - ref: address scipy deprecation warnings 24 | - ref: replace print statements with warnings (#17) 25 | - ci: switch to Python 3.10 26 | - ci: minor cleanup 27 | 0.4.3 28 | - docs: added original meep C++ simulation files in "misc" (#14) 29 | - docs: add ``if __name__ == "__main__"`` guard to 3D backpropagation 30 | examples (#15) 31 | 0.4.2 32 | - build: migrate to GitHub Actions 33 | - build: setup.py test is deprecated 34 | - docs: refurbish docs 35 | - ref: change instances of np.int to int due to 36 | numpy deprecation warnings 37 | 0.4.1 38 | - setup: bump scipy to 1.4.0 (updated QHull in griddata) 39 | 0.4.0 40 | - BREAKING CHANGES: 41 | - renamed submodule `_preproc` to `_prepare_sino` 42 | - renamed submodule `_postproc` to `_translate_ri` 43 | - missing apple core correction is now applied to the 44 | object function (`f`) instead of the refractive index (`n`), 45 | which is the physically correct approach 46 | - default keyword for `padval` is now "edge" 47 | instead of `None`; the meaning is retained 48 | - enh: added symmetric histogram apple core correction method "sh" 49 | - fix: using "float32" dtype in 3D backpropagation lead to 50 | casting error in numexpr 51 | - enh: improve performance when padding is disabled 52 | - docs: minor update 53 | 0.3.0 54 | - feat: basic missing apple core correction (#6) 55 | - docs: reordered 3D examples (decreasing importance) 56 | 0.2.6 57 | - fix: make phase unwrapping deterministic 58 | - tests: remove one test of the 2D Fourier mapping algorithm due to 59 | instabilities in using scipy.interpolate.griddata 60 | - ref: use `empty_aligned` instead of deprecated `n_byte_align_empty` 61 | - docs: add hint for windows users how to run the 3D examples 62 | 0.2.5 63 | - fix: reconstruction volume rotated by 180° due to floating point 64 | inaccuracies (affects `backpropagate_3d_tilted`). 65 | 0.2.4 66 | - maintenance release 67 | 0.2.3 68 | - enh: employ slice-wise padding to reduce the memory usage (and 69 | possibly the computation time) of 70 | - basic 3D backpropagation algorithm (#7) 71 | - 3D backpropagation with a tilted axis of rotation (#9) 72 | - ref: replace asserts with raises (#8) 73 | - ref: multiprocessing-based rotation does not anymore require 74 | a variable (_shared_array) at the top level of the module; As a 75 | result, multiprocessing-rotation should now also work on Windows. 76 | 0.2.2 77 | - docs: minor update and add changelog 78 | 0.2.1 79 | - fix: Allow sinogram data type other than complex128 80 | - docs: Add example with experimental data (#3) 81 | - ci: automated deployment with travis-ci 82 | 0.2.0 83 | - BREAKING CHANGES: 84 | - Dropped support for Python 2 85 | - Renamed `sum_2d` to `integrate_2d` 86 | - Refactoring (#4, #5): 87 | - Moved each reconstruction algorithm to a separate file 88 | - Modified code to comply with PEP8 89 | - Moved long doc strings from source to docs directory 90 | - Migrate from unwrap to scikit-image 91 | - Cleaned up example scripts 92 | - Bugfixes: 93 | - Mistake in "negative-modulo" method for determination of 94 | 2PI sinogram phase offsets 95 | 0.1.8 96 | - Updated documentation 97 | - Cleaned up examples 98 | 0.1.7 99 | - Move documentation from GitHub to readthedocs.io 100 | - Add universal wheel on PyPI 101 | - Update tests on travis with new versions of NumPy 102 | 0.1.6 103 | - Bugfixes: 104 | - size of reconstruction volume in z too large for cases where 105 | the y-size is larger than the x-size of the sinogram images 106 | - `backpropagate_3d_tilted` used wrong shape of projections 107 | - 3D backpropagation methods did not use power-of-two padding size 108 | 0.1.5 109 | - Code optimization (speed, memory) with numexpr 110 | - New keyword argument `save_memory` for 3D reconstruction 111 | on machines with limited memory 112 | - New keyword argument `copy` for 3D reconstruction to protect 113 | input sinogram data. 114 | 0.1.4 115 | - The exponential term containing the distance between center 116 | of rotation and detector `lD` is now multiplied with 117 | the factor `M-1` instead of `M`. This is necessary, 118 | because usually the scattered wave is normalized in both 119 | amplitude and phase (`u_0`) and not only amplitude `a_0` 120 | - Allow angles of shape (A,1) in `backpropagate_3d_tilted` 121 | - Set default value lD=0 for all reconstruction algorithms 122 | - Improvement of documentation 123 | 0.1.3 124 | - Fixes for `backpropagate_3d_tilted` when `angles` are 125 | points on the unit sphere: 126 | - Make sure each point is normalized 127 | - Correctly rotate each point w.r.t. `tilted_axis` 128 | 0.1.2 129 | - Added reconstruction algorithm for tilted axis of rotation 130 | 0.1.1 131 | - Support NumPy 1.10. 132 | - Allow to weight backpropagation using keyword `weight_angles` 133 | - Bugfix: backpropagate_3d with keyword `onlyreal=True` did not work 134 | - Bugfix: sum_2d did not return correctly shaped array 135 | - Code coverage is now 90% 136 | - Added more examples to the documentation 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Paul Müller 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of ODTbrain nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include LICENSE 3 | include README.rst 4 | recursive-include examples *.py *.jpg 5 | recursive-include docs *.py *.md *.rst *.txt *.bib 6 | recursive-include tests *.py *.md *.zip 7 | prune docs/_build 8 | exclude docs/_version_save.py 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ODTbrain 2 | ======== 3 | 4 | |PyPI Version| |Tests Status| |Coverage Status| |Docs Status| 5 | 6 | 7 | **ODTbrain** provides image reconstruction algorithms for **O**\ ptical **D**\ iffraction **T**\ omography with a **B**\ orn and **R**\ ytov 8 | **A**\ pproximation-based **I**\ nversion to compute the refractive index (**n**\ ) in 2D and in 3D. 9 | 10 | 11 | Documentation 12 | ------------- 13 | 14 | The documentation, including the reference and examples, is available at `odtbrain.readthedocs.io `__. 15 | 16 | 17 | Installation 18 | ------------ 19 | :: 20 | 21 | pip install odtbrain 22 | 23 | 24 | 25 | Testing 26 | ------- 27 | 28 | After cloning into odtbrain, create a virtual environment:: 29 | 30 | virtualenv --system-site-packages env 31 | source env/bin/activate 32 | 33 | Install ODTbrain in editable mode:: 34 | 35 | pip install -e . 36 | 37 | Running an example:: 38 | 39 | python examples/backprop_from_fdtd_2d.py 40 | 41 | Running tests:: 42 | 43 | pip install pytest 44 | pytest tests 45 | 46 | 47 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/odtbrain.svg 48 | :target: https://pypi.python.org/pypi/odtbrain 49 | .. |Tests Status| image:: https://img.shields.io/github/actions/workflow/status/RI-Imaging/ODTbrain/check.yml 50 | :target: https://github.com/RI-Imaging/ODTbrain/actions?query=workflow%3AChecks 51 | .. |Coverage Status| image:: https://img.shields.io/codecov/c/github/RI-imaging/ODTbrain/master.svg 52 | :target: https://codecov.io/gh/RI-imaging/ODTbrain 53 | .. |Docs Status| image:: https://readthedocs.org/projects/odtbrain/badge/?version=latest 54 | :target: https://readthedocs.org/projects/odtbrain/builds/ 55 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ODTbrain documentation 2 | ====================== 3 | To install the requirements for building the documentation, run 4 | 5 | pip install -r requirements.txt 6 | 7 | To compile the documentation, run 8 | 9 | sphinx-build . _build 10 | 11 | -------------------------------------------------------------------------------- /docs/apple.rst: -------------------------------------------------------------------------------- 1 | 3D Apple core correction 2 | ------------------------ 3 | The missing apple core (in Fourier space) leads to ringing and blurring 4 | artifacts in optical diffraction tomography :cite:`Vertu2009`. 5 | This module contains basic functions that can be used to attenuate 6 | these artifacts. 7 | 8 | .. versionadded:: 0.3.0 9 | 10 | .. versionchanged:: 0.4.0 11 | 12 | .. automodule:: odtbrain.apple 13 | :members: 14 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # project documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Feb 22 09:35:49 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import os.path as op 17 | import sys 18 | 19 | import odtbrain 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | 25 | # include parent directory 26 | pdir = op.dirname(op.dirname(op.abspath(__file__))) 27 | sys.path.insert(0, pdir) 28 | 29 | sys.path.append(op.abspath('extensions')) 30 | 31 | # http://www.sphinx-doc.org/en/stable/ext/autodoc.html#confval-autodoc_member_order 32 | # Order class attributes and functions in separate blocks 33 | autodoc_member_order = 'bysource' 34 | 35 | # Display link to GitHub repo instead of doc on rtfd 36 | rst_prolog = """ 37 | :github_url: https://github.com/RI-imaging/ODTbrain 38 | """ 39 | 40 | # -- General configuration ------------------------------------------------ 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = ['sphinx.ext.intersphinx', 46 | 'sphinx.ext.autodoc', 47 | 'sphinx.ext.mathjax', 48 | 'sphinx.ext.autosummary', 49 | 'sphinx.ext.napoleon', 50 | 'sphinxcontrib.bibtex', 51 | 'fancy_include', 52 | 'github_changelog', 53 | ] 54 | 55 | # specify bibtex files (required for sphinxcontrib.bibtex>=2.0) 56 | bibtex_bibfiles = ['odtbrain.bib'] 57 | 58 | # Add any paths that contain templates here, relative to this directory. 59 | templates_path = ['_templates'] 60 | 61 | # The suffix of source filenames. 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # General information about the project. 68 | year = "2015" 69 | name = "odtbrain" 70 | author = "Paul Müller" 71 | authors = [author] 72 | projectname = name 73 | 74 | # The version info for the project you're documenting, acts as replacement for 75 | # |version| and |release|, also used in various other places throughout the 76 | # built documents. 77 | # 78 | # The short X.Y version. 79 | # 80 | # The full version, including alpha/beta/rc tags. 81 | # This gets 'version' 82 | version = odtbrain.__version__ 83 | release = version 84 | 85 | project = projectname 86 | copyright = year + ", " + author # @ReservedAssignment 87 | github_project = 'RI-imaging/' + project 88 | 89 | # The language for content autogenerated by Sphinx. Refer to documentation 90 | # for a list of supported languages. 91 | # language = None 92 | 93 | # There are two options for replacing |today|: either, you set today to some 94 | # non-false value, then it is used: 95 | # today = '' 96 | # Else, today_fmt is used as the format for a strftime call. 97 | # today_fmt = '%B %d, %Y' 98 | 99 | # List of patterns, relative to source directory, that match files and 100 | # directories to ignore when looking for source files. 101 | exclude_patterns = ['_build'] 102 | 103 | # The reST default role (used for this markup: `text`) to use for all 104 | # documents. 105 | # default_role = None 106 | 107 | # If true, '()' will be appended to :func: etc. cross-reference text. 108 | # add_function_parentheses = True 109 | 110 | # If true, the current module name will be prepended to all description 111 | # unit titles (such as .. function::). 112 | # add_module_names = True 113 | 114 | # If true, sectionauthor and moduleauthor directives will be shown in the 115 | # output. They are ignored by default. 116 | # show_authors = False 117 | 118 | # The name of the Pygments (syntax highlighting) style to use. 119 | # pygments_style = 'default' 120 | 121 | # A list of ignored prefixes for module index sorting. 122 | # modindex_common_prefix = [] 123 | 124 | # If true, keep warnings as "system message" paragraphs in the built documents. 125 | # keep_warnings = False 126 | 127 | 128 | # -- Options for HTML output ---------------------------------------------- 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | html_theme = 'sphinx_rtd_theme' 133 | 134 | # Output file base name for HTML help builder. 135 | htmlhelp_basename = projectname+'doc' 136 | 137 | 138 | # -- Options for LaTeX output --------------------------------------------- 139 | 140 | latex_elements = { 141 | # The paper size ('letterpaper' or 'a4paper'). 142 | # 'papersize': 'letterpaper', 143 | 144 | # The font size ('10pt', '11pt' or '12pt'). 145 | # 'pointsize': '10pt', 146 | 147 | # Additional stuff for the LaTeX preamble. 148 | # 'preamble': '', 149 | } 150 | 151 | # Grouping the document tree into LaTeX files. List of tuples 152 | # (source start file, target name, title, 153 | # author, documentclass [howto, manual, or own class]). 154 | latex_documents = [ 155 | ('index', projectname+'.tex', projectname+' Documentation', 156 | author, 'manual'), 157 | ] 158 | 159 | 160 | # -- Options for manual page output --------------------------------------- 161 | 162 | # One entry per manual page. List of tuples 163 | # (source start file, name, description, authors, manual section). 164 | man_pages = [ 165 | ('index', projectname, projectname+' Documentation', 166 | authors, 1) 167 | ] 168 | 169 | 170 | # -- Options for Texinfo output ------------------------------------------- 171 | 172 | # Grouping the document tree into Texinfo files. List of tuples 173 | # (source start file, target name, title, author, 174 | # dir menu entry, description, category) 175 | texinfo_documents = [ 176 | ('index', projectname, 'ODTbrain Documentation', 177 | author, projectname, 178 | "Algorithms for optical diffraction tomography", 179 | 'Numeric'), 180 | ] 181 | 182 | 183 | # ----------------------------------------------------------------------------- 184 | # intersphinx 185 | # ----------------------------------------------------------------------------- 186 | intersphinx_mapping = { 187 | "python": ('https://docs.python.org/', None), 188 | "nrefocus": ('http://nrefocus.readthedocs.io/en/stable', None), 189 | "numpy": ('http://docs.scipy.org/doc/numpy', None), 190 | "cellsino": ('http://cellsino.readthedocs.io/en/stable', None), 191 | "qpimage": ('http://qpimage.readthedocs.io/en/stable', None), 192 | "radontea": ('http://radontea.readthedocs.io/en/stable', None), 193 | "scipy": ('https://docs.scipy.org/doc/scipy/reference/', None), 194 | "skimage": ('http://scikit-image.org/docs/stable/', None), 195 | } 196 | -------------------------------------------------------------------------------- /docs/dev_notes/_Back_3D_tilted.sphere_points_from_angles_and_tilt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/docs/dev_notes/_Back_3D_tilted.sphere_points_from_angles_and_tilt.pdf -------------------------------------------------------------------------------- /docs/ex2d.rst: -------------------------------------------------------------------------------- 1 | 2D examples 2 | =========== 3 | These examples require raw data which are automatically 4 | downloaded from the source repository by the script 5 | :download:`example_helper.py <../examples/example_helper.py>`. 6 | Please make sure that this script is present in the example 7 | script folder. 8 | 9 | .. fancy_include:: backprop_from_mie_2d_cylinder_offcenter.py 10 | 11 | .. fancy_include:: backprop_from_mie_2d_weights_angles.py 12 | 13 | .. fancy_include:: backprop_from_mie_2d_incomplete_coverage.py 14 | 15 | .. fancy_include:: backprop_from_fdtd_2d.py 16 | 17 | -------------------------------------------------------------------------------- /docs/ex3d.rst: -------------------------------------------------------------------------------- 1 | 3D examples 2 | =========== 3 | These examples require raw data which are automatically 4 | downloaded from the source repository by the script 5 | :download:`example_helper.py <../examples/example_helper.py>`. 6 | Please make sure that this script is present in the example 7 | script folder. 8 | 9 | .. note:: 10 | The ``if __name__ == "__main__"`` guard is necessary on Windows and macOS 11 | which *spawn* new processes instead of *forking* the current process. 12 | The 3D backpropagation algorithm makes use of ``multiprocessing.Pool``. 13 | 14 | 15 | .. fancy_include:: backprop_from_rytov_3d_phantom_apple.py 16 | 17 | .. fancy_include:: backprop_from_qlsi_3d_hl60.py 18 | 19 | .. fancy_include:: backprop_from_fdtd_3d.py 20 | 21 | .. fancy_include:: backprop_from_fdtd_3d_tilted.py 22 | 23 | .. fancy_include:: backprop_from_fdtd_3d_tilted2.py 24 | 25 | .. fancy_include:: backprop_from_mie_3d_sphere.py 26 | -------------------------------------------------------------------------------- /docs/extensions/fancy_include.py: -------------------------------------------------------------------------------- 1 | """Include single scripts with doc string, code, and image 2 | 3 | Use case 4 | -------- 5 | There is an "examples" directory in the root of a repository, 6 | e.g. 'include_doc_code_img_path = "../examples"' in conf.py 7 | (default). An example is a file ("an_example.py") that consists 8 | of a doc string at the beginning of the file, the example code, 9 | and, optionally, an image file (png, jpg) ("an_example.png"). 10 | 11 | 12 | Configuration 13 | ------------- 14 | In conf.py, set the parameter 15 | 16 | fancy_include_path = "../examples" 17 | 18 | to wherever the included files reside. 19 | 20 | 21 | Usage 22 | ----- 23 | The directive 24 | 25 | .. fancy_include:: an_example.py 26 | 27 | will display the doc string formatted with the first line as a 28 | heading, a code block with line numbers, and the image file. 29 | """ 30 | import pathlib 31 | 32 | from docutils.statemachine import ViewList 33 | from docutils.parsers.rst import Directive 34 | from sphinx.util.nodes import nested_parse_with_titles 35 | from docutils import nodes 36 | 37 | 38 | class IncludeDirective(Directive): 39 | required_arguments = 1 40 | optional_arguments = 0 41 | 42 | def run(self): 43 | path = self.state.document.settings.env.config.fancy_include_path 44 | path = pathlib.Path(path) 45 | full_path = path / self.arguments[0] 46 | 47 | text = full_path.read_text() 48 | 49 | # add reference 50 | name = full_path.stem 51 | rst = [".. _example_{}:".format(name), 52 | "", 53 | ] 54 | 55 | # add docstring 56 | source = text.split('"""') 57 | doc = source[1].split("\n") 58 | doc.insert(1, "~" * len(doc[0])) # make title heading 59 | 60 | code = source[2].split("\n") 61 | 62 | for line in doc: 63 | rst.append(line) 64 | 65 | # image 66 | for ext in [".png", ".jpg"]: 67 | image_path = full_path.with_suffix(ext) 68 | if image_path.exists(): 69 | break 70 | else: 71 | image_path = "" 72 | if image_path: 73 | rst.append(".. figure:: {}".format(image_path.as_posix())) 74 | rst.append("") 75 | 76 | # download file 77 | rst.append(":download:`{} <{}>`".format( 78 | full_path.name, full_path.as_posix())) 79 | 80 | # code 81 | rst.append("") 82 | rst.append(".. code-block:: python") 83 | rst.append(" :linenos:") 84 | rst.append("") 85 | for line in code: 86 | rst.append(" {}".format(line)) 87 | rst.append("") 88 | 89 | vl = ViewList(rst, "fakefile.rst") 90 | # Create a node. 91 | node = nodes.section() 92 | node.document = self.state.document 93 | # Parse the rst. 94 | nested_parse_with_titles(self.state, vl, node) 95 | return node.children 96 | 97 | 98 | def setup(app): 99 | app.add_config_value('fancy_include_path', "../examples", 'html') 100 | 101 | app.add_directive('fancy_include', IncludeDirective) 102 | 103 | return {'version': '0.1'} # identifies the version of our extension 104 | -------------------------------------------------------------------------------- /docs/extensions/github_changelog.py: -------------------------------------------------------------------------------- 1 | """Display changelog with links to GitHub issues 2 | 3 | Usage 4 | ----- 5 | The directive 6 | 7 | .. include_changelog:: ../CHANGELOG 8 | 9 | adds the content of the changelog file into the current document. 10 | References to GitHub issues are identified as "(#XY)" (with parentheses 11 | and hash) and a link is inserted 12 | 13 | https://github.com/RI-imaging/{PROJECT}/issues/{XY} 14 | 15 | where PROJECT ist the `project` variable defined in conf.py. 16 | """ 17 | import io 18 | import re 19 | 20 | from docutils.statemachine import ViewList 21 | from docutils.parsers.rst import Directive 22 | from sphinx.util.nodes import nested_parse_with_titles 23 | from docutils import nodes 24 | 25 | 26 | class IncludeDirective(Directive): 27 | required_arguments = 1 28 | optional_arguments = 0 29 | 30 | def run(self): 31 | full_path = self.arguments[0] 32 | project = self.state.document.settings.env.config.github_project 33 | 34 | def insert_github_link(reobj): 35 | line = reobj.string 36 | instr = line[reobj.start():reobj.end()] 37 | issue = instr.strip("#()") 38 | link = "https://github.com/{}/issues/".format(project) 39 | rstlink = "(`#{issue} <{link}{issue}>`_)".format(issue=issue, 40 | link=link) 41 | return rstlink 42 | 43 | with io.open(full_path, "r") as myfile: 44 | text = myfile.readlines() 45 | 46 | rst = [] 47 | for line in text: 48 | line = line.strip("\n") 49 | if line.startswith(" ") and line.strip().startswith("-"): 50 | # list in list: 51 | rst.append("") 52 | if not line.startswith(" "): 53 | rst.append("") 54 | line = "version " + line 55 | rst.append(line) 56 | rst.append("-"*len(line)) 57 | elif not line.strip(): 58 | rst.append(line) 59 | else: 60 | line = re.sub(r"\(#[0-9]*\)", insert_github_link, line) 61 | rst.append(line) 62 | 63 | vl = ViewList(rst, "fakefile.rst") 64 | # Create a node. 65 | node = nodes.section() 66 | node.document = self.state.document 67 | # Parse the rst. 68 | nested_parse_with_titles(self.state, vl, node) 69 | return node.children 70 | 71 | 72 | def setup(app): 73 | app.add_config_value('github_project', "user/project", 'html') 74 | app.add_directive('include_changelog', IncludeDirective) 75 | return {'version': '0.1'} # identifies the version of our extension 76 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | ODTbrain provides image reconstruction algorithms for Optical Diffraction 4 | Tomography with a Born and Rytov Approximation-based Inversion to compute 5 | the refractive index (n) in 2D and in 3D. This is the documentaion of 6 | ODTbrain version |release|. 7 | 8 | 9 | Documentation 10 | ============= 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | sec_introduction 16 | sec_code_reference 17 | sec_examples 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | 22 | sec_changelog 23 | zsec_bib 24 | 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/odtbrain.bib: -------------------------------------------------------------------------------- 1 | % Encoding: UTF-8 2 | 3 | @Book{Kak2001, 4 | title = {{Principles of Computerized Tomographic Imaging}}, 5 | publisher = {SIAM}, 6 | year = {2001}, 7 | author = {Kak, Aninash C. and Slaney, Malcom G.}, 8 | editor = {O'Malley, Robert E.}, 9 | address = {Philadelphia, USA}, 10 | isbn = {089871494X}, 11 | doi = {10.1137/1.9780898719277}, 12 | keywords = {tomography}, 13 | pages = {327}, 14 | url = {http://www.slaney.org/pct/pct-toc.html}, 15 | } 16 | 17 | @Article{Mueller2015tilted, 18 | author = {Müller, Paul and Schürmann, Mirjam and Chan, Chii J and Guck, Jochen}, 19 | title = {{Single-cell diffraction tomography with optofluidic rotation about a tilted axis}}, 20 | journal = {Proc. SPIE}, 21 | year = {2015}, 22 | volume = {9548}, 23 | pages = {95480U--95480U--5}, 24 | doi = {10.1117/12.2191501}, 25 | } 26 | 27 | @Article{Mueller2015, 28 | author = {Müller, Paul and Schürmann, Mirjam and Guck, Jochen}, 29 | title = {{ODTbrain: a Python library for full-view, dense diffraction tomography}}, 30 | journal = {BMC Bioinformatics}, 31 | year = {2015}, 32 | volume = {16}, 33 | number = {1}, 34 | pages = {1--9}, 35 | issn = {1471-2105}, 36 | doi = {10.1186/s12859-015-0764-0}, 37 | } 38 | 39 | @Article{Mueller2015arxiv, 40 | author = {Müller, Paul and Schürmann, Mirjam and Guck, Jochen}, 41 | title = {{The Theory of Diffraction Tomography}}, 42 | journal = {ArXiv e-prints}, 43 | year = {2015}, 44 | archiveprefix = {arXiv}, 45 | arxivid = {q-bio.QM/1507.00466v2}, 46 | eprint = {1507.00466v2}, 47 | keywords = {81U40,J.2,Physics - Biological Physics,Physics - Optics,Quantitative Biology - Quantitative Methods}, 48 | primaryclass = {q-bio.QM}, 49 | } 50 | 51 | @Article{Tam1981, 52 | author = {Tam, K C and Perez-Mendez, V}, 53 | title = {{Tomographical imaging with limited-angle input}}, 54 | journal = {J. Opt. Soc. Am.}, 55 | year = {1981}, 56 | volume = {71}, 57 | number = {5}, 58 | pages = {582--592}, 59 | doi = {10.1364/JOSA.71.000582}, 60 | keywords = {tomography}, 61 | publisher = {OSA}, 62 | } 63 | 64 | @Article{Wolf1969, 65 | author = {Wolf, Emil}, 66 | title = {{Three-dimensional structure determination of semi-transparent objects from holographic data}}, 67 | journal = {Optics Communications}, 68 | year = {1969}, 69 | volume = {1}, 70 | number = {4}, 71 | pages = {153--156}, 72 | month = {sep}, 73 | issn = {00304018}, 74 | doi = {10.1016/0030-4018(69)90052-2}, 75 | keywords = {tomography}, 76 | } 77 | 78 | @Article{Schuermann2017, 79 | author = {M. Schürmann and G. Cojoc and S. Girardo and E. Ulbricht and J. Guck and P. Müller}, 80 | title = {Three-dimensional correlative single-cell imaging utilizing fluorescence and refractive index tomography}, 81 | journal = {Journal of Biophotonics}, 82 | year = {2017}, 83 | volume = {11}, 84 | number = {3}, 85 | pages = {e201700145}, 86 | month = {aug}, 87 | doi = {10.1002/jbio.201700145}, 88 | publisher = {Wiley-Blackwell}, 89 | } 90 | 91 | @Article{Vertu2009, 92 | author = {Vertu, Stanislas and Delaunay, Jean-Jacques and Yamada, Ichiro and Haeberlé, Olivier}, 93 | title = {{Diffraction microtomography with sample rotation: influence of a missing apple core in the recorded frequency space}}, 94 | journal = {Central European Journal of Physics}, 95 | year = {2009}, 96 | volume = {7}, 97 | number = {1}, 98 | pages = {22--31}, 99 | issn = {1895-1082}, 100 | doi = {10.2478/s11534-008-0154-6}, 101 | keywords = {Fourier optics,holographic interferometry,image reconstruction,tomography}, 102 | publisher = {SP Versita}, 103 | } 104 | 105 | @Comment{jabref-meta: databaseType:bibtex;} 106 | -------------------------------------------------------------------------------- /docs/processing.rst: -------------------------------------------------------------------------------- 1 | Data conversion methods 2 | ----------------------- 3 | .. currentmodule:: odtbrain 4 | 5 | .. autosummary:: 6 | odt_to_ri 7 | opt_to_ri 8 | sinogram_as_radon 9 | sinogram_as_rytov 10 | 11 | 12 | Sinogram preparation 13 | ~~~~~~~~~~~~~~~~~~~~ 14 | Tomographic data sets consist of detector images for different 15 | rotational positions :math:`\phi_0` of the object. Sinogram 16 | preparation means that the measured field :math:`u(\mathbf{r})` 17 | is transformed to either the Rytov approximation (diffraction tomography) 18 | or the Radon phase (classical tomography). 19 | 20 | .. autofunction:: sinogram_as_radon 21 | .. autofunction:: sinogram_as_rytov 22 | 23 | 24 | Translation of object function to refractive index 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | To obtain the refractive index map :math:`n(\mathbf{r})` 27 | from an object function :math:`f(\mathbf{r})` returned 28 | by e.g. :func:`backpropagate_3d`, an additional conversion 29 | step is necessary. For diffraction based models, :func:`odt_to_ri` 30 | must be used whereas for Radon-based models :func:`opt_to_ri` 31 | must be used. 32 | 33 | .. autofunction:: odt_to_ri 34 | .. autofunction:: opt_to_ri 35 | -------------------------------------------------------------------------------- /docs/recon_2d.rst: -------------------------------------------------------------------------------- 1 | 2D inversion 2 | ------------ 3 | The first Born approximation for a 2D scattering problem with a plane 4 | wave 5 | :math:`u_0(\mathbf{r}) = a_0 \exp(-ik_\mathrm{m}\mathbf{s_0r})` 6 | reads: 7 | 8 | .. math:: 9 | u_\mathrm{B}(\mathbf{r}) = \iint \!\! d^2r' 10 | G(\mathbf{r-r'}) f(\mathbf{r'}) u_0(\mathbf{r'}) 11 | 12 | The Green's function in 2D is the zero-order Hankel function 13 | of the first kind: 14 | 15 | .. math:: 16 | G(\mathbf{r-r'}) = \frac{i}{4} 17 | H_0^\mathrm{(1)}(k_\mathrm{m} \left| \mathbf{r-r'} \right|) 18 | 19 | Solving for :math:`f(\mathbf{r})` yields the Fourier diffraction theorem 20 | in 2D 21 | 22 | .. math:: 23 | \widehat{F}(k_\mathrm{m}(\mathbf{s-s_0})) = 24 | - \sqrt{\frac{2}{\pi}} 25 | \frac{i k_\mathrm{m}}{a_0} M 26 | \widehat{U}_{\mathrm{B},\phi_0}(k_\mathrm{Dx}) 27 | \exp \! \left(-i k_\mathrm{m} M l_\mathrm{D} \right) 28 | 29 | where 30 | :math:`\widehat{F}(k_\mathrm{x}, k_\mathrm{z})` 31 | is the Fourier transformed object function and 32 | :math:`\widehat{U}_{\mathrm{B}, \phi_0}(k_\mathrm{Dx})` is the 33 | Fourier transformed complex wave that travels along :math:`\mathbf{s_0}` 34 | (in the direction of :math:`\phi_0`) measured at the detector 35 | :math:`\mathbf{r_D}`. 36 | 37 | 38 | The following identities are used: 39 | 40 | .. math:: 41 | k_\mathrm{m} (\mathbf{s-s_0}) &= k_\mathrm{Dx} \, \mathbf{t_\perp} + 42 | k_\mathrm{m}(M - 1) \, \mathbf{s_0} 43 | 44 | \mathbf{s_0} &= \left(p_0 , \, M_0 \right) = 45 | (-\sin\phi_0, \, \cos\phi_0) 46 | 47 | \mathbf{t_\perp} &= \left(- M_0 , \, p_0 \right) = 48 | (\cos\phi_0, \, \sin\phi_0) 49 | 50 | .. currentmodule:: odtbrain 51 | 52 | 53 | Method summary 54 | ~~~~~~~~~~~~~~ 55 | .. autosummary:: 56 | backpropagate_2d 57 | fourier_map_2d 58 | integrate_2d 59 | 60 | 61 | Backpropagation 62 | ~~~~~~~~~~~~~~~ 63 | .. autofunction:: backpropagate_2d 64 | 65 | Fourier mapping 66 | ~~~~~~~~~~~~~~~ 67 | .. autofunction:: fourier_map_2d 68 | 69 | Direct sum 70 | ~~~~~~~~~~ 71 | .. autofunction:: integrate_2d 72 | -------------------------------------------------------------------------------- /docs/recon_3d.rst: -------------------------------------------------------------------------------- 1 | 3D inversion 2 | ------------ 3 | .. currentmodule:: odtbrain 4 | 5 | 6 | The first Born approximation for a 3D scattering problem with a plane 7 | wave 8 | :math:`u_0(\mathbf{r}) = a_0 \exp(-ik_\mathrm{m}\mathbf{s_0r})` 9 | reads: 10 | 11 | 12 | .. math:: 13 | u_\mathrm{B}(\mathbf{r}) = \iiint \!\! d^3r' 14 | G(\mathbf{r-r'}) f(\mathbf{r'}) u_0(\mathbf{r'}) 15 | 16 | The Green's function in 3D can be written as: 17 | 18 | .. math:: 19 | G(\mathbf{r-r'}) = \frac{ik_\mathrm{m}}{8\pi^2} \iint \!\! dpdq 20 | \frac{1}{M} \exp\! \left \lbrace i k_\mathrm{m} \left[ 21 | p(x-x') + q(y-y') + M(z-z') \right] \right \rbrace 22 | 23 | with 24 | 25 | .. math:: 26 | 27 | M = \sqrt{1-p^2-q^2} 28 | 29 | Solving for :math:`f(\mathbf{r})` yields the Fourier diffraction theorem 30 | in 3D 31 | 32 | .. math:: 33 | \widehat{F}(k_\mathrm{m}(\mathbf{s-s_0})) = 34 | - \sqrt{\frac{2}{\pi}} 35 | \frac{i k_\mathrm{m}}{a_0} M 36 | \widehat{U}_{\mathrm{B},\phi_0}(k_\mathrm{Dx}, k_\mathrm{Dy}) 37 | \exp \! \left(-i k_\mathrm{m} M l_\mathrm{D} \right) 38 | 39 | where 40 | :math:`\widehat{F}(k_\mathrm{x}, k_\mathrm{y}, k_\mathrm{z})` 41 | is the Fourier transformed object function and 42 | :math:`\widehat{U}_{\mathrm{B}, \phi_0}(k_\mathrm{Dx}, k_\mathrm{Dy})` 43 | is the Fourier transformed complex wave that travels along 44 | :math:`\mathbf{s_0}` 45 | (in the direction of :math:`\phi_0`) measured at the detector 46 | :math:`\mathbf{r_D}`. 47 | 48 | 49 | The following identities are used: 50 | 51 | .. math:: 52 | k_\mathrm{m} (\mathbf{s-s_0}) &= k_\mathrm{Dx} \, \mathbf{t_\perp} + 53 | k_\mathrm{m}(M - 1) \, \mathbf{s_0} 54 | 55 | \mathbf{s} &= (p, q, M) 56 | 57 | \mathbf{s_0} &= (p_0, q_0, M_0) = (-\sin\phi_0, \, 0, \, \cos\phi_0) 58 | 59 | \mathbf{t_\perp} &= \left(\cos\phi_0, \, 60 | \frac{k_\mathrm{Dy}}{k_\mathrm{Dx}}, \, 61 | \sin\phi_0 \right)^\top 62 | 63 | .. currentmodule:: odtbrain 64 | 65 | 66 | Method summary 67 | ~~~~~~~~~~~~~~ 68 | 69 | .. autosummary:: 70 | backpropagate_3d 71 | backpropagate_3d_tilted 72 | 73 | 74 | Backpropagation 75 | ~~~~~~~~~~~~~~~ 76 | .. autofunction:: backpropagate_3d 77 | 78 | 79 | Backpropagation with tilted axis of rotation 80 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 81 | .. autofunction:: backpropagate_3d_tilted 82 | 83 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib.bibtex 3 | sphinx_rtd_theme 4 | -------------------------------------------------------------------------------- /docs/sec_changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | List of changes in-between ODTbrain releases. 5 | 6 | .. include_changelog:: ../CHANGELOG 7 | -------------------------------------------------------------------------------- /docs/sec_code_reference.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Code reference 3 | ============== 4 | 5 | .. toctree:: 6 | :maxdepth: 4 7 | 8 | processing 9 | recon_2d 10 | recon_3d 11 | apple 12 | -------------------------------------------------------------------------------- /docs/sec_examples.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | ex2d 9 | ex3d 10 | -------------------------------------------------------------------------------- /docs/sec_introduction.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Introduction 3 | ============ 4 | 5 | This package provides reconstruction algorithms for diffraction 6 | tomography in two and three dimensions. 7 | 8 | Installation 9 | ------------ 10 | To install via the `Python Package Index (PyPI)`_, run: 11 | 12 | pip install odtbrain 13 | 14 | 15 | On some systems, the `FFTW3 library`_ might have to be 16 | installed manually before installing ODTbrain. All other 17 | dependencies are installed automatically. 18 | If the above command does not work, please refer to the 19 | installation instructions at the `GitHub repository`_ or 20 | `create an issue`_ 21 | 22 | .. _`FFTW3 library`: http://fftw.org 23 | .. _`GitHub repository`: https://github.com/RI-imaging/ODTbrain 24 | .. _`Python Package Index (PyPI)`: https://pypi.python.org/pypi/odtbrain/ 25 | .. _`create an issue`: https://github.com/RI-imaging/ODTbrain/issues 26 | 27 | 28 | Theoretical background 29 | ---------------------- 30 | A detailed summary of the underlying theory is available 31 | in :cite:`Mueller2015arxiv`. 32 | 33 | The Fourier diffraction theorem states, that the Fourier transform 34 | :math:`\widehat{U}_{\mathrm{B},\phi_0}(\mathbf{k_\mathrm{D}})` of 35 | the scattered field :math:`u_\mathrm{B}(\mathbf{r_D})`, measured at 36 | a certain angle :math:`\phi_0`, is distributed along a circular arc 37 | (2D) or along a semi-spherical surface (3D) in Fourier space, 38 | synthesizing the Fourier transform 39 | :math:`\widehat{F}(\mathbf{k})` of the object function 40 | :math:`f(\mathbf{r})` :cite:`Kak2001`, :cite:`Wolf1969`. 41 | 42 | .. math:: 43 | 44 | \widehat{F}(k_\mathrm{m}(\mathbf{s - s_0}))= 45 | - \sqrt{\frac{2}{\pi}} \frac{i k_\mathrm{m}}{a_0} 46 | M \widehat{U}_{\mathrm{B},\phi_0}(\mathbf{k_\mathrm{D}}) 47 | \exp \! \left(-i k_\mathrm{m} M l_\mathrm{D} \right) 48 | 49 | In this notation, 50 | :math:`k_\mathrm{m}` is the wave number, 51 | :math:`\mathbf{s_0}` is the norm vector pointing at :math:`\phi_0`, 52 | :math:`M=\sqrt{1-s_\mathrm{x}^2}` (2D) and 53 | :math:`M=\sqrt{1-s_\mathrm{x}^2-s_\mathrm{y}^2}` (3D) 54 | enforces the spherical constraint, and 55 | :math:`l_\mathrm{D}` is the distance from the center of the object 56 | function :math:`f(\mathbf{r})` to the detector plane 57 | :math:`\mathbf{r_D}`. 58 | 59 | 60 | Fields of Application 61 | --------------------- 62 | The algorithms presented here are based on the (scalar) Helmholtz 63 | equation. Furthermore, the Born and Rytov approximations to the 64 | scattered wave :math:`u(\mathbf{r})` are used to linearize the 65 | problem for a straight-forward inversion. 66 | 67 | The package is intended for optical diffraction 68 | tomography to determine the refractive index of biological cells. 69 | Because the Helmholtz equation is only an approximation to the 70 | Maxwell equations, describing the propagation of light, 71 | :abbr:`FDTD (Finite Difference Time Domain)` simulations were performed 72 | to test the reconstruction algorithms within this package. 73 | The algorithms present in this package should also be valid for the 74 | following cases, but have not been tested appropriately: 75 | 76 | * tomographic measurements of absorbing materials (complex refractive 77 | index :math:`n(\mathbf{r})`) 78 | 79 | * ultrasonic diffraction tomography, which is correctly described by 80 | the Helmholtz equation 81 | 82 | How to cite 83 | ----------- 84 | If you use ODTbrain in a scientific publication, please cite 85 | Müller et al., *BMC Bioinformatics* (2015) :cite:`Mueller2015`. 86 | 87 | -------------------------------------------------------------------------------- /docs/zsec_bib.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Bilbliography 3 | ============= 4 | 5 | .. bibliography:: odtbrain.bib 6 | -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_2d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_fdtd_2d.jpg -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_2d.py: -------------------------------------------------------------------------------- 1 | """FDTD cell phantom 2 | 3 | The *in silico* data set was created with the 4 | :abbr:`FDTD (Finite Difference Time Domain)` software `meep`_. The data 5 | are 1D projections of a 2D refractive index phantom. The 6 | reconstruction of the refractive index with the Rytov approximation 7 | is in good agreement with the phantom that was used in the 8 | simulation. 9 | 10 | .. note:: 11 | 12 | In the initial version of this example, I did not notice that the 13 | 1D fields in the data file were actually not in-focus. Back then, 14 | I used an auto-focusing algorithm to numerically focus the sinogram 15 | data to the rotation center, and this distance was inaccurate. 16 | Upon closer inspection of the reconstructed image, it turned out that 17 | the focus position is still about 0.5 wavelengths away from the 18 | rotation center. I have added this information to the `fdtd_info.txt` 19 | file in the data archive. 20 | 21 | The take-home message is that for a proper tomographic reconstruction 22 | it is important that the distance between the rotation center and 23 | the focus of the sinogram is known. If this is not the case, it might 24 | make sense to sweep the reconstruction distance and use an 25 | appropriate metric to determine the best reconstruction. 26 | 27 | .. _`meep`: http://ab-initio.mit.edu/wiki/index.php/Meep 28 | """ 29 | import matplotlib.pylab as plt 30 | import numpy as np 31 | import odtbrain as odt 32 | 33 | from example_helper import load_data 34 | 35 | 36 | sino, angles, phantom, cfg = load_data("fdtd_2d_sino_A100_R13.zip", 37 | f_angles="fdtd_angles.txt", 38 | f_sino_imag="fdtd_imag.txt", 39 | f_sino_real="fdtd_real.txt", 40 | f_info="fdtd_info.txt", 41 | f_phantom="fdtd_phantom.txt", 42 | ) 43 | 44 | print("Example: Backpropagation from 2D FDTD simulations") 45 | print("Refractive index of medium:", cfg["nm"]) 46 | print("Measurement position from object center in wavelengths:", cfg["lD"]) 47 | print("Wavelength sampling:", cfg["res"]) 48 | print("Performing backpropagation.") 49 | 50 | # Apply the Rytov approximation 51 | sino_rytov = odt.sinogram_as_rytov(sino) 52 | 53 | # perform backpropagation to obtain object function f 54 | f = odt.backpropagate_2d(uSin=sino_rytov, 55 | angles=angles, 56 | res=cfg["res"], 57 | nm=cfg["nm"], 58 | lD=cfg["lD"] * cfg["res"] 59 | ) 60 | 61 | # compute refractive index n from object function 62 | n = odt.odt_to_ri(f, res=cfg["res"], nm=cfg["nm"]) 63 | 64 | # compare phantom and reconstruction in plot 65 | fig, axes = plt.subplots(1, 3, figsize=(8, 2.8)) 66 | 67 | axes[0].set_title("FDTD phantom") 68 | axes[0].imshow(phantom, vmin=phantom.min(), vmax=phantom.max()) 69 | sino_phase = np.unwrap(np.angle(sino), axis=1) 70 | 71 | axes[1].set_title("phase sinogram") 72 | axes[1].imshow(sino_phase, vmin=sino_phase.min(), vmax=sino_phase.max(), 73 | aspect=sino.shape[1] / sino.shape[0], 74 | cmap="coolwarm") 75 | axes[1].set_xlabel("detector") 76 | axes[1].set_ylabel("angle [rad]") 77 | 78 | axes[2].set_title("reconstructed image") 79 | axes[2].imshow(n.real, vmin=phantom.min(), vmax=phantom.max()) 80 | 81 | # set y ticks for sinogram 82 | labels = np.linspace(0, 2 * np.pi, len(axes[1].get_yticks())) 83 | labels = ["{:.2f}".format(i) for i in labels] 84 | axes[1].set_yticks(np.linspace(0, len(angles), len(labels))) 85 | axes[1].set_yticklabels(labels) 86 | 87 | plt.tight_layout() 88 | plt.show() 89 | -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_fdtd_3d.jpg -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d.py: -------------------------------------------------------------------------------- 1 | """FDTD cell phantom 2 | The *in silico* data set was created with the 3 | :abbr:`FDTD (Finite Difference Time Domain)` software `meep`_. The data 4 | are 2D projections of a 3D refractive index phantom. The reconstruction 5 | of the refractive index with the Rytov approximation is in good 6 | agreement with the phantom that was used in the simulation. The data 7 | are downsampled by a factor of two. The rotational axis is the `y`-axis. 8 | A total of 180 projections are used for the reconstruction. A detailed 9 | description of this phantom is given in :cite:`Mueller2015`. 10 | 11 | .. _`meep`: http://ab-initio.mit.edu/wiki/index.php/Meep 12 | """ 13 | import matplotlib.pylab as plt 14 | import numpy as np 15 | 16 | import odtbrain as odt 17 | 18 | from example_helper import load_data 19 | 20 | 21 | if __name__ == "__main__": 22 | sino, angles, phantom, cfg = \ 23 | load_data("fdtd_3d_sino_A180_R6.500.tar.lzma") 24 | 25 | A = angles.shape[0] 26 | 27 | print("Example: Backpropagation from 3D FDTD simulations") 28 | print("Refractive index of medium:", cfg["nm"]) 29 | print("Measurement position from object center:", cfg["lD"]) 30 | print("Wavelength sampling:", cfg["res"]) 31 | print("Number of projections:", A) 32 | print("Performing backpropagation.") 33 | 34 | # Apply the Rytov approximation 35 | sinoRytov = odt.sinogram_as_rytov(sino) 36 | 37 | # perform backpropagation to obtain object function f 38 | f = odt.backpropagate_3d(uSin=sinoRytov, 39 | angles=angles, 40 | res=cfg["res"], 41 | nm=cfg["nm"], 42 | lD=cfg["lD"] 43 | ) 44 | 45 | # compute refractive index n from object function 46 | n = odt.odt_to_ri(f, res=cfg["res"], nm=cfg["nm"]) 47 | 48 | sx, sy, sz = n.shape 49 | px, py, pz = phantom.shape 50 | 51 | sino_phase = np.angle(sino) 52 | 53 | # compare phantom and reconstruction in plot 54 | fig, axes = plt.subplots(2, 3, figsize=(8, 4)) 55 | kwri = {"vmin": n.real.min(), "vmax": n.real.max()} 56 | kwph = {"vmin": sino_phase.min(), "vmax": sino_phase.max(), 57 | "cmap": "coolwarm"} 58 | 59 | # Phantom 60 | axes[0, 0].set_title("FDTD phantom center") 61 | rimap = axes[0, 0].imshow(phantom[px // 2], **kwri) 62 | axes[0, 0].set_xlabel("x") 63 | axes[0, 0].set_ylabel("y") 64 | 65 | axes[1, 0].set_title("FDTD phantom nucleolus") 66 | axes[1, 0].imshow(phantom[int(px / 2 + 2 * cfg["res"])], **kwri) 67 | axes[1, 0].set_xlabel("x") 68 | axes[1, 0].set_ylabel("y") 69 | 70 | # Sinogram 71 | axes[0, 1].set_title("phase projection") 72 | phmap = axes[0, 1].imshow(sino_phase[A // 2, :, :], **kwph) 73 | axes[0, 1].set_xlabel("detector x") 74 | axes[0, 1].set_ylabel("detector y") 75 | 76 | axes[1, 1].set_title("sinogram slice") 77 | axes[1, 1].imshow(sino_phase[:, :, sino.shape[2] // 2], 78 | aspect=sino.shape[1] / sino.shape[0], **kwph) 79 | axes[1, 1].set_xlabel("detector y") 80 | axes[1, 1].set_ylabel("angle [rad]") 81 | # set y ticks for sinogram 82 | labels = np.linspace(0, 2 * np.pi, len(axes[1, 1].get_yticks())) 83 | labels = ["{:.2f}".format(i) for i in labels] 84 | axes[1, 1].set_yticks(np.linspace(0, len(angles), len(labels))) 85 | axes[1, 1].set_yticklabels(labels) 86 | 87 | axes[0, 2].set_title("reconstruction center") 88 | axes[0, 2].imshow(n[sx // 2].real, **kwri) 89 | axes[0, 2].set_xlabel("x") 90 | axes[0, 2].set_ylabel("y") 91 | 92 | axes[1, 2].set_title("reconstruction nucleolus") 93 | axes[1, 2].imshow(n[int(sx / 2 + 2 * cfg["res"])].real, **kwri) 94 | axes[1, 2].set_xlabel("x") 95 | axes[1, 2].set_ylabel("y") 96 | 97 | # color bars 98 | cbkwargs = {"fraction": 0.045, 99 | "format": "%.3f"} 100 | plt.colorbar(phmap, ax=axes[0, 1], **cbkwargs) 101 | plt.colorbar(phmap, ax=axes[1, 1], **cbkwargs) 102 | plt.colorbar(rimap, ax=axes[0, 0], **cbkwargs) 103 | plt.colorbar(rimap, ax=axes[1, 0], **cbkwargs) 104 | plt.colorbar(rimap, ax=axes[0, 2], **cbkwargs) 105 | plt.colorbar(rimap, ax=axes[1, 2], **cbkwargs) 106 | 107 | plt.tight_layout() 108 | plt.show() 109 | -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d_tilted.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_fdtd_3d_tilted.jpg -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d_tilted.py: -------------------------------------------------------------------------------- 1 | """FDTD cell phantom with tilted axis of rotation 2 | 3 | The *in silico* data set was created with the 4 | :abbr:`FDTD (Finite Difference Time Domain)` software `meep`_. The data 5 | are 2D projections of a 3D refractive index phantom that is rotated 6 | about an axis which is tilted by 0.2 rad (11.5 degrees) with respect 7 | to the imaging plane. The example showcases the method 8 | :func:`odtbrain.backpropagate_3d_tilted` which takes into account 9 | such a tilted axis of rotation. The data are downsampled by a factor 10 | of two. A total of 220 projections are used for the reconstruction. 11 | Note that the information required for reconstruction decreases as the 12 | tilt angle increases. If the tilt angle is 90 degrees w.r.t. the 13 | imaging plane, then we get a rotating image of a cell (not images of a 14 | rotating cell) and tomographic reconstruction is impossible. A brief 15 | description of this algorithm is given in :cite:`Mueller2015tilted`. 16 | 17 | 18 | The first column shows the measured phase, visualizing the 19 | tilt (compare to other examples). The second column shows a 20 | reconstruction that does not take into account the tilted axis of 21 | rotation; the result is a blurry reconstruction. The third column 22 | shows the improved reconstruction; the known tilted axis of rotation 23 | is used in the reconstruction process. 24 | 25 | .. _`meep`: http://ab-initio.mit.edu/wiki/index.php/Meep 26 | """ 27 | import matplotlib.pylab as plt 28 | import numpy as np 29 | 30 | import odtbrain as odt 31 | 32 | from example_helper import load_data 33 | 34 | 35 | if __name__ == "__main__": 36 | sino, angles, phantom, cfg = \ 37 | load_data("fdtd_3d_sino_A220_R6.500_tiltyz0.2.tar.lzma") 38 | 39 | A = angles.shape[0] 40 | 41 | print("Example: Backpropagation from 3D FDTD simulations") 42 | print("Refractive index of medium:", cfg["nm"]) 43 | print("Measurement position from object center:", cfg["lD"]) 44 | print("Wavelength sampling:", cfg["res"]) 45 | print("Axis tilt in y-z direction:", cfg["tilt_yz"]) 46 | print("Number of projections:", A) 47 | 48 | print("Performing normal backpropagation.") 49 | # Apply the Rytov approximation 50 | sinoRytov = odt.sinogram_as_rytov(sino) 51 | 52 | # Perform naive backpropagation 53 | f_naiv = odt.backpropagate_3d(uSin=sinoRytov, 54 | angles=angles, 55 | res=cfg["res"], 56 | nm=cfg["nm"], 57 | lD=cfg["lD"] 58 | ) 59 | 60 | print("Performing tilted backpropagation.") 61 | # Determine tilted axis 62 | tilted_axis = [0, np.cos(cfg["tilt_yz"]), np.sin(cfg["tilt_yz"])] 63 | 64 | # Perform tilted backpropagation 65 | f_tilt = odt.backpropagate_3d_tilted(uSin=sinoRytov, 66 | angles=angles, 67 | res=cfg["res"], 68 | nm=cfg["nm"], 69 | lD=cfg["lD"], 70 | tilted_axis=tilted_axis, 71 | ) 72 | 73 | # compute refractive index n from object function 74 | n_naiv = odt.odt_to_ri(f_naiv, res=cfg["res"], nm=cfg["nm"]) 75 | n_tilt = odt.odt_to_ri(f_tilt, res=cfg["res"], nm=cfg["nm"]) 76 | 77 | sx, sy, sz = n_tilt.shape 78 | px, py, pz = phantom.shape 79 | 80 | sino_phase = np.angle(sino) 81 | 82 | # compare phantom and reconstruction in plot 83 | fig, axes = plt.subplots(2, 3, figsize=(8, 4.5)) 84 | kwri = {"vmin": n_tilt.real.min(), "vmax": n_tilt.real.max()} 85 | kwph = {"vmin": sino_phase.min(), "vmax": sino_phase.max(), 86 | "cmap": "coolwarm"} 87 | 88 | # Sinogram 89 | axes[0, 0].set_title("phase projection") 90 | phmap = axes[0, 0].imshow(sino_phase[A // 2, :, :], **kwph) 91 | axes[0, 0].set_xlabel("detector x") 92 | axes[0, 0].set_ylabel("detector y") 93 | 94 | axes[1, 0].set_title("sinogram slice") 95 | axes[1, 0].imshow(sino_phase[:, :, sino.shape[2] // 2], 96 | aspect=sino.shape[1] / sino.shape[0], **kwph) 97 | axes[1, 0].set_xlabel("detector y") 98 | axes[1, 0].set_ylabel("angle [rad]") 99 | # set y ticks for sinogram 100 | labels = np.linspace(0, 2 * np.pi, len(axes[1, 1].get_yticks())) 101 | labels = ["{:.2f}".format(i) for i in labels] 102 | axes[1, 0].set_yticks(np.linspace(0, len(angles), len(labels))) 103 | axes[1, 0].set_yticklabels(labels) 104 | 105 | axes[0, 1].set_title("normal (center)") 106 | rimap = axes[0, 1].imshow(n_naiv[sx // 2].real, **kwri) 107 | axes[0, 1].set_xlabel("x") 108 | axes[0, 1].set_ylabel("y") 109 | 110 | axes[1, 1].set_title("normal (nucleolus)") 111 | axes[1, 1].imshow(n_naiv[int(sx / 2 + 2 * cfg["res"])].real, **kwri) 112 | axes[1, 1].set_xlabel("x") 113 | axes[1, 1].set_ylabel("y") 114 | 115 | axes[0, 2].set_title("tilt correction (center)") 116 | axes[0, 2].imshow(n_tilt[sx // 2].real, **kwri) 117 | axes[0, 2].set_xlabel("x") 118 | axes[0, 2].set_ylabel("y") 119 | 120 | axes[1, 2].set_title("tilt correction (nucleolus)") 121 | axes[1, 2].imshow(n_tilt[int(sx / 2 + 2 * cfg["res"])].real, **kwri) 122 | axes[1, 2].set_xlabel("x") 123 | axes[1, 2].set_ylabel("y") 124 | 125 | # color bars 126 | cbkwargs = {"fraction": 0.045, 127 | "format": "%.3f"} 128 | plt.colorbar(phmap, ax=axes[0, 0], **cbkwargs) 129 | plt.colorbar(phmap, ax=axes[1, 0], **cbkwargs) 130 | plt.colorbar(rimap, ax=axes[0, 1], **cbkwargs) 131 | plt.colorbar(rimap, ax=axes[1, 1], **cbkwargs) 132 | plt.colorbar(rimap, ax=axes[0, 2], **cbkwargs) 133 | plt.colorbar(rimap, ax=axes[1, 2], **cbkwargs) 134 | 135 | plt.tight_layout() 136 | plt.show() 137 | -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d_tilted2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_fdtd_3d_tilted2.jpg -------------------------------------------------------------------------------- /examples/backprop_from_fdtd_3d_tilted2.py: -------------------------------------------------------------------------------- 1 | """FDTD cell phantom with tilted and rolled axis of rotation 2 | 3 | The *in silico* data set was created with the 4 | :abbr:`FDTD (Finite Difference Time Domain)` software `meep`_. The data 5 | are 2D projections of a 3D refractive index phantom that is rotated 6 | about an axis which is tilted by 0.2 rad (11.5 degrees) with respect to 7 | the imaging plane and rolled by -.42 rad (-24.1 degrees) within the 8 | imaging plane. The data are the same as were used in the previous 9 | example. A brief description of this algorithm is given in 10 | :cite:`Mueller2015tilted`. 11 | 12 | .. _`meep`: http://ab-initio.mit.edu/wiki/index.php/Meep 13 | """ 14 | import matplotlib.pylab as plt 15 | import numpy as np 16 | from scipy.ndimage import rotate 17 | 18 | import odtbrain as odt 19 | 20 | from example_helper import load_data 21 | 22 | 23 | if __name__ == "__main__": 24 | sino, angles, phantom, cfg = \ 25 | load_data("fdtd_3d_sino_A220_R6.500_tiltyz0.2.tar.lzma") 26 | 27 | # Perform titlt by -.42 rad in detector plane 28 | rotang = -0.42 29 | rotkwargs = {"mode": "constant", 30 | "order": 2, 31 | "reshape": False, 32 | } 33 | for ii in range(len(sino)): 34 | sino[ii].real = rotate( 35 | sino[ii].real, np.rad2deg(rotang), cval=1, **rotkwargs) 36 | sino[ii].imag = rotate( 37 | sino[ii].imag, np.rad2deg(rotang), cval=0, **rotkwargs) 38 | 39 | A = angles.shape[0] 40 | 41 | print("Example: Backpropagation from 3D FDTD simulations") 42 | print("Refractive index of medium:", cfg["nm"]) 43 | print("Measurement position from object center:", cfg["lD"]) 44 | print("Wavelength sampling:", cfg["res"]) 45 | print("Axis tilt in y-z direction:", cfg["tilt_yz"]) 46 | print("Number of projections:", A) 47 | 48 | # Apply the Rytov approximation 49 | sinoRytov = odt.sinogram_as_rytov(sino) 50 | 51 | # Determine tilted axis 52 | tilted_axis = [0, np.cos(cfg["tilt_yz"]), np.sin(cfg["tilt_yz"])] 53 | rotmat = np.array([ 54 | [np.cos(rotang), -np.sin(rotang), 0], 55 | [np.sin(rotang), np.cos(rotang), 0], 56 | [0, 0, 1], 57 | ]) 58 | tilted_axis = np.dot(rotmat, tilted_axis) 59 | 60 | print("Performing tilted backpropagation.") 61 | # Perform tilted backpropagation 62 | f_tilt = odt.backpropagate_3d_tilted(uSin=sinoRytov, 63 | angles=angles, 64 | res=cfg["res"], 65 | nm=cfg["nm"], 66 | lD=cfg["lD"], 67 | tilted_axis=tilted_axis, 68 | ) 69 | 70 | # compute refractive index n from object function 71 | n_tilt = odt.odt_to_ri(f_tilt, res=cfg["res"], nm=cfg["nm"]) 72 | 73 | sx, sy, sz = n_tilt.shape 74 | px, py, pz = phantom.shape 75 | 76 | sino_phase = np.angle(sino) 77 | 78 | # compare phantom and reconstruction in plot 79 | fig, axes = plt.subplots(1, 3, figsize=(8, 2.4)) 80 | kwri = {"vmin": n_tilt.real.min(), "vmax": n_tilt.real.max()} 81 | kwph = {"vmin": sino_phase.min(), "vmax": sino_phase.max(), 82 | "cmap": "coolwarm"} 83 | 84 | # Sinogram 85 | axes[0].set_title("phase projection") 86 | phmap = axes[0].imshow(sino_phase[A // 2, :, :], **kwph) 87 | axes[0].set_xlabel("detector x") 88 | axes[0].set_ylabel("detector y") 89 | 90 | axes[1].set_title("sinogram slice") 91 | axes[1].imshow(sino_phase[:, :, sino.shape[2] // 2], 92 | aspect=sino.shape[1] / sino.shape[0], **kwph) 93 | axes[1].set_xlabel("detector y") 94 | axes[1].set_ylabel("angle [rad]") 95 | # set y ticks for sinogram 96 | labels = np.linspace(0, 2 * np.pi, len(axes[1].get_yticks())) 97 | labels = ["{:.2f}".format(i) for i in labels] 98 | axes[1].set_yticks(np.linspace(0, len(angles), len(labels))) 99 | axes[1].set_yticklabels(labels) 100 | 101 | axes[2].set_title("tilt correction (nucleolus)") 102 | rimap = axes[2].imshow(n_tilt[int(sx / 2 + 2 * cfg["res"])].real, **kwri) 103 | axes[2].set_xlabel("x") 104 | axes[2].set_ylabel("y") 105 | 106 | # color bars 107 | cbkwargs = {"fraction": 0.045, 108 | "format": "%.3f"} 109 | plt.colorbar(phmap, ax=axes[0], **cbkwargs) 110 | plt.colorbar(phmap, ax=axes[1], **cbkwargs) 111 | plt.colorbar(rimap, ax=axes[2], **cbkwargs) 112 | 113 | plt.tight_layout() 114 | plt.show() 115 | -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_cylinder_offcenter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_mie_2d_cylinder_offcenter.jpg -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_cylinder_offcenter.py: -------------------------------------------------------------------------------- 1 | """Mie off-center cylinder 2 | 3 | The *in silico* data set was created with the 4 | softare `miefield `_. 5 | The data are 1D projections of an off-center cylinder of constant 6 | refractive index. The Born approximation is error-prone due to 7 | a relatively large radius of the cylinder (30 wavelengths) and 8 | a refractive index difference of 0.006 between cylinder and 9 | surrounding medium. The reconstruction of the refractive index 10 | with the Rytov approximation is in good agreement with the 11 | input data. When only 50 projections are used for the reconstruction, 12 | artifacts appear. These vanish when more projections are used for 13 | the reconstruction. 14 | """ 15 | import matplotlib.pylab as plt 16 | import numpy as np 17 | 18 | import odtbrain as odt 19 | 20 | from example_helper import load_data 21 | 22 | 23 | # simulation data 24 | sino, angles, cfg = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 25 | f_sino_imag="sino_imag.txt", 26 | f_sino_real="sino_real.txt", 27 | f_angles="mie_angles.txt", 28 | f_info="mie_info.txt") 29 | A, size = sino.shape 30 | 31 | # background sinogram computed with Mie theory 32 | # miefield.GetSinogramCylinderRotation(radius, nmed, nmed, lD, lC, size, A,res) 33 | u0 = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 34 | f_sino_imag="u0_imag.txt", 35 | f_sino_real="u0_real.txt") 36 | # create 2d array 37 | u0 = np.tile(u0, size).reshape(A, size).transpose() 38 | 39 | # background field necessary to compute initial born field 40 | # u0_single = mie.GetFieldCylinder(radius, nmed, nmed, lD, size, res) 41 | u0_single = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 42 | f_sino_imag="u0_single_imag.txt", 43 | f_sino_real="u0_single_real.txt") 44 | 45 | print("Example: Backpropagation from 2D Mie simulations") 46 | print("Refractive index of medium:", cfg["nmed"]) 47 | print("Measurement position from object center:", cfg["lD"]) 48 | print("Wavelength sampling:", cfg["res"]) 49 | print("Performing backpropagation.") 50 | 51 | # Set measurement parameters 52 | # Compute scattered field from cylinder 53 | radius = cfg["radius"] # wavelengths 54 | nmed = cfg["nmed"] 55 | ncyl = cfg["ncyl"] 56 | 57 | lD = cfg["lD"] # measurement distance in wavelengths 58 | lC = cfg["lC"] # displacement from center of image 59 | size = cfg["size"] 60 | res = cfg["res"] # px/wavelengths 61 | A = cfg["A"] # number of projections 62 | 63 | x = np.arange(size) - size / 2 64 | X, Y = np.meshgrid(x, x) 65 | rad_px = radius * res 66 | phantom = np.array(((Y - lC * res)**2 + X**2) < rad_px**2, 67 | dtype=float) * (ncyl - nmed) + nmed 68 | 69 | # Born 70 | u_sinB = (sino / u0 * u0_single - u0_single) # fake born 71 | fB = odt.backpropagate_2d(u_sinB, angles, res, nmed, lD * res) 72 | nB = odt.odt_to_ri(fB, res, nmed) 73 | 74 | # Rytov 75 | u_sinR = odt.sinogram_as_rytov(sino / u0) 76 | fR = odt.backpropagate_2d(u_sinR, angles, res, nmed, lD * res) 77 | nR = odt.odt_to_ri(fR, res, nmed) 78 | 79 | # Rytov 50 80 | u_sinR50 = odt.sinogram_as_rytov((sino / u0)[::5, :]) 81 | fR50 = odt.backpropagate_2d(u_sinR50, angles[::5], res, nmed, lD * res) 82 | nR50 = odt.odt_to_ri(fR50, res, nmed) 83 | 84 | # Plot sinogram phase and amplitude 85 | ph = odt.sinogram_as_radon(sino / u0) 86 | 87 | am = np.abs(sino / u0) 88 | 89 | # prepare plot 90 | vmin = np.min(np.array([phantom, nB.real, nR50.real, nR.real])) 91 | vmax = np.max(np.array([phantom, nB.real, nR50.real, nR.real])) 92 | 93 | fig, axes = plt.subplots(2, 3, figsize=(8, 5)) 94 | axes = np.array(axes).flatten() 95 | 96 | phantommap = axes[0].imshow(phantom, vmin=vmin, vmax=vmax) 97 | axes[0].set_title("phantom \n(non-centered cylinder)") 98 | 99 | amplmap = axes[1].imshow(am, cmap="gray") 100 | axes[1].set_title("amplitude sinogram \n(background-corrected)") 101 | 102 | phasemap = axes[2].imshow(ph, cmap="coolwarm") 103 | axes[2].set_title("phase sinogram [rad] \n(background-corrected)") 104 | 105 | axes[3].imshow(nB.real, vmin=vmin, vmax=vmax) 106 | axes[3].set_title("reconstruction (Born) \n(250 projections)") 107 | 108 | axes[4].imshow(nR50.real, vmin=vmin, vmax=vmax) 109 | axes[4].set_title("reconstruction (Rytov) \n(50 projections)") 110 | 111 | axes[5].imshow(nR.real, vmin=vmin, vmax=vmax) 112 | axes[5].set_title("reconstruction (Rytov) \n(250 projections)") 113 | 114 | # color bars 115 | cbkwargs = {"fraction": 0.045} 116 | plt.colorbar(phantommap, ax=axes[0], **cbkwargs) 117 | plt.colorbar(amplmap, ax=axes[1], **cbkwargs) 118 | plt.colorbar(phasemap, ax=axes[2], **cbkwargs) 119 | plt.colorbar(phantommap, ax=axes[3], **cbkwargs) 120 | plt.colorbar(phantommap, ax=axes[4], **cbkwargs) 121 | plt.colorbar(phantommap, ax=axes[5], **cbkwargs) 122 | 123 | plt.tight_layout() 124 | plt.show() 125 | -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_incomplete_coverage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_mie_2d_incomplete_coverage.jpg -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_incomplete_coverage.py: -------------------------------------------------------------------------------- 1 | """Mie cylinder with incomplete angular coverage 2 | 3 | This example illustrates how the backpropagation algorithm of ODTbrain 4 | handles incomplete angular coverage. All examples use 100 projections 5 | at 100%, 60%, and 40% total angular coverage. The keyword argument 6 | `weight_angles` that invokes angular weighting is set to `True` by 7 | default. The *in silico* data set was created with the 8 | softare `miefield `_. 9 | The data are 1D projections of a non-centered cylinder of constant 10 | refractive index 1.339 embedded in water with refractive index 1.333. 11 | The first column shows the used sinograms (missing angles are displayed 12 | as zeros) that were created from the original sinogram with 250 13 | projections. The second column shows the reconstruction without angular 14 | weights and the third column shows the reconstruction with angular 15 | weights. The keyword argument `weight_angles` was introduced in version 16 | 0.1.1. 17 | 18 | A 180 degree coverage results in a good reconstruction of the object. 19 | Angular weighting as implemented in the backpropagation algorithm 20 | of ODTbrain automatically addresses uneven and incomplete angular 21 | coverage. 22 | """ 23 | import matplotlib.pylab as plt 24 | import numpy as np 25 | 26 | import odtbrain as odt 27 | 28 | from example_helper import load_data 29 | 30 | sino, angles, cfg = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 31 | f_angles="mie_angles.txt", 32 | f_sino_real="sino_real.txt", 33 | f_sino_imag="sino_imag.txt", 34 | f_info="mie_info.txt") 35 | A, size = sino.shape 36 | 37 | # background sinogram computed with Mie theory 38 | # miefield.GetSinogramCylinderRotation(radius, nmed, nmed, lD, lC, size, A,res) 39 | u0 = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 40 | f_sino_imag="u0_imag.txt", 41 | f_sino_real="u0_real.txt") 42 | # create 2d array 43 | u0 = np.tile(u0, size).reshape(A, size).transpose() 44 | 45 | # background field necessary to compute initial born field 46 | # u0_single = mie.GetFieldCylinder(radius, nmed, nmed, lD, size, res) 47 | u0_single = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 48 | f_sino_imag="u0_single_imag.txt", 49 | f_sino_real="u0_single_real.txt") 50 | 51 | print("Example: Backpropagation from 2D FDTD simulations") 52 | print("Refractive index of medium:", cfg["nmed"]) 53 | print("Measurement position from object center:", cfg["lD"]) 54 | print("Wavelength sampling:", cfg["res"]) 55 | print("Performing backpropagation.") 56 | 57 | # Set measurement parameters 58 | # Compute scattered field from cylinder 59 | radius = cfg["radius"] # wavelengths 60 | nmed = cfg["nmed"] 61 | ncyl = cfg["ncyl"] 62 | 63 | lD = cfg["lD"] # measurement distance in wavelengths 64 | lC = cfg["lC"] # displacement from center of image 65 | size = cfg["size"] 66 | res = cfg["res"] # px/wavelengths 67 | A = cfg["A"] # number of projections 68 | 69 | x = np.arange(size) - size / 2.0 70 | X, Y = np.meshgrid(x, x) 71 | rad_px = radius * res 72 | phantom = np.array(((Y - lC * res)**2 + X**2) < rad_px ** 73 | 2, dtype=float) * (ncyl - nmed) + nmed 74 | 75 | u_sinR = odt.sinogram_as_rytov(sino / u0) 76 | 77 | # Rytov 100 projections evenly distributed 78 | removeeven = np.argsort(angles % .002)[:150] 79 | angleseven = np.delete(angles, removeeven, axis=0) 80 | u_sinReven = np.delete(u_sinR, removeeven, axis=0) 81 | pheven = odt.sinogram_as_radon(sino / u0) 82 | pheven[removeeven] = 0 83 | 84 | fReven = odt.backpropagate_2d(u_sinReven, angleseven, res, nmed, lD * res) 85 | nReven = odt.odt_to_ri(fReven, res, nmed) 86 | fRevennw = odt.backpropagate_2d( 87 | u_sinReven, angleseven, res, nmed, lD * res, weight_angles=False) 88 | nRevennw = odt.odt_to_ri(fRevennw, res, nmed) 89 | 90 | # Rytov 100 projections more than 180 91 | removemiss = 249 - \ 92 | np.concatenate((np.arange(100), 100 + np.arange(150)[::3])) 93 | anglesmiss = np.delete(angles, removemiss, axis=0) 94 | u_sinRmiss = np.delete(u_sinR, removemiss, axis=0) 95 | phmiss = odt.sinogram_as_radon(sino / u0) 96 | phmiss[removemiss] = 0 97 | 98 | fRmiss = odt.backpropagate_2d(u_sinRmiss, anglesmiss, res, nmed, lD * res) 99 | nRmiss = odt.odt_to_ri(fRmiss, res, nmed) 100 | fRmissnw = odt.backpropagate_2d( 101 | u_sinRmiss, anglesmiss, res, nmed, lD * res, weight_angles=False) 102 | nRmissnw = odt.odt_to_ri(fRmissnw, res, nmed) 103 | 104 | # Rytov 100 projections less than 180 105 | removebad = 249 - np.arange(150) 106 | anglesbad = np.delete(angles, removebad, axis=0) 107 | u_sinRbad = np.delete(u_sinR, removebad, axis=0) 108 | phbad = odt.sinogram_as_radon(sino / u0) 109 | phbad[removebad] = 0 110 | 111 | fRbad = odt.backpropagate_2d(u_sinRbad, anglesbad, res, nmed, lD * res) 112 | nRbad = odt.odt_to_ri(fRbad, res, nmed) 113 | fRbadnw = odt.backpropagate_2d( 114 | u_sinRbad, anglesbad, res, nmed, lD * res, weight_angles=False) 115 | nRbadnw = odt.odt_to_ri(fRbadnw, res, nmed) 116 | 117 | # prepare plot 118 | kw_ri = {"vmin": np.min(np.array([phantom, nRmiss.real, nReven.real])), 119 | "vmax": np.max(np.array([phantom, nRmiss.real, nReven.real]))} 120 | 121 | kw_ph = {"vmin": np.min(np.array([pheven, phmiss])), 122 | "vmax": np.max(np.array([pheven, phmiss])), 123 | "cmap": "coolwarm", 124 | "interpolation": "none"} 125 | 126 | fig, axes = plt.subplots(3, 3, figsize=(8, 6.5)) 127 | 128 | axes[0, 0].set_title("100% coverage ({} proj.)".format(angleseven.shape[0])) 129 | phmap = axes[0, 0].imshow(pheven, **kw_ph) 130 | 131 | axes[0, 1].set_title("RI without angular weights") 132 | rimap = axes[0, 1].imshow(nRevennw.real, **kw_ri) 133 | 134 | axes[0, 2].set_title("RI with angular weights") 135 | rimap = axes[0, 2].imshow(nReven.real, **kw_ri) 136 | 137 | axes[1, 0].set_title("60% coverage ({} proj.)".format(anglesmiss.shape[0])) 138 | axes[1, 0].imshow(phmiss, **kw_ph) 139 | 140 | axes[1, 1].set_title("RI without angular weights") 141 | axes[1, 1].imshow(nRmissnw.real, **kw_ri) 142 | 143 | axes[1, 2].set_title("RI with angular weights") 144 | axes[1, 2].imshow(nRmiss.real, **kw_ri) 145 | 146 | axes[2, 0].set_title("40% coverage ({} proj.)".format(anglesbad.shape[0])) 147 | axes[2, 0].imshow(phbad, **kw_ph) 148 | 149 | axes[2, 1].set_title("RI without angular weights") 150 | axes[2, 1].imshow(nRbadnw.real, **kw_ri) 151 | 152 | axes[2, 2].set_title("RI with angular weights") 153 | axes[2, 2].imshow(nRbad.real, **kw_ri) 154 | 155 | # color bars 156 | cbkwargs = {"fraction": 0.045, 157 | "format": "%.3f"} 158 | plt.colorbar(phmap, ax=axes[0, 0], **cbkwargs) 159 | plt.colorbar(phmap, ax=axes[1, 0], **cbkwargs) 160 | plt.colorbar(phmap, ax=axes[2, 0], **cbkwargs) 161 | plt.colorbar(rimap, ax=axes[0, 1], **cbkwargs) 162 | plt.colorbar(rimap, ax=axes[1, 1], **cbkwargs) 163 | plt.colorbar(rimap, ax=axes[2, 1], **cbkwargs) 164 | plt.colorbar(rimap, ax=axes[0, 2], **cbkwargs) 165 | plt.colorbar(rimap, ax=axes[1, 2], **cbkwargs) 166 | plt.colorbar(rimap, ax=axes[2, 2], **cbkwargs) 167 | 168 | plt.tight_layout() 169 | plt.show() 170 | -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_weights_angles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_mie_2d_weights_angles.jpg -------------------------------------------------------------------------------- /examples/backprop_from_mie_2d_weights_angles.py: -------------------------------------------------------------------------------- 1 | """Mie cylinder with unevenly spaced angles 2 | 3 | Angular weighting can significantly improve reconstruction quality 4 | when the angular projections are sampled at non-equidistant 5 | intervals :cite:`Tam1981`. The *in silico* data set was created with 6 | the softare `miefield `_. 7 | The data are 1D projections of a non-centered cylinder of constant 8 | refractive index 1.339 embedded in water with refractive index 1.333. 9 | The first column shows the used sinograms (missing angles are displayed 10 | as zeros) that were created from the original sinogram with 250 11 | projections. The second column shows the reconstruction without angular 12 | weights and the third column shows the reconstruction with angular 13 | weights. The keyword argument `weight_angles` was introduced in version 14 | 0.1.1. 15 | """ 16 | import matplotlib.pylab as plt 17 | import numpy as np 18 | from skimage.restoration import unwrap_phase 19 | 20 | import odtbrain as odt 21 | 22 | from example_helper import load_data 23 | 24 | 25 | sino, angles, cfg = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 26 | f_angles="mie_angles.txt", 27 | f_sino_real="sino_real.txt", 28 | f_sino_imag="sino_imag.txt", 29 | f_info="mie_info.txt") 30 | A, size = sino.shape 31 | 32 | # background sinogram computed with Mie theory 33 | # miefield.GetSinogramCylinderRotation(radius, nmed, nmed, lD, lC, size, A,res) 34 | u0 = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 35 | f_sino_imag="u0_imag.txt", 36 | f_sino_real="u0_real.txt") 37 | # create 2d array 38 | u0 = np.tile(u0, size).reshape(A, size).transpose() 39 | 40 | # background field necessary to compute initial born field 41 | # u0_single = mie.GetFieldCylinder(radius, nmed, nmed, lD, size, res) 42 | u0_single = load_data("mie_2d_noncentered_cylinder_A250_R2.zip", 43 | f_sino_imag="u0_single_imag.txt", 44 | f_sino_real="u0_single_real.txt") 45 | 46 | 47 | print("Example: Backpropagation from 2D FDTD simulations") 48 | print("Refractive index of medium:", cfg["nmed"]) 49 | print("Measurement position from object center:", cfg["lD"]) 50 | print("Wavelength sampling:", cfg["res"]) 51 | print("Performing backpropagation.") 52 | 53 | # Set measurement parameters 54 | # Compute scattered field from cylinder 55 | radius = cfg["radius"] # wavelengths 56 | nmed = cfg["nmed"] 57 | ncyl = cfg["ncyl"] 58 | 59 | lD = cfg["lD"] # measurement distance in wavelengths 60 | lC = cfg["lC"] # displacement from center of image 61 | size = cfg["size"] 62 | res = cfg["res"] # px/wavelengths 63 | A = cfg["A"] # number of projections 64 | 65 | x = np.arange(size) - size / 2.0 66 | X, Y = np.meshgrid(x, x) 67 | rad_px = radius * res 68 | phantom = np.array(((Y - lC * res)**2 + X**2) < rad_px ** 69 | 2, dtype=float) * (ncyl - nmed) + nmed 70 | 71 | u_sinR = odt.sinogram_as_rytov(sino / u0) 72 | 73 | # Rytov 200 projections 74 | # remove 50 projections from total of 250 projections 75 | remove200 = np.argsort(angles % .0002)[:50] 76 | angles200 = np.delete(angles, remove200, axis=0) 77 | u_sinR200 = np.delete(u_sinR, remove200, axis=0) 78 | ph200 = unwrap_phase(np.angle(sino / u0)) 79 | ph200[remove200] = 0 80 | 81 | fR200 = odt.backpropagate_2d(u_sinR200, angles200, res, nmed, lD*res) 82 | nR200 = odt.odt_to_ri(fR200, res, nmed) 83 | fR200nw = odt.backpropagate_2d(u_sinR200, angles200, res, nmed, lD*res, 84 | weight_angles=False) 85 | nR200nw = odt.odt_to_ri(fR200nw, res, nmed) 86 | 87 | # Rytov 50 projections 88 | remove50 = np.argsort(angles % .0002)[:200] 89 | angles50 = np.delete(angles, remove50, axis=0) 90 | u_sinR50 = np.delete(u_sinR, remove50, axis=0) 91 | ph50 = unwrap_phase(np.angle(sino / u0)) 92 | ph50[remove50] = 0 93 | 94 | fR50 = odt.backpropagate_2d(u_sinR50, angles50, res, nmed, lD*res) 95 | nR50 = odt.odt_to_ri(fR50, res, nmed) 96 | fR50nw = odt.backpropagate_2d(u_sinR50, angles50, res, nmed, lD*res, 97 | weight_angles=False) 98 | nR50nw = odt.odt_to_ri(fR50nw, res, nmed) 99 | 100 | # prepare plot 101 | kw_ri = {"vmin": 1.330, 102 | "vmax": 1.340} 103 | 104 | kw_ph = {"vmin": np.min(np.array([ph200, ph50])), 105 | "vmax": np.max(np.array([ph200, ph50])), 106 | "cmap": "coolwarm", 107 | "interpolation": "none"} 108 | 109 | fig, axes = plt.subplots(2, 3, figsize=(8, 4)) 110 | axes = np.array(axes).flatten() 111 | 112 | phmap = axes[0].imshow(ph200, **kw_ph) 113 | axes[0].set_title("Phase sinogram (200 proj.)") 114 | 115 | rimap = axes[1].imshow(nR200nw.real, **kw_ri) 116 | axes[1].set_title("RI without angular weights") 117 | 118 | axes[2].imshow(nR200.real, **kw_ri) 119 | axes[2].set_title("RI with angular weights") 120 | 121 | axes[3].imshow(ph50, **kw_ph) 122 | axes[3].set_title("Phase sinogram (50 proj.)") 123 | 124 | axes[4].imshow(nR50nw.real, **kw_ri) 125 | axes[4].set_title("RI without angular weights") 126 | 127 | axes[5].imshow(nR50.real, **kw_ri) 128 | axes[5].set_title("RI with angular weights") 129 | 130 | # color bars 131 | cbkwargs = {"fraction": 0.045, 132 | "format": "%.3f"} 133 | plt.colorbar(phmap, ax=axes[0], **cbkwargs) 134 | plt.colorbar(phmap, ax=axes[3], **cbkwargs) 135 | plt.colorbar(rimap, ax=axes[1], **cbkwargs) 136 | plt.colorbar(rimap, ax=axes[2], **cbkwargs) 137 | plt.colorbar(rimap, ax=axes[5], **cbkwargs) 138 | plt.colorbar(rimap, ax=axes[4], **cbkwargs) 139 | 140 | plt.tight_layout() 141 | plt.show() 142 | -------------------------------------------------------------------------------- /examples/backprop_from_mie_3d_sphere.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_mie_3d_sphere.jpg -------------------------------------------------------------------------------- /examples/backprop_from_mie_3d_sphere.py: -------------------------------------------------------------------------------- 1 | r"""Mie sphere 2 | The *in silico* data set was created with the Mie calculation software 3 | `GMM-field`_. The data consist of a two-dimensional projection of a 4 | sphere with radius :math:`R=14\lambda`, 5 | refractive index :math:`n_\mathrm{sph}=1.006`, 6 | embedded in a medium of refractive index :math:`n_\mathrm{med}=1.0` 7 | onto a detector which is :math:`l_\mathrm{D} = 20\lambda` away from the 8 | center of the sphere. 9 | 10 | The package :mod:`nrefocus` must be used to numerically focus 11 | the detected field prior to the 3D backpropagation with ODTbrain. 12 | In :func:`odtbrain.backpropagate_3d`, the parameter `lD` must 13 | be set to zero (:math:`l_\mathrm{D}=0`). 14 | 15 | The figure shows the 3D reconstruction from Mie simulations of a 16 | perfect sphere using 200 projections. Missing angle artifacts are 17 | visible along the :math:`y`-axis due to the :math:`2\pi`-only 18 | coverage in 3D Fourier space. 19 | 20 | .. _`GMM-field`: https://code.google.com/p/scatterlib/wiki/Nearfield 21 | """ 22 | import matplotlib.pylab as plt 23 | import nrefocus 24 | import numpy as np 25 | 26 | import odtbrain as odt 27 | 28 | from example_helper import load_data 29 | 30 | 31 | if __name__ == "__main__": 32 | Ex, cfg = load_data("mie_3d_sphere_field.zip", 33 | f_sino_imag="mie_sphere_imag.txt", 34 | f_sino_real="mie_sphere_real.txt", 35 | f_info="mie_info.txt") 36 | 37 | # Manually set number of angles: 38 | A = 200 39 | 40 | print("Example: Backpropagation from 3D Mie scattering") 41 | print("Refractive index of medium:", cfg["nm"]) 42 | print("Measurement position from object center:", cfg["lD"]) 43 | print("Wavelength sampling:", cfg["res"]) 44 | print("Number of angles for reconstruction:", A) 45 | print("Performing backpropagation.") 46 | 47 | # Reconstruction angles 48 | angles = np.linspace(0, 2 * np.pi, A, endpoint=False) 49 | 50 | # Perform focusing 51 | Ex = nrefocus.refocus(Ex, 52 | d=-cfg["lD"]*cfg["res"], 53 | nm=cfg["nm"], 54 | res=cfg["res"], 55 | ) 56 | 57 | # Create sinogram 58 | u_sin = np.tile(Ex.flat, A).reshape(A, int(cfg["size"]), int(cfg["size"])) 59 | 60 | # Apply the Rytov approximation 61 | u_sinR = odt.sinogram_as_rytov(u_sin) 62 | 63 | # Backpropagation 64 | fR = odt.backpropagate_3d(uSin=u_sinR, 65 | angles=angles, 66 | res=cfg["res"], 67 | nm=cfg["nm"], 68 | lD=0, 69 | padfac=2.1, 70 | save_memory=True) 71 | 72 | # RI computation 73 | nR = odt.odt_to_ri(fR, cfg["res"], cfg["nm"]) 74 | 75 | # Plotting 76 | fig, axes = plt.subplots(2, 3, figsize=(8, 5)) 77 | axes = np.array(axes).flatten() 78 | # field 79 | axes[0].set_title("Mie field phase") 80 | axes[0].set_xlabel("detector x") 81 | axes[0].set_ylabel("detector y") 82 | axes[0].imshow(np.angle(Ex), cmap="coolwarm") 83 | axes[1].set_title("Mie field amplitude") 84 | axes[1].set_xlabel("detector x") 85 | axes[1].set_ylabel("detector y") 86 | axes[1].imshow(np.abs(Ex), cmap="gray") 87 | 88 | # line plot 89 | axes[2].set_title("line plots") 90 | axes[2].set_xlabel("distance [px]") 91 | axes[2].set_ylabel("real refractive index") 92 | center = int(cfg["size"] / 2) 93 | x = np.arange(cfg["size"]) - center 94 | axes[2].plot(x, nR[:, center, center].real, label="x") 95 | axes[2].plot(x, nR[center, center, :].real, label="z") 96 | axes[2].plot(x, nR[center, :, center].real, label="y") 97 | axes[2].legend(loc=4) 98 | axes[2].set_xlim((-center, center)) 99 | dn = abs(cfg["nsph"] - cfg["nm"]) 100 | axes[2].set_ylim((cfg["nm"] - dn / 10, cfg["nsph"] + dn)) 101 | axes[2].ticklabel_format(useOffset=False) 102 | 103 | # cross sections 104 | axes[3].set_title("RI reconstruction\nsection at x=0") 105 | axes[3].set_xlabel("z") 106 | axes[3].set_ylabel("y") 107 | axes[3].imshow(nR[center, :, :].real) 108 | 109 | axes[4].set_title("RI reconstruction\nsection at y=0") 110 | axes[4].set_xlabel("x") 111 | axes[4].set_ylabel("z") 112 | axes[4].imshow(nR[:, center, :].real) 113 | 114 | axes[5].set_title("RI reconstruction\nsection at z=0") 115 | axes[5].set_xlabel("y") 116 | axes[5].set_ylabel("x") 117 | axes[5].imshow(nR[:, :, center].real) 118 | 119 | plt.tight_layout() 120 | plt.show() 121 | -------------------------------------------------------------------------------- /examples/backprop_from_qlsi_3d_hl60.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_qlsi_3d_hl60.jpg -------------------------------------------------------------------------------- /examples/backprop_from_qlsi_3d_hl60.py: -------------------------------------------------------------------------------- 1 | """HL60 cell 2 | 3 | The quantitative phase data of an HL60 S/4 cell were recorded using 4 | :abbr:`QLSI (quadri-wave lateral shearing interferometry)`. 5 | The original dataset was used in a previous publication 6 | :cite:`Schuermann2017` to illustrate the capabilities of combined 7 | fluorescence and refractive index tomography. 8 | 9 | The example data set is already aligned and background-corrected as 10 | described in the original publication and the fluorescence data are 11 | not included. The lzma-archive contains the sinogram data stored in 12 | the :ref:`qpimage ` file format and the rotational 13 | positions of each sinogram image as a text file. 14 | 15 | The figure reproduces parts of figure 4 of the original manuscript. 16 | Note that minor deviations from the original figure can be attributed 17 | to the strong compression (scale offset filter) and due to the fact 18 | that the original sinogram images were cropped from 196x196 px to 19 | 140x140 px (which in particular affects the background-part of the 20 | refractive index histogram). 21 | 22 | The raw data is available 23 | `on figshare ` 24 | (hl60_sinogram_qpi.h5). 25 | """ 26 | import pathlib 27 | import tarfile 28 | import tempfile 29 | 30 | import matplotlib.pylab as plt 31 | import numpy as np 32 | import odtbrain as odt 33 | import qpimage 34 | 35 | from example_helper import get_file, extract_lzma 36 | 37 | 38 | if __name__ == "__main__": 39 | # ascertain the data 40 | path = get_file("qlsi_3d_hl60-cell_A140.tar.lzma") 41 | tarf = extract_lzma(path) 42 | tdir = tempfile.mkdtemp(prefix="odtbrain_example_") 43 | 44 | with tarfile.open(tarf) as tf: 45 | tf.extract("series.h5", path=tdir) 46 | angles = np.loadtxt(tf.extractfile("angles.txt")) 47 | 48 | # extract the complex field sinogram from the qpimage series data 49 | h5file = pathlib.Path(tdir) / "series.h5" 50 | with qpimage.QPSeries(h5file=h5file, h5mode="r") as qps: 51 | qp0 = qps[0] 52 | meta = qp0.meta 53 | sino = np.zeros((len(qps), qp0.shape[0], qp0.shape[1]), 54 | dtype=np.complex) 55 | for ii in range(len(qps)): 56 | sino[ii] = qps[ii].field 57 | 58 | # perform backpropagation 59 | u_sinR = odt.sinogram_as_rytov(sino) 60 | res = meta["wavelength"] / meta["pixel size"] 61 | nm = meta["medium index"] 62 | 63 | fR = odt.backpropagate_3d(uSin=u_sinR, 64 | angles=angles, 65 | res=res, 66 | nm=nm) 67 | 68 | ri = odt.odt_to_ri(fR, res, nm) 69 | 70 | # plot results 71 | ext = meta["pixel size"] * 1e6 * 70 72 | kw = {"vmin": ri.real.min(), 73 | "vmax": ri.real.max(), 74 | "extent": [-ext, ext, -ext, ext]} 75 | fig, axes = plt.subplots(1, 3, figsize=(8, 2.5)) 76 | axes[0].imshow(ri[70, :, :].real, **kw) 77 | axes[0].set_xlabel("x [µm]") 78 | axes[0].set_ylabel("y [µm]") 79 | 80 | x = np.linspace(-ext, ext, 140) 81 | axes[1].plot(x, ri[70, :, 70], label="line plot x=0") 82 | axes[1].plot(x, ri[70, 70, :], label="line plot y=0") 83 | axes[1].set_xlabel("distance from center [µm]") 84 | axes[1].set_ylabel("refractive index") 85 | axes[1].legend() 86 | 87 | hist, xh = np.histogram(ri.real, bins=100) 88 | axes[2].plot(xh[1:], hist) 89 | axes[2].set_yscale('log') 90 | axes[2].set_xlabel("refractive index") 91 | axes[2].set_ylabel("histogram") 92 | 93 | plt.tight_layout() 94 | plt.show() 95 | -------------------------------------------------------------------------------- /examples/backprop_from_rytov_3d_phantom_apple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/backprop_from_rytov_3d_phantom_apple.jpg -------------------------------------------------------------------------------- /examples/backprop_from_rytov_3d_phantom_apple.py: -------------------------------------------------------------------------------- 1 | """Missing apple core correction 2 | 3 | The missing apple core :cite:`Vertu2009` is a phenomenon in diffraction 4 | tomography that is a result of the fact the the Fourier space is not 5 | filled completely when the sample is rotated only about a single axis. 6 | The resulting artifacts include ringing and blurring in the 7 | reconstruction parallel to the original rotation axis. By enforcing 8 | constraints (refractive index real-valued and larger than the 9 | surrounding medium), these artifacts can be attenuated. 10 | 11 | This example generates an artificial sinogram using the Python 12 | library :ref:`cellsino ` (The example parameters 13 | are reused from :ref:`this example `). 14 | The sinogram is then reconstructed with the backpropagation algorithm 15 | and the missing apple core correction is applied. 16 | 17 | .. note:: 18 | The missing apple core correction :func:`odtbrain.apple.correct` 19 | was implemented in version 0.3.0 and is thus not used in the 20 | older examples. 21 | """ 22 | import matplotlib.pylab as plt 23 | import numpy as np 24 | 25 | import cellsino 26 | import odtbrain as odt 27 | 28 | 29 | if __name__ == "__main__": 30 | # number of sinogram angles 31 | num_ang = 160 32 | # sinogram acquisition angles 33 | angles = np.linspace(0, 2*np.pi, num_ang, endpoint=False) 34 | # detector grid size 35 | grid_size = (250, 250) 36 | # vacuum wavelength [m] 37 | wavelength = 550e-9 38 | # pixel size [m] 39 | pixel_size = 0.08e-6 40 | # refractive index of the surrounding medium 41 | medium_index = 1.335 42 | 43 | # initialize cell phantom 44 | phantom = cellsino.phantoms.SimpleCell() 45 | 46 | # initialize sinogram with geometric parameters 47 | sino = cellsino.Sinogram(phantom=phantom, 48 | wavelength=wavelength, 49 | pixel_size=pixel_size, 50 | grid_size=grid_size) 51 | 52 | # compute sinogram (field with Rytov approximation and fluorescence) 53 | sino = sino.compute(angles=angles, propagator="rytov", mode="field") 54 | 55 | # reconstruction of refractive index 56 | sino_rytov = odt.sinogram_as_rytov(sino) 57 | f = odt.backpropagate_3d(uSin=sino_rytov, 58 | angles=angles, 59 | res=wavelength/pixel_size, 60 | nm=medium_index) 61 | 62 | ri = odt.odt_to_ri(f=f, 63 | res=wavelength/pixel_size, 64 | nm=medium_index) 65 | 66 | # apple core correction 67 | fc = odt.apple.correct(f=f, 68 | res=wavelength/pixel_size, 69 | nm=medium_index, 70 | method="sh") 71 | 72 | ric = odt.odt_to_ri(f=fc, 73 | res=wavelength/pixel_size, 74 | nm=medium_index) 75 | 76 | # plotting 77 | idx = ri.shape[2] // 2 78 | 79 | # log-scaled power spectra 80 | ft = np.log(1 + np.abs(np.fft.fftshift(np.fft.fftn(ri)))) 81 | ftc = np.log(1 + np.abs(np.fft.fftshift(np.fft.fftn(ric)))) 82 | 83 | plt.figure(figsize=(7, 5.5)) 84 | 85 | plotkwri = {"vmax": ri.real.max(), 86 | "vmin": ri.real.min(), 87 | "interpolation": "none", 88 | } 89 | 90 | plotkwft = {"vmax": ft.max(), 91 | "vmin": 0, 92 | "interpolation": "none", 93 | } 94 | 95 | ax1 = plt.subplot(221, title="plain refractive index") 96 | mapper = ax1.imshow(ri[:, :, idx].real, **plotkwri) 97 | plt.colorbar(mappable=mapper, ax=ax1) 98 | 99 | ax2 = plt.subplot(222, title="corrected refractive index") 100 | mapper = ax2.imshow(ric[:, :, idx].real, **plotkwri) 101 | plt.colorbar(mappable=mapper, ax=ax2) 102 | 103 | ax3 = plt.subplot(223, title="Fourier space (visible apple core)") 104 | mapper = ax3.imshow(ft[:, :, idx], **plotkwft) 105 | plt.colorbar(mappable=mapper, ax=ax3) 106 | 107 | ax4 = plt.subplot(224, title="Fourier space (with correction)") 108 | mapper = ax4.imshow(ftc[:, :, idx], **plotkwft) 109 | plt.colorbar(mappable=mapper, ax=ax4) 110 | 111 | plt.tight_layout() 112 | plt.show() 113 | -------------------------------------------------------------------------------- /examples/data/fdtd_2d_sino_A100_R13.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/fdtd_2d_sino_A100_R13.zip -------------------------------------------------------------------------------- /examples/data/fdtd_3d_sino_A180_R6.500.tar.lzma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/fdtd_3d_sino_A180_R6.500.tar.lzma -------------------------------------------------------------------------------- /examples/data/fdtd_3d_sino_A220_R6.500_tiltyz0.2.tar.lzma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/fdtd_3d_sino_A220_R6.500_tiltyz0.2.tar.lzma -------------------------------------------------------------------------------- /examples/data/mie_2d_noncentered_cylinder_A250_R2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/mie_2d_noncentered_cylinder_A250_R2.zip -------------------------------------------------------------------------------- /examples/data/mie_3d_sphere_field.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/mie_3d_sphere_field.zip -------------------------------------------------------------------------------- /examples/data/qlsi_3d_hl60-cell_A140.tar.lzma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/examples/data/qlsi_3d_hl60-cell_A140.tar.lzma -------------------------------------------------------------------------------- /examples/example_helper.py: -------------------------------------------------------------------------------- 1 | """Miscellaneous methods for example data handling""" 2 | import lzma 3 | import os 4 | import pathlib 5 | import tarfile 6 | import tempfile 7 | import warnings 8 | import zipfile 9 | 10 | import numpy as np 11 | 12 | 13 | datapath = pathlib.Path(__file__).parent / "data" 14 | webloc = "https://github.com/RI-imaging/ODTbrain/raw/master/examples/data/" 15 | 16 | 17 | def dl_file(url, dest, chunk_size=6553): 18 | """Download `url` to `dest`""" 19 | import urllib3 20 | http = urllib3.PoolManager() 21 | r = http.request('GET', url, preload_content=False) 22 | with dest.open('wb') as out: 23 | while True: 24 | data = r.read(chunk_size) 25 | if data is None or len(data) == 0: 26 | break 27 | out.write(data) 28 | r.release_conn() 29 | 30 | 31 | def extract_lzma(path): 32 | """Extract an lzma file and return the temporary file name""" 33 | tlfile = pathlib.Path(path) 34 | # open lzma file 35 | with tlfile.open("rb") as td: 36 | data = lzma.decompress(td.read()) 37 | # write temporary tar file 38 | fd, tmpname = tempfile.mkstemp(prefix="odt_ex_", suffix=".tar") 39 | with open(fd, "wb") as fo: 40 | fo.write(data) 41 | return tmpname 42 | 43 | 44 | def get_file(fname, datapath=datapath): 45 | """Return path of an example data file 46 | 47 | Return the full path to an example data file name. 48 | If the file does not exist in the `datapath` directory, 49 | tries to download it from the ODTbrain GitHub repository. 50 | """ 51 | # download location 52 | datapath = pathlib.Path(datapath) 53 | datapath.mkdir(parents=True, exist_ok=True) 54 | 55 | dlfile = datapath / fname 56 | if not dlfile.exists(): 57 | print("Attempting to download file {} from {} to {}.". 58 | format(fname, webloc, datapath)) 59 | try: 60 | dl_file(url=webloc+fname, dest=dlfile) 61 | except BaseException: 62 | warnings.warn("Download failed: {}".format(fname)) 63 | raise 64 | return dlfile 65 | 66 | 67 | def load_data(fname, **kwargs): 68 | """Load example data""" 69 | fname = get_file(fname) 70 | if fname.suffix == ".lzma": 71 | return load_tar_lzma_data(fname) 72 | elif fname.suffix == ".zip": 73 | return load_zip_data(fname, **kwargs) 74 | 75 | 76 | def load_tar_lzma_data(tlfile): 77 | """Load example sinogram data from a .tar.lzma file""" 78 | tmpname = extract_lzma(tlfile) 79 | 80 | # open tar file 81 | fields_real = [] 82 | fields_imag = [] 83 | phantom = [] 84 | parms = {} 85 | 86 | with tarfile.open(tmpname, "r") as t: 87 | members = t.getmembers() 88 | members.sort(key=lambda x: x.name) 89 | 90 | for m in members: 91 | n = m.name 92 | f = t.extractfile(m) 93 | if n.startswith("fdtd_info"): 94 | for ln in f.readlines(): 95 | ln = ln.decode() 96 | if ln.count("=") == 1: 97 | key, val = ln.split("=") 98 | parms[key.strip()] = float(val.strip()) 99 | elif n.startswith("phantom"): 100 | phantom.append(np.loadtxt(f)) 101 | elif n.startswith("field"): 102 | if n.endswith("imag.txt"): 103 | fields_imag.append(np.loadtxt(f)) 104 | elif n.endswith("real.txt"): 105 | fields_real.append(np.loadtxt(f)) 106 | 107 | try: 108 | os.remove(tmpname) 109 | except OSError: 110 | pass 111 | 112 | phantom = np.array(phantom) 113 | sino = np.array(fields_real) + 1j * np.array(fields_imag) 114 | angles = np.linspace(0, 2 * np.pi, sino.shape[0], endpoint=False) 115 | 116 | return sino, angles, phantom, parms 117 | 118 | 119 | def load_zip_data(zipname, f_sino_real, f_sino_imag, 120 | f_angles=None, f_phantom=None, f_info=None): 121 | """Load example sinogram data from a .zip file""" 122 | ret = [] 123 | with zipfile.ZipFile(str(zipname)) as arc: 124 | sino_real = np.loadtxt(arc.open(f_sino_real)) 125 | sino_imag = np.loadtxt(arc.open(f_sino_imag)) 126 | sino = sino_real + 1j * sino_imag 127 | ret.append(sino) 128 | if f_angles: 129 | angles = np.loadtxt(arc.open(f_angles)) 130 | ret.append(angles) 131 | if f_phantom: 132 | phantom = np.loadtxt(arc.open(f_phantom)) 133 | ret.append(phantom) 134 | if f_info: 135 | with arc.open(f_info) as info: 136 | cfg = {} 137 | for li in info.readlines(): 138 | li = li.decode() 139 | if li.count("=") == 1: 140 | key, val = li.split("=") 141 | cfg[key.strip()] = float(val.strip()) 142 | ret.append(cfg) 143 | return ret 144 | -------------------------------------------------------------------------------- /examples/generate_example_images.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path as op 3 | import sys 4 | 5 | import matplotlib.pylab as plt 6 | 7 | thisdir = op.dirname(op.abspath(__file__)) 8 | sys.path.insert(0, op.dirname(thisdir)) 9 | 10 | DPI = 80 11 | 12 | 13 | if __name__ == "__main__": 14 | # Do not display example plots 15 | plt.show = lambda: None 16 | files = os.listdir(thisdir) 17 | files = [f for f in files if f.endswith(".py")] 18 | files = [f for f in files if not f == op.basename(__file__)] 19 | files = sorted([op.join(thisdir, f) for f in files]) 20 | 21 | for f in files: 22 | fname = f[:-3] + "_.jpg" 23 | if not op.exists(fname): 24 | exec_str = open(f).read() 25 | if exec_str.count("plt.show()"): 26 | exec(exec_str) 27 | plt.savefig(fname, dpi=DPI) 28 | print("Image created: '{}'".format(fname)) 29 | else: 30 | print("No image: '{}'".format(fname)) 31 | else: 32 | print("Image skipped (already exists): '{}'".format(fname)) 33 | plt.close() 34 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | cellsino 2 | nrefocus 3 | qpimage 4 | scikit-image 5 | -------------------------------------------------------------------------------- /misc/Readme.md: -------------------------------------------------------------------------------- 1 | ## C++ Meep simulation files 2 | I wrote these [meep](http://ab-initio.mit.edu/wiki/index.php?title=Meep) 3 | simulation files to obtain the in-silico sinogram data used in the 4 | [original manuscript](https://dx.doi.org/10.1186/s12859-015-0764-0). 5 | 6 | ### Before you start 7 | Note that at the time of writing this, there is a Python interface for meep 8 | available for Ubuntu/Debian. If you are starting from scratch, it might be 9 | worth working with Python instead of wrapping these C++ scripts. 10 | 11 | ### Simulation workflow 12 | You will need a working meep installation (I recommend the parallel version) 13 | and a C++ compiler. 14 | 15 | I designed the files such that it is easy to write a wrapper and modify 16 | certain aspects. For instance, you can modify the incident angle of 17 | illumination simply by replacing the line starting with `#define ACQUISITION_PHI`. 18 | You can then create multiple subdirectories, one for each angle, and compile 19 | and run the simulation: 20 | 21 | ``` 22 | # compile (using GNU C++ Compiler on linux) 23 | g++ -malign-double meep_phantom_2d.cpp -o meep_phantom_2d.bin -lmeep_mpi -lhdf5 -lz -lgsl -lharminv -llapack -lcblas -latlas -lfftw3 -lm 24 | # run with 4 cores (requires meep compiled with Open MPI support) 25 | mpirun -n 4 ./meep_phantom_2d.bin 26 | ``` 27 | -------------------------------------------------------------------------------- /misc/meep_phantom_2d.cpp: -------------------------------------------------------------------------------- 1 | // Scattering of a plane light wave at a 2D cell phantom 2 | // Author: Paul Mueller 3 | // DOI: https://dx.doi.org/10.1186/s12859-015-0764-0 4 | 5 | # include 6 | # include 7 | # include 8 | # include 9 | # include 10 | # include 11 | # include 12 | 13 | // Do not set this time to short. Wee need the system to equilibrate. 14 | // Very good agreement with "#define TIME 1000000" 15 | // This worked well with "#define TIME 100000" 16 | // This worked worse with "#define TIME 10000" 17 | #define TIME 15000 18 | 19 | // Coordinates 20 | // The cytoplasm is centered at the origin 21 | // All coordinates are in wavelengths 22 | // Assuming a wavelength of 500nm, 20 wavelengths is 10um 23 | // The cytoplasm and nucleus have major axis (A) and minor axis (B) 24 | // The angle of rotation PHI is angle between major axis (A) and x-axis. 25 | 26 | #define ACQUISITION_PHI .5 27 | 28 | // These SIZEs include the PML and are total domain sizes (not half) 29 | // LATERALSIZE can be large to grab all scattered fields 30 | #define LATERALSIZE 30.0 31 | // up to which distance in axial direction do we need the field? 32 | #define AXIALSIZE 20.0 33 | 34 | #define MEDIUM_RI 1.333 35 | 36 | #define CYTOPLASM_RI 1.365 37 | #define CYTOPLASM_A 8.5 38 | #define CYTOPLASM_B 7.0 39 | 40 | #define NUCLEUS_RI 1.360 41 | #define NUCLEUS_A 4.5 42 | #define NUCLEUS_B 3.5 43 | #define NUCLEUS_PHI 0.5 44 | #define NUCLEUS_X 2.0 45 | #define NUCLEUS_Y 1.0 46 | 47 | #define NUCLEOLUS_RI 1.387 48 | #define NUCLEOLUS_A 1.0 49 | #define NUCLEOLUS_B 1.0 50 | #define NUCLEOLUS_PHI 0.0 51 | #define NUCLEOLUS_X 2.0 52 | #define NUCLEOLUS_Y 2.0 53 | 54 | // Choosing the resolution 55 | // http://ab-initio.mit.edu/wiki/index.php/Meep_Tutorial 56 | // In general, at least 8 pixels/wavelength in the highest 57 | // dielectric is a good idea. 58 | #define SAMPLING 13. // How many pixels for a wavelength? 59 | // A PML thickness of about half the wavelength is ok. 60 | // http://www.mail-archive.com/meep-discuss@ab-initio.mit.edu/msg00525.html 61 | #define PMLTHICKNESS 0.5 // PML thickness in wavelengths 62 | 63 | #define ONLYMEDIUM false 64 | #define SAVEALLFRAMES false 65 | #define SAVELASTPERIODS 1 // If SAVEALLFRAMES is false, save such many 66 | // periods before end of simulation 67 | 68 | using namespace meep; 69 | 70 | double eps(const vec &p) 71 | { 72 | if (ONLYMEDIUM) { 73 | return pow(MEDIUM_RI,2.0); 74 | } else { 75 | double cox = p.x()-LATERALSIZE/2.0; 76 | double coy = p.y()-AXIALSIZE/2.0; 77 | 78 | double rottx = cox*cos(ACQUISITION_PHI) - coy*sin(ACQUISITION_PHI); 79 | double rotty = cox*sin(ACQUISITION_PHI) + coy*cos(ACQUISITION_PHI); 80 | 81 | // Cytoplasm 82 | double crotx = rottx; 83 | double croty = rotty; 84 | 85 | // Nucleus 86 | double nrotx = (rottx-NUCLEUS_X)*cos(NUCLEUS_PHI) - (rotty-NUCLEUS_Y)*sin(NUCLEUS_PHI); 87 | double nroty = (rottx-NUCLEUS_X)*sin(NUCLEUS_PHI) + (rotty-NUCLEUS_Y)*cos(NUCLEUS_PHI); 88 | 89 | // Nucleolus 90 | double nnrotx = (rottx-NUCLEOLUS_X)*cos(NUCLEOLUS_PHI) - (rotty-NUCLEOLUS_Y)*sin(NUCLEOLUS_PHI); 91 | double nnroty = (rottx-NUCLEOLUS_X)*sin(NUCLEOLUS_PHI) + (rotty-NUCLEOLUS_Y)*cos(NUCLEOLUS_PHI); 92 | 93 | // Nucleolus 94 | if ( pow(nnrotx,2.0)/pow(NUCLEOLUS_A,2.0) + pow(nnroty,2.0)/pow(NUCLEOLUS_B,2.0) <= 1 ) { 95 | return pow(NUCLEOLUS_RI,2.0); 96 | } 97 | // Nucleus 98 | else if ( pow(nrotx,2.0)/pow(NUCLEUS_A,2.0) + pow(nroty,2.0)/pow(NUCLEUS_B,2.0) <= 1 ) { 99 | return pow(NUCLEUS_RI,2.0); 100 | } 101 | // Cytoplasm 102 | else if ( pow(crotx,2.0)/pow(CYTOPLASM_A,2.0) + pow(croty,2.0)/pow(CYTOPLASM_B,2.0) <= 1 ) { 103 | return pow(CYTOPLASM_RI,2.0); 104 | } 105 | // Medium 106 | else { 107 | return pow(MEDIUM_RI,2.0); 108 | } 109 | 110 | } 111 | } 112 | 113 | 114 | complex one(const vec &p) 115 | { 116 | return 1.0; 117 | } 118 | 119 | 120 | int main(int argc, char **argv) { 121 | 122 | initialize mpi(argc, argv); // do this even for non-MPI Meep 123 | 124 | //////////////////////////////////////////////////////////////////// 125 | // CHECK THE eps() FUNCTION WHEN CHANGING THIS BLOCK. 126 | // 127 | // determine the size of the copmutational volume 128 | // 1. The wavelength defines the grid size. One wavelength 129 | // is sampled with SAMPLING pixels. 130 | double resolution = SAMPLING; 131 | // The lateral extension sr of the computational grid. 132 | // make sure sr is even 133 | double sr = LATERALSIZE; 134 | double sz = AXIALSIZE; 135 | // The axial extension is the same as the lateral extension, 136 | // since we rotate the sample. 137 | 138 | // Wavelength has size of one unit. c=1, so frequency is one as 139 | // well. 140 | double frequency = 1.; 141 | //////////////////////////////////////////////////////////////////// 142 | 143 | if (ONLYMEDIUM){ 144 | // see eps() for more info 145 | master_printf("...Using empty structure \n"); 146 | } 147 | else{ 148 | // see eps() for more info 149 | master_printf("...Using phantom structure \n"); 150 | } 151 | 152 | master_printf("...Lateral object size [wavelengths]: %f \n", 2.0 * CYTOPLASM_A); 153 | // take the largest number (A) here 154 | master_printf("...Axial object size [wavelengths]: %f \n", 2.0 * CYTOPLASM_B); 155 | master_printf("...PML thickness [wavelengths]: %f \n", PMLTHICKNESS); 156 | master_printf("...Medium RI: %f \n", MEDIUM_RI); 157 | master_printf("...Sampling per wavelength [px]: %f \n", SAMPLING); 158 | master_printf("...Radial extension [px]: %f \n", sr * SAMPLING); 159 | master_printf("...Axial extension [px]: %f \n", sz * SAMPLING); 160 | 161 | int clock0=clock(); 162 | 163 | master_printf("...Initializing grid volume \n"); 164 | grid_volume v = vol2d(sr,sz,resolution); 165 | 166 | master_printf("...Initializing structure \n"); 167 | structure s(v, eps, pml(PMLTHICKNESS)); // structure 168 | s.set_epsilon(eps,true); //switch eps averaging on or off 169 | 170 | master_printf("...Initializing fields \n"); 171 | fields f(&s); 172 | 173 | const char *dirname = make_output_directory(__FILE__); 174 | f.set_output_directory(dirname); 175 | 176 | master_printf("...Saving dielectric structure \n"); 177 | f.output_hdf5(Dielectric, v.surroundings(),0,false,true,0); 178 | 179 | master_printf("...Adding light source \n"); 180 | // Wavelength is one unit. Since c=1, frequency is also 1. 181 | continuous_src_time src(1.); 182 | // Light source is a plane at z = 2*PMLTHICKNESS 183 | // to not let the source sit in the PML 184 | volume src_plane(vec(0.0,2*PMLTHICKNESS),vec(sr,2*PMLTHICKNESS)); 185 | 186 | f.add_volume_source(Ez,src,src_plane,one,1.0); 187 | master_printf("...Starting simulation \n"); 188 | 189 | 190 | for (int i=0; i= TIME-SAVELASTPERIODS*SAMPLING ){ 203 | if ( i == TIME - 1){ 204 | f.output_hdf5(Ez,v.surroundings(),0,true,false,0); 205 | } 206 | } 207 | } 208 | 209 | return 0; 210 | } 211 | 212 | 213 | -------------------------------------------------------------------------------- /misc/meep_phantom_3d.cpp: -------------------------------------------------------------------------------- 1 | // Scattering of a plane light wave at a 3D cell phantom 2 | // Author: Paul Mueller 3 | // DOI: https://dx.doi.org/10.1186/s12859-015-0764-0 4 | 5 | # include 6 | # include 7 | # include 8 | # include 9 | # include 10 | # include 11 | # include 12 | 13 | // Do not set this time to short. Wee need the system to equilibrate. 14 | // Very good agreement with "#define TIME 1000000" 15 | // This worked well with "#define TIME 100000" 16 | // This worked worse with "#define TIME 10000" 17 | #define TIME 1000 18 | 19 | // Coordinates 20 | // The cytoplasm is centered at the origin 21 | // All coordinates are in wavelengths 22 | // Assuming a wavelength of 500nm, 20 wavelengths is 10um 23 | // The cytoplasm and nucleus have major axis (A) and minor axis (B) 24 | // The angle of rotation PHI is angle between major axis (A) and x-axis. 25 | 26 | // rotation in x-z 27 | #define ACQUISITION_PHI .5 28 | 29 | // These SIZEs include the PML and are total domain sizes (not half) 30 | // LATERALSIZE can be large to grab all scattered fields 31 | #define LATERALSIZE 30.0 32 | // up to which distance in axial direction do we need the field? 33 | #define AXIALSIZE 20.0 34 | 35 | #define MEDIUM_RI 1.333 36 | 37 | // A and B are the radii not the diamters 38 | #define CYTOPLASM_RI 1.365 39 | #define CYTOPLASM_A 7.0 40 | #define CYTOPLASM_B 8.5 41 | #define CYTOPLASM_C 7.0 42 | 43 | #define NUCLEUS_RI 1.360 44 | #define NUCLEUS_A 4.5 45 | #define NUCLEUS_B 3.5 46 | #define NUCLEUS_C 3.5 47 | // rotation in x-y 48 | #define NUCLEUS_PHI 0.5 49 | #define NUCLEUS_X 2.0 50 | #define NUCLEUS_Y 1.0 51 | #define NUCLEUS_Z 1.0 52 | 53 | #define NUCLEOLUS_RI 1.387 54 | #define NUCLEOLUS_A 1.0 55 | #define NUCLEOLUS_B 1.0 56 | #define NUCLEOLUS_C 1.0 57 | // rotation in x-y 58 | #define NUCLEOLUS_PHI 0.0 59 | #define NUCLEOLUS_X 2.0 60 | #define NUCLEOLUS_Y 2.0 61 | #define NUCLEOLUS_Z 2.0 62 | 63 | // Choosing the resolution 64 | // http://ab-initio.mit.edu/wiki/index.php/Meep_Tutorial 65 | // In general, at least 8 pixels/wavelength in the highest 66 | // dielectric is a good idea. 67 | #define SAMPLING 10. // How many pixels for a wavelength? 68 | // A PML thickness of about half the wavelength is ok. 69 | // http://www.mail-archive.com/meep-discuss@ab-initio.mit.edu/msg00525.html 70 | #define PMLTHICKNESS 0.5 // PML thickness in wavelengths 71 | 72 | #define ONLYMEDIUM false 73 | #define SAVEALLFRAMES false 74 | #define SAVELASTPERIODS 1 // If SAVEALLFRAMES is false, save such many 75 | // periods before end of simulation 76 | 77 | using namespace meep; 78 | 79 | double eps(const vec &p) 80 | { 81 | if (ONLYMEDIUM) { 82 | return pow(MEDIUM_RI,2.0); 83 | } else { 84 | // Propagation in z-direction 85 | double cox = p.x()-LATERALSIZE/2.0; 86 | double coy = p.y()-LATERALSIZE/2.0; 87 | double coz = p.z()-AXIALSIZE/2.0; 88 | 89 | // Rotation in x-z (around y axis) 90 | double rottx = cox*cos(ACQUISITION_PHI) - coz*sin(ACQUISITION_PHI); 91 | double rotty = coy; 92 | double rottz = cox*sin(ACQUISITION_PHI) + coz*cos(ACQUISITION_PHI); 93 | 94 | // Cytoplasm 95 | double crotx = rottx; 96 | double croty = rotty; 97 | double crotz = rottz; 98 | 99 | // Nucleus 100 | double nrotx = (rottx-NUCLEUS_X)*cos(NUCLEUS_PHI) - (rotty-NUCLEUS_Y)*sin(NUCLEUS_PHI); 101 | double nroty = (rottx-NUCLEUS_X)*sin(NUCLEUS_PHI) + (rotty-NUCLEUS_Y)*cos(NUCLEUS_PHI); 102 | double nrotz = (rottz-NUCLEUS_Z); 103 | 104 | // Nucleolus 105 | double nnrotx = (rottx-NUCLEOLUS_X)*cos(NUCLEOLUS_PHI) - (rotty-NUCLEOLUS_Y)*sin(NUCLEOLUS_PHI); 106 | double nnroty = (rottx-NUCLEOLUS_X)*sin(NUCLEOLUS_PHI) + (rotty-NUCLEOLUS_Y)*cos(NUCLEOLUS_PHI); 107 | double nnrotz = (rottz-NUCLEOLUS_Z); 108 | 109 | // Nucleolus 110 | if ( pow(nnrotx,2.0)/pow(NUCLEOLUS_A,2.0) + pow(nnroty,2.0)/pow(NUCLEOLUS_B,2.0) + pow(nnrotz,2.0)/pow(NUCLEOLUS_C,2.0)<= 1 ) { 111 | return pow(NUCLEOLUS_RI,2.0); 112 | } 113 | // Nucleus 114 | else if ( pow(nrotx,2.0)/pow(NUCLEUS_A,2.0) + pow(nroty,2.0)/pow(NUCLEUS_B,2.0) + pow(nrotz,2.0)/pow(NUCLEUS_C,2.0) <= 1 ) { 115 | return pow(NUCLEUS_RI,2.0); 116 | } 117 | // Cytoplasm 118 | else if ( pow(crotx,2.0)/pow(CYTOPLASM_A,2.0) + pow(croty,2.0)/pow(CYTOPLASM_B,2.0) + pow(crotz,2.0)/pow(CYTOPLASM_C,2.0) <= 1 ) { 119 | return pow(CYTOPLASM_RI,2.0); 120 | } 121 | // Medium 122 | else { 123 | return pow(MEDIUM_RI,2.0); 124 | } 125 | 126 | } 127 | } 128 | 129 | 130 | complex one(const vec &p) 131 | { 132 | return 1.0; 133 | } 134 | 135 | 136 | int main(int argc, char **argv) { 137 | 138 | initialize mpi(argc, argv); // do this even for non-MPI Meep 139 | 140 | //////////////////////////////////////////////////////////////////// 141 | // CHECK THE eps() FUNCTION WHEN CHANGING THIS BLOCK. 142 | // 143 | // determine the size of the copmutational volume 144 | // 1. The wavelength defines the grid size. One wavelength 145 | // is sampled with SAMPLING pixels. 146 | double resolution = SAMPLING; 147 | // The lateral extension sr of the computational grid. 148 | // make sure sr is even 149 | double sr = LATERALSIZE; 150 | double sz = AXIALSIZE; 151 | // The axial extension is the same as the lateral extension, 152 | // since we rotate the sample. 153 | 154 | // Wavelength has size of one unit. c=1, so frequency is one as 155 | // well. 156 | double frequency = 1.; 157 | //////////////////////////////////////////////////////////////////// 158 | 159 | if (ONLYMEDIUM){ 160 | // see eps() for more info 161 | master_printf("...Using empty structure \n"); 162 | } 163 | else{ 164 | // see eps() for more info 165 | master_printf("...Using phantom structure \n"); 166 | } 167 | 168 | master_printf("...Lateral object size [wavelengths]: %f \n", 2.0 * CYTOPLASM_A); 169 | master_printf("...Axial object size 1 [wavelengths]: %f \n", 2.0 * CYTOPLASM_B); 170 | master_printf("...Axial object size 2 [wavelengths]: %f \n", 2.0 * CYTOPLASM_C); 171 | master_printf("...PML thickness [wavelengths]: %f \n", PMLTHICKNESS); 172 | master_printf("...Medium RI: %f \n", MEDIUM_RI); 173 | master_printf("...Sampling per wavelength [px]: %f \n", SAMPLING); 174 | master_printf("...Radial extension [px]: %f \n", sr * SAMPLING); 175 | master_printf("...Axial extension [px]: %f \n", sz * SAMPLING); 176 | 177 | int clock0=clock(); 178 | 179 | master_printf("...Initializing grid volume \n"); 180 | grid_volume v = vol3d(sr,sr,sz,resolution); 181 | 182 | master_printf("...Initializing structure \n"); 183 | structure s(v, eps, pml(PMLTHICKNESS)); // structure 184 | //subpix averaging, tolerance (1e-4), maxeval (100 000) 185 | s.set_epsilon(eps,true,.01,1000); 186 | 187 | 188 | master_printf("...Initializing fields \n"); 189 | fields f(&s); 190 | 191 | const char *dirname = make_output_directory(__FILE__); 192 | f.set_output_directory(dirname); 193 | 194 | master_printf("...Saving dielectric structure \n"); 195 | f.output_hdf5(Dielectric, v.surroundings(),0,false,true,0); 196 | 197 | master_printf("...Adding light source \n"); 198 | // Wavelength is one unit. Since c=1, frequency is also 1. 199 | continuous_src_time src(1.); 200 | // Volume is a cube in 3 dimensions 201 | // two corners identify that cube 202 | // 203 | // Light propagates in z-direction 204 | // Sample is rotated alogn y axis 205 | // 206 | // Light source is a plane at z = 2*PMLTHICKNESS 207 | // to not let the source sit in the PML 208 | 209 | volume src_plane(vec(0.0,0.0,2*PMLTHICKNESS),vec(sr,sr,2*PMLTHICKNESS)); 210 | 211 | f.add_volume_source(Ey,src,src_plane,one,1.0); 212 | master_printf("...Starting simulation \n"); 213 | 214 | 215 | for (int i=0; i= TIME-SAVELASTPERIODS*SAMPLING ){ 228 | if ( i == TIME - 1){ 229 | f.output_hdf5(Ey,v.surroundings(),0,true,false,0); 230 | } 231 | } 232 | } 233 | 234 | return 0; 235 | } 236 | 237 | 238 | -------------------------------------------------------------------------------- /odtbrain/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | """Algorithms for scalar diffraction tomography""" 3 | from ._alg2d_bpp import backpropagate_2d 4 | from ._alg2d_fmp import fourier_map_2d 5 | from ._alg2d_int import integrate_2d 6 | 7 | from ._alg3d_bpp import backpropagate_3d 8 | from ._alg3d_bppt import backpropagate_3d_tilted 9 | 10 | from ._prepare_sino import sinogram_as_radon, sinogram_as_rytov 11 | from ._translate_ri import odt_to_ri, opt_to_ri 12 | from ._version import version as __version__ 13 | 14 | from . import apple 15 | from . import util 16 | from . import warn 17 | 18 | 19 | __author__ = "Paul Müller" 20 | __license__ = "BSD (3 clause)" 21 | -------------------------------------------------------------------------------- /odtbrain/_alg2d_fmp.py: -------------------------------------------------------------------------------- 1 | """2D Fourier mapping""" 2 | import warnings 3 | 4 | import numpy as np 5 | import scipy.interpolate as intp 6 | 7 | from .warn import DataUndersampledWarning, ImplementationAmbiguousWarning 8 | 9 | 10 | def fourier_map_2d(uSin, angles, res, nm, lD=0, semi_coverage=False, 11 | coords=None, count=None, max_count=None, verbose=0): 12 | r"""2D Fourier mapping with the Fourier diffraction theorem 13 | 14 | Two-dimensional diffraction tomography reconstruction 15 | algorithm for scattering of a plane wave 16 | :math:`u_0(\mathbf{r}) = u_0(x,z)` 17 | by a dielectric object with refractive index 18 | :math:`n(x,z)`. 19 | 20 | This function implements the solution by interpolation in 21 | Fourier space. 22 | 23 | Parameters 24 | ---------- 25 | uSin: (A,N) ndarray 26 | Two-dimensional sinogram of line recordings 27 | :math:`u_{\mathrm{B}, \phi_j}(x_\mathrm{D})` 28 | divided by the incident plane wave :math:`u_0(l_\mathrm{D})` 29 | measured at the detector. 30 | angles: (A,) ndarray 31 | Angular positions :math:`\phi_j` of `uSin` in radians. 32 | res: float 33 | Vacuum wavelength of the light :math:`\lambda` in pixels. 34 | nm: float 35 | Refractive index of the surrounding medium :math:`n_\mathrm{m}`. 36 | lD: float 37 | Distance from center of rotation to detector plane 38 | :math:`l_\mathrm{D}` in pixels. 39 | semi_coverage: bool 40 | If set to `True`, it is assumed that the sinogram does not 41 | necessarily cover the full angular range from 0 to 2π, but an 42 | equidistant coverage over 2π can be achieved by inferring point 43 | (anti)symmetry of the (imaginary) real parts of the Fourier 44 | transform of f. Valid for any set of angles {X} that result in 45 | a 2π coverage with the union set {X}U{X+π}. 46 | coords: None [(2,M) ndarray] 47 | Computes only the output image at these coordinates. This 48 | keyword is reserved for future versions and is not 49 | implemented yet. 50 | count, max_count: multiprocessing.Value or `None` 51 | Can be used to monitor the progress of the algorithm. 52 | Initially, the value of `max_count.value` is incremented 53 | by the total number of steps. At each step, the value 54 | of `count.value` is incremented. 55 | verbose: int 56 | Increment to increase verbosity. 57 | 58 | 59 | Returns 60 | ------- 61 | f: ndarray of shape (N,N), complex if `onlyreal` is `False` 62 | Reconstructed object function :math:`f(\mathbf{r})` as defined 63 | by the Helmholtz equation. 64 | :math:`f(x,z) = 65 | k_m^2 \left(\left(\frac{n(x,z)}{n_m}\right)^2 -1\right)` 66 | 67 | 68 | See Also 69 | -------- 70 | backpropagate_2d: implementation by backpropagation 71 | odt_to_ri: conversion of the object function :math:`f(\mathbf{r})` 72 | to refractive index :math:`n(\mathbf{r})` 73 | 74 | Notes 75 | ----- 76 | Do not use the parameter `lD` in combination with the Rytov 77 | approximation - the propagation is not correctly described. 78 | Instead, numerically refocus the sinogram prior to converting 79 | it to Rytov data (using e.g. :func:`odtbrain.sinogram_as_rytov`) 80 | with a numerical focusing algorithm (available in the Python 81 | package :py:mod:`nrefocus`). 82 | 83 | The interpolation in Fourier space (which is done with 84 | :func:`scipy.interpolate.griddata`) may be unstable and lead to 85 | artifacts if the data to interpolate contains sharp spikes. This 86 | issue is not handled at all by this method (in fact, a test has 87 | been removed in version 0.2.6 because ``griddata`` gave different 88 | results on Windows and Linux). 89 | """ 90 | warnings.warn( 91 | "The method `fourier_map_2d` produces inconsistent results on " 92 | "different platforms. I have not been able to figure out what " 93 | "is going on. See https://github.com/RI-imaging/ODTbrain/issues/13.", 94 | ImplementationAmbiguousWarning) 95 | ## 96 | ## 97 | # TODO: 98 | # - zero-padding as for backpropagate_2D - However this is not 99 | # necessary as Fourier interpolation is not parallelizable with 100 | # multiprocessing and thus unattractive. Could be interesting for 101 | # specific environments without the Python GIL. 102 | # - Deal with oversampled data. Maybe issue a warning. 103 | ## 104 | ## 105 | A = angles.shape[0] 106 | if max_count is not None: 107 | max_count.value += 4 108 | # Check input data 109 | assert len(uSin.shape) == 2, "Input data `uSin` must have shape (A,N)!" 110 | assert len(uSin) == A, "`len(angles)` must be equal to `len(uSin)`!" 111 | 112 | if coords is not None: 113 | raise NotImplementedError("Output coordinates cannot yet be set" 114 | + "for the 2D backrpopagation algorithm.") 115 | # Cut-Off frequency 116 | # km [1/px] 117 | km = (2 * np.pi * nm) / res 118 | 119 | # Fourier transform of all uB's 120 | # In the script we used the unitary angular frequency (uaf) Fourier 121 | # Transform. The discrete Fourier transform is equivalent to the 122 | # unitary ordinary frequency (uof) Fourier transform. 123 | # 124 | # uof: f₁(ξ) = int f(x) exp(-2πi xξ) 125 | # 126 | # uaf: f₃(ω) = (2π)^(-n/2) int f(x) exp(-i ωx) 127 | # 128 | # f₁(ω/(2π)) = (2π)^(n/2) f₃(ω) 129 | # ω = 2πξ 130 | # 131 | # Our Backpropagation Formula is with uaf convention of the Form 132 | # 133 | # F(k) = 1/sqrt(2π) U(kD) 134 | # 135 | # If we convert now to uof convention, we get 136 | # 137 | # F(k) = U(kD) 138 | # 139 | # This means that if we divide the Fourier transform of the input 140 | # data by sqrt(2π) to convert f₃(ω) to f₁(ω/(2π)), the resulting 141 | # value for F is off by a factor of 2π. 142 | # 143 | # Instead, we can just multiply *UB* by sqrt(2π) and calculate 144 | # everything in uof. 145 | # UB = np.fft.fft(np.fft.ifftshift(uSin, axes=-1))/np.sqrt(2*np.pi) 146 | # 147 | # 148 | # Furthermore, we define 149 | # a wave propagating to the right as: 150 | # 151 | # u0(x) = exp(ikx) 152 | # 153 | # However, in physics usually we use the other sign convention: 154 | # 155 | # u0(x) = exp(-ikx) 156 | # 157 | # In order to be consistent with programs like Meep or our 158 | # scattering script for a dielectric cylinder, we want to use the 159 | # latter sign convention. 160 | # This is not a big problem. We only need to multiply the imaginary 161 | # part of the scattered wave by -1. 162 | 163 | UB = np.fft.fft(np.fft.ifftshift(uSin, axes=-1)) * np.sqrt(2 * np.pi) 164 | 165 | # Corresponding sample frequencies 166 | fx = np.fft.fftfreq(len(uSin[0])) # 1D array 167 | 168 | # kx is a 1D array. 169 | kx = 2 * np.pi * fx 170 | 171 | if count is not None: 172 | count.value += 1 173 | 174 | # Undersampling/oversampling? 175 | # Determine if the resolution of the image is too low by looking 176 | # at the maximum value for kx. This is no comparison between 177 | # Nyquist and Rayleigh frequency. 178 | if verbose and np.max(kx**2) <= km**2: 179 | # Detector is not set up properly. Higher resolution 180 | # can be achieved. 181 | warnings.warn("Measurement data are undersampled.", 182 | DataUndersampledWarning) 183 | else: 184 | # Measurement data are oversampled. 185 | pass 186 | 187 | # F(kD-kₘs₀) = - i kₘ sqrt(2/π) / a₀ * M exp(-i kₘ M lD) * UB(kD) 188 | # kₘM = sqrt( kₘ² - kx² ) 189 | # s₀ = ( -sin(ϕ₀), cos(ϕ₀) ) 190 | # 191 | # We create the 2D interpolation object F 192 | # - We compute the real coordinates (krx,kry) = kD-kₘs₀ 193 | # - We set as grid points the right side of the equation 194 | # 195 | # The interpolated griddata may go up to sqrt(2)*kₘ for kx and ky. 196 | 197 | kx = kx.reshape(1, -1) 198 | # a0 should have same shape as kx and UB 199 | # a0 = np.atleast_1d(a0) 200 | # a0 = a0.reshape(1,-1) 201 | 202 | filter_klp = (kx**2 < km**2) 203 | M = 1. / km * np.sqrt(km**2 - kx**2) 204 | # Fsin = -1j * km * np.sqrt(2/np.pi) / a0 * M * np.exp(-1j*km*M*lD) 205 | # new in version 0.1.4: 206 | # We multiply by the factor (M-1) instead of just (M) 207 | # to take into account that we have a scattered 208 | # wave that is normalized by u0. 209 | Fsin = -1j * km * np.sqrt(2 / np.pi) * M * np.exp(-1j * km * (M-1) * lD) 210 | 211 | # UB has same shape (len(angles), len(kx)) 212 | Fsin = Fsin * UB * filter_klp 213 | 214 | ang = angles.reshape(-1, 1) 215 | 216 | if semi_coverage: 217 | Fsin = np.vstack((Fsin, np.conj(Fsin))) 218 | ang = np.vstack((ang, ang + np.pi)) 219 | 220 | if count is not None: 221 | count.value += 1 222 | 223 | # Compute kxl and kyl (in rotated system ϕ₀) 224 | kxl = kx 225 | kyl = np.sqrt((km**2 - kx**2) * filter_klp) - km 226 | # rotate kxl and kyl to where they belong 227 | krx = np.cos(ang) * kxl + np.sin(ang) * kyl 228 | kry = - np.sin(ang) * kxl + np.cos(ang) * kyl 229 | 230 | Xf = krx.flatten() 231 | Yf = kry.flatten() 232 | Zf = Fsin.flatten() 233 | 234 | # DEBUG: plot kry vs krx 235 | # from matplotlib import pylab as plt 236 | # plt.figure() 237 | # for i in range(len(krx)): 238 | # plt.plot(krx[i],kry[i],"x") 239 | # plt.axes().set_aspect('equal') 240 | # plt.show() 241 | 242 | # interpolation on grid with same resolution as input data 243 | kintp = np.fft.fftshift(kx.reshape(-1)) 244 | 245 | Fcomp = intp.griddata((Xf, Yf), Zf, (kintp[None, :], kintp[:, None])) 246 | 247 | if count is not None: 248 | count.value += 1 249 | 250 | # removed nans 251 | Fcomp[np.where(np.isnan(Fcomp))] = 0 252 | 253 | # Filter data 254 | kinx, kiny = np.meshgrid(np.fft.fftshift(kx), np.fft.fftshift(kx)) 255 | Fcomp[np.where((kinx**2 + kiny**2) > 2 * km**2)] = 0 256 | 257 | # Fcomp is centered at K = 0 due to the way we chose kintp/coords 258 | f = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(Fcomp))) 259 | 260 | if count is not None: 261 | count.value += 1 262 | 263 | return f[::-1] 264 | -------------------------------------------------------------------------------- /odtbrain/_alg2d_int.py: -------------------------------------------------------------------------------- 1 | """2D slow integration""" 2 | import warnings 3 | 4 | import numpy as np 5 | 6 | from .warn import DataUndersampledWarning 7 | 8 | 9 | def integrate_2d(uSin, angles, res, nm, lD=0, coords=None, 10 | count=None, max_count=None, verbose=0): 11 | r"""(slow) 2D reconstruction with the Fourier diffraction theorem 12 | 13 | Two-dimensional diffraction tomography reconstruction 14 | algorithm for scattering of a plane wave 15 | :math:`u_0(\mathbf{r}) = u_0(x,z)` 16 | by a dielectric object with refractive index 17 | :math:`n(x,z)`. 18 | 19 | This function implements the solution by summation in real 20 | space, which is extremely slow. 21 | 22 | Parameters 23 | ---------- 24 | uSin: (A,N) ndarray 25 | Two-dimensional sinogram of line recordings 26 | :math:`u_{\mathrm{B}, \phi_j}(x_\mathrm{D})` 27 | divided by the incident plane wave :math:`u_0(l_\mathrm{D})` 28 | measured at the detector. 29 | angles: (A,) ndarray 30 | Angular positions :math:`\phi_j` of `uSin` in radians. 31 | res: float 32 | Vacuum wavelength of the light :math:`\lambda` in pixels. 33 | nm: float 34 | Refractive index of the surrounding medium :math:`n_\mathrm{m}`. 35 | lD: float 36 | Distance from center of rotation to detector plane 37 | :math:`l_\mathrm{D}` in pixels. 38 | coords: None or (2,M) ndarray] 39 | Computes only the output image at these coordinates. This 40 | keyword is reserved for future versions and is not 41 | implemented yet. 42 | count, max_count: multiprocessing.Value or `None` 43 | Can be used to monitor the progress of the algorithm. 44 | Initially, the value of `max_count.value` is incremented 45 | by the total number of steps. At each step, the value 46 | of `count.value` is incremented. 47 | verbose: int 48 | Increment to increase verbosity. 49 | 50 | Returns 51 | ------- 52 | f: ndarray of shape (N,N), complex if `onlyreal` is `False` 53 | Reconstructed object function :math:`f(\mathbf{r})` as defined 54 | by the Helmholtz equation. 55 | :math:`f(x,z) = 56 | k_m^2 \left(\left(\frac{n(x,z)}{n_m}\right)^2 -1\right)` 57 | 58 | 59 | See Also 60 | -------- 61 | backpropagate_2d: implementation by backprojection 62 | fourier_map_2d: implementation by Fourier interpolation 63 | odt_to_ri: conversion of the object function :math:`f(\mathbf{r})` 64 | to refractive index :math:`n(\mathbf{r})` 65 | 66 | 67 | Notes 68 | ----- 69 | This method is not meant for production use. The computation time 70 | is very long and the reconstruction quality is bad. This function 71 | is included in the package, because of its educational value, 72 | exemplifying the backpropagation algorithm. 73 | 74 | Do not use the parameter `lD` in combination with the Rytov 75 | approximation - the propagation is not correctly described. 76 | Instead, numerically refocus the sinogram prior to converting 77 | it to Rytov data (using e.g. :func:`odtbrain.sinogram_as_rytov`) 78 | with a numerical focusing algorithm (available in the Python 79 | package :py:mod:`nrefocus`). 80 | """ 81 | if coords is None: 82 | lx = uSin.shape[1] 83 | x = np.linspace(-lx/2, lx/2, lx, endpoint=False) 84 | xv, yv = np.meshgrid(x, x) 85 | coords = np.zeros((2, lx**2)) 86 | coords[0, :] = xv.flat 87 | coords[1, :] = yv.flat 88 | 89 | if max_count is not None: 90 | max_count.value += coords.shape[1] + 1 91 | 92 | # Cut-Off frequency 93 | km = (2 * np.pi * nm) / res 94 | 95 | # Fourier transform of all uB's 96 | # In the script we used the unitary angular frequency (uaf) Fourier 97 | # Transform. The discrete Fourier transform is equivalent to the 98 | # unitary ordinary frequency (uof) Fourier transform. 99 | # 100 | # uof: f₁(ξ) = int f(x) exp(-2πi xξ) 101 | # 102 | # uaf: f₃(ω) = (2π)^(-n/2) int f(x) exp(-i ωx) 103 | # 104 | # f₁(ω/(2π)) = (2π)^(n/2) f₃(ω) 105 | # ω = 2πξ 106 | # 107 | # We have a one-dimensional (n=1) Fourier transform and UB in the 108 | # script is equivalent to f₃(ω). Because we are working with the 109 | # uaf, we divide by sqrt(2π) after computing the fft with the uof. 110 | # 111 | # We calculate the fourier transform of uB further below. This is 112 | # necessary for memory control. 113 | 114 | # Corresponding sample frequencies 115 | fx = np.fft.fftfreq(uSin[0].shape[0]) # 1D array 116 | # kx is a 1D array. 117 | kx = 2 * np.pi * fx 118 | 119 | # Undersampling/oversampling? 120 | # Determine if the resolution of the image is too low by looking 121 | # at the maximum value for kx. This is no comparison between 122 | # Nyquist and Rayleigh frequency. 123 | if np.max(kx**2) <= 2 * km**2: 124 | # Detector is not set up properly. Higher resolution 125 | # can be achieved. 126 | warnings.warn("Measurement data are undersampled.", 127 | DataUndersampledWarning) 128 | else: 129 | raise NotImplementedError("Oversampled data not yet supported." + 130 | " Please rescale input data") 131 | 132 | # Differentials for integral 133 | dphi0 = 2 * np.pi / len(angles) 134 | dkx = kx[1] - kx[0] 135 | 136 | # We will later multiply with phi0. 137 | # Make sure we are using correct shapes 138 | kx = kx.reshape(1, kx.shape[0]) 139 | 140 | # Low-pass filter: 141 | # less-than-or-equal would give us zero division error. 142 | filter_klp = (kx**2 < km**2) 143 | 144 | # a0 will be multiplied with kx 145 | # a0 = np.atleast_1d(a0) 146 | # a0 = a0.reshape(1,-1) 147 | 148 | # Create the integrand 149 | # Integrals over ϕ₀ [0,2π]; kx [-kₘ,kₘ] 150 | # - double coverage factor 1/2 already included 151 | # - unitary angular frequency to unitary ordinary frequency 152 | # conversion performed in calculation of UB=FT(uB). 153 | # 154 | # f(r) = -i kₘ / ((2π)^(3/2) a₀) (prefactor) 155 | # * iint dϕ₀ dkx (prefactor) 156 | # * |kx| (prefactor) 157 | # * exp(-i kₘ M lD ) (prefactor) 158 | # * UBϕ₀(kx) (dependent on ϕ₀) 159 | # * exp( i (kx t⊥ + kₘ (M - 1) s₀) r ) (dependent on ϕ₀ and r) 160 | # 161 | # (r and s₀ are vectors. In the last term we perform the dot-product) 162 | # 163 | # kₘM = sqrt( kₘ² - kx² ) 164 | # t⊥ = ( cos(ϕ₀), sin(ϕ₀) ) 165 | # s₀ = ( -sin(ϕ₀), cos(ϕ₀) ) 166 | # 167 | # 168 | # everything that is not dependent on phi0: 169 | # 170 | # Filter M so there are no nans from the root 171 | M = 1. / km * np.sqrt((km**2 - kx**2) * filter_klp) 172 | prefactor = -1j * km / ((2 * np.pi)**(3. / 2)) 173 | prefactor *= dphi0 * dkx 174 | # Also filter the prefactor, so nothing outside the required 175 | # low-pass contributes to the sum. 176 | prefactor *= np.abs(kx) * filter_klp 177 | # new in version 0.1.4: 178 | # We multiply by the factor (M-1) instead of just (M) 179 | # to take into account that we have a scattered 180 | # wave that is normalized by u0. 181 | prefactor *= np.exp(-1j * km * (M-1) * lD) 182 | 183 | # Initiate function f 184 | f = np.zeros(len(coords[0]), dtype=np.complex128) 185 | lenf = len(f) 186 | lenu0 = len(uSin[0]) # lenu0 = len(kx[0]) 187 | 188 | # Initiate vector r that corresponds to calculating a value of f. 189 | r = np.zeros((2, 1, 1)) 190 | 191 | # Everything is normal. 192 | # Get the angles ϕ₀. 193 | phi0 = angles.reshape(-1, 1) 194 | # Compute the Fourier transform of uB. 195 | # This is true: np.fft.fft(UB)[0] == np.fft.fft(UB[0]) 196 | # because axis -1 is always used. 197 | # 198 | # 199 | # Furthermore, The notation in the our optical tomography script for 200 | # a wave propagating to the right is: 201 | # 202 | # u0(x) = exp(ikx) 203 | # 204 | # However, in physics usually usethe other sign convention: 205 | # 206 | # u0(x) = exp(-ikx) 207 | # 208 | # In order to be consisten with programs like Meep or our scattering 209 | # script for a dielectric cylinder, we want to use the latter sign 210 | # convention. 211 | # This is not a big problem. We only need to multiply the imaginary 212 | # part of the scattered wave by -1. 213 | UB = np.fft.fft(np.fft.ifftshift(uSin, axes=-1)) / np.sqrt(2 * np.pi) 214 | UBi = UB.reshape(len(angles), lenu0) 215 | 216 | if count is not None: 217 | count.value += 1 218 | 219 | for j in range(lenf): 220 | # Get r (We compute f(r) in this for-loop) 221 | r[0][:] = coords[0, j] # x 222 | r[1][:] = coords[1, j] # y 223 | 224 | # Integrand changes with r, so we have to create a new 225 | # array: 226 | integrand = prefactor * UBi 227 | 228 | # We save memory by directly applying the following to 229 | # the integrand: 230 | # 231 | # Vector along which we measured 232 | # s0 = np.zeros((2, phi0.shape[0], kx.shape[0])) 233 | # s0[0] = -np.sin(phi0) 234 | # s0[1] = +np.cos(phi0) 235 | 236 | # Vector perpendicular to s0 237 | # t_perp_kx = np.zeros((2, phi0.shape[0], kx.shape[1])) 238 | # 239 | # t_perp_kx[0] = kx*np.cos(phi0) 240 | # t_perp_kx[1] = kx*np.sin(phi0) 241 | 242 | # 243 | # term3 = np.exp(1j*np.sum(r*( t_perp_kx + (gamma-km)*s0 ), axis=0)) 244 | # integrand* = term3 245 | # 246 | # Reminder: 247 | # f(r) = -i kₘ / ((2π)^(3/2) a₀) (prefactor) 248 | # * iint dϕ₀ dkx (prefactor) 249 | # * |kx| (prefactor) 250 | # * exp(-i kₘ M lD ) (prefactor) 251 | # * UB(kx) (dependent on ϕ₀) 252 | # * exp( i (kx t⊥ + kₘ(M - 1) s₀) r ) (dependent on ϕ₀ and r) 253 | # 254 | # (r and s₀ are vectors. In the last term we perform the dot-product) 255 | # 256 | # kₘM = sqrt( kₘ² - kx² ) 257 | # t⊥ = ( cos(ϕ₀), sin(ϕ₀) ) 258 | # s₀ = ( -sin(ϕ₀), cos(ϕ₀) ) 259 | integrand *= np.exp(1j * ( 260 | r[0] * (kx * np.cos(phi0) - km * (M - 1) * np.sin(phi0)) + 261 | r[1] * (kx * np.sin(phi0) + km * (M - 1) * np.cos(phi0)))) 262 | 263 | # Calculate the integral for the position r 264 | # integrand.sort() 265 | f[j] = np.sum(integrand) 266 | 267 | # free memory 268 | del integrand 269 | 270 | if count is not None: 271 | count.value += 1 272 | 273 | return f.reshape(lx, lx) 274 | -------------------------------------------------------------------------------- /odtbrain/_prepare_sino.py: -------------------------------------------------------------------------------- 1 | """Sinogram preparation""" 2 | import numpy as np 3 | from scipy.stats import mode 4 | from skimage.restoration import unwrap_phase 5 | 6 | 7 | def align_unwrapped(sino): 8 | """Align an unwrapped phase array to zero-phase 9 | 10 | All operations are performed in-place. 11 | """ 12 | samples = [] 13 | if len(sino.shape) == 2: 14 | # 2D 15 | # take 1D samples at beginning and end of array 16 | samples.append(sino[:, 0]) 17 | samples.append(sino[:, 1]) 18 | samples.append(sino[:, 2]) 19 | samples.append(sino[:, -1]) 20 | samples.append(sino[:, -2]) 21 | 22 | elif len(sino.shape) == 3: 23 | # 3D 24 | # take 1D samples at beginning and end of array 25 | samples.append(sino[:, 0, 0]) 26 | samples.append(sino[:, 0, -1]) 27 | samples.append(sino[:, -1, 0]) 28 | samples.append(sino[:, -1, -1]) 29 | samples.append(sino[:, 0, 1]) 30 | 31 | # find discontinuities in the samples 32 | steps = np.zeros((len(samples), samples[0].shape[0])) 33 | for i in range(len(samples)): 34 | t = np.unwrap(samples[i]) 35 | steps[i] = samples[i] - t 36 | 37 | # if the majority believes so, add a step of PI 38 | remove = mode(steps, axis=0, keepdims=True)[0][0] 39 | 40 | # obtain divmod min 41 | twopi = 2*np.pi 42 | minimum = divmod_neg(np.min(sino), twopi)[0] 43 | remove += minimum*twopi 44 | 45 | for i in range(len(sino)): 46 | sino[i] -= remove[i] 47 | 48 | 49 | def divmod_neg(a, b): 50 | """Return divmod with closest result to zero""" 51 | q, r = divmod(a, b) 52 | # make sure r is close to zero 53 | sr = np.sign(r) 54 | if np.abs(r) > b/2: 55 | q += sr 56 | r -= b * sr 57 | return q, r 58 | 59 | 60 | def sinogram_as_radon(uSin, align=True): 61 | r"""Compute the phase from a complex wave field sinogram 62 | 63 | This step is essential when using the ray approximation before 64 | computation of the refractive index with the inverse Radon 65 | transform. 66 | 67 | Parameters 68 | ---------- 69 | uSin: 2d or 3d complex ndarray 70 | The background-corrected sinogram of the complex scattered wave 71 | :math:`u(\mathbf{r})/u_0(\mathbf{r})`. The first axis iterates 72 | through the angles :math:`\phi_0`. 73 | align: bool 74 | Tries to correct for a phase offset in the phase sinogram. 75 | 76 | Returns 77 | ------- 78 | phase: 2d or 3d real ndarray 79 | The unwrapped phase array corresponding to `uSin`. 80 | 81 | See Also 82 | -------- 83 | skimage.restoration.unwrap_phase: phase unwrapping 84 | radontea.backproject_3d: e.g. reconstruction via backprojection 85 | """ 86 | ndims = len(uSin.shape) 87 | 88 | if ndims == 2: 89 | # unwrapping is very important 90 | phiR = np.unwrap(np.angle(uSin), axis=-1) 91 | else: 92 | # Unwrap gets the dimension of the problem from the input 93 | # data. Since we have a sinogram, we need to pass it the 94 | # slices one by one. 95 | phiR = np.angle(uSin) 96 | for ii in range(len(phiR)): 97 | phiR[ii] = unwrap_phase(phiR[ii], rng=47) 98 | 99 | if align: 100 | align_unwrapped(phiR) 101 | 102 | return phiR 103 | 104 | 105 | def sinogram_as_rytov(uSin, u0=1, align=True): 106 | r"""Convert the complex wave field sinogram to the Rytov phase 107 | 108 | This method applies the Rytov approximation to the 109 | recorded complex wave sinogram. To achieve this, the following 110 | filter is applied: 111 | 112 | .. math:: 113 | u_\mathrm{B}(\mathbf{r}) = u_\mathrm{0}(\mathbf{r}) 114 | \ln\!\left( 115 | \frac{u_\mathrm{R}(\mathbf{r})}{u_\mathrm{0}(\mathbf{r})} 116 | +1 \right) 117 | 118 | This filter step effectively replaces the Born approximation 119 | :math:`u_\mathrm{B}(\mathbf{r})` with the Rytov approximation 120 | :math:`u_\mathrm{R}(\mathbf{r})`, assuming that the scattered 121 | field is equal to 122 | :math:`u(\mathbf{r})\approx u_\mathrm{R}(\mathbf{r})+ 123 | u_\mathrm{0}(\mathbf{r})`. 124 | 125 | 126 | Parameters 127 | ---------- 128 | uSin: 2d or 3d complex ndarray 129 | The sinogram of the complex wave 130 | :math:`u_\mathrm{R}(\mathbf{r}) + u_\mathrm{0}(\mathbf{r})`. 131 | The first axis iterates through the angles :math:`\phi_0`. 132 | u0: ndarray of dimension as `uSin` or less, or int. 133 | The incident plane wave 134 | :math:`u_\mathrm{0}(\mathbf{r})` at the detector. 135 | If `u0` is "1", it is assumed that the data is already 136 | background-corrected ( 137 | `uSin` :math:`= \frac{u_\mathrm{R}(\mathbf{r})}{ 138 | u_\mathrm{0}(\mathbf{r})} + 1` 139 | ). Note that if the reconstruction distance :math:`l_\mathrm{D}` 140 | of the original experiment is non-zero and `u0` is set to 1, 141 | then the reconstruction will be wrong; the field is not focused 142 | to the center of the reconstruction volume. 143 | align: bool 144 | Tries to correct for a phase offset in the phase sinogram. 145 | 146 | Returns 147 | ------- 148 | uB: 2d or 3d real ndarray 149 | The Rytov-filtered complex sinogram 150 | :math:`u_\mathrm{B}(\mathbf{r})`. 151 | 152 | See Also 153 | -------- 154 | skimage.restoration.unwrap_phase: phase unwrapping 155 | """ 156 | ndims = len(uSin.shape) 157 | 158 | # imaginary part of the complex Rytov phase 159 | phiR = np.angle(uSin / u0) 160 | 161 | # real part of the complex Rytov phase 162 | lna = np.log(np.absolute(uSin / u0)) 163 | 164 | if ndims == 2: 165 | # unwrapping is very important 166 | phiR[:] = np.unwrap(phiR, axis=-1) 167 | else: 168 | # Unwrap gets the dimension of the problem from the input 169 | # data. Since we have a sinogram, we need to pass it the 170 | # slices one by one. 171 | for ii in range(len(phiR)): 172 | phiR[ii] = unwrap_phase(phiR[ii], rng=47) 173 | 174 | if align: 175 | align_unwrapped(phiR) 176 | 177 | # rytovSin = u0*(np.log(a/a0) + 1j*phiR) 178 | # u0 is one - we already did background correction 179 | 180 | # complex rytov phase: 181 | rytovSin = 1j * phiR + lna 182 | return u0 * rytovSin 183 | -------------------------------------------------------------------------------- /odtbrain/_translate_ri.py: -------------------------------------------------------------------------------- 1 | """Translate reconstructed object functions to refractive index""" 2 | import numpy as np 3 | 4 | 5 | def odt_to_ri(f, res, nm): 6 | r"""Convert the ODT object function to refractive index 7 | 8 | In :abbr:`ODT (Optical Diffraction Tomography)`, the object function 9 | is defined by the Helmholtz equation 10 | 11 | .. math:: 12 | 13 | f(\mathbf{r}) = k_\mathrm{m}^2 \left[ 14 | \left( \frac{n(\mathbf{r})}{n_\mathrm{m}} \right)^2 - 1 15 | \right] 16 | 17 | with :math:`k_\mathrm{m} = \frac{2\pi n_\mathrm{m}}{\lambda}`. 18 | By inverting this equation, we obtain the refractive index 19 | :math:`n(\mathbf{r})`. 20 | 21 | .. math:: 22 | 23 | n(\mathbf{r}) = n_\mathrm{m} 24 | \sqrt{\frac{f(\mathbf{r})}{k_\mathrm{m}^2} + 1 } 25 | 26 | Parameters 27 | ---------- 28 | f: n-dimensional ndarray 29 | The reconstructed object function :math:`f(\mathbf{r})`. 30 | res: float 31 | The size of the vacuum wave length :math:`\lambda` in pixels. 32 | nm: float 33 | The refractive index of the medium :math:`n_\mathrm{m}` that 34 | surrounds the object in :math:`f(\mathbf{r})`. 35 | 36 | Returns 37 | ------- 38 | ri: n-dimensional ndarray 39 | The complex refractive index :math:`n(\mathbf{r})`. 40 | 41 | Notes 42 | ----- 43 | Because this function computes the root of a complex number, there 44 | are several solutions to the refractive index. Always the positive 45 | (real) root of the refractive index is used. 46 | """ 47 | km = (2 * np.pi * nm) / res 48 | ri = nm * np.sqrt(f / km**2 + 1) 49 | # Always take the positive root as the refractive index. 50 | # Because f can be imaginary, numpy cannot return the correct 51 | # positive root of f. However, we know that *ri* must be postive and 52 | # thus we take the absolute value of ri. 53 | # This also is what happens in Slaneys 54 | # diffract/Src/back.c in line 414. 55 | negrootcoord = np.where(ri.real < 0) 56 | ri[negrootcoord] *= -1 57 | return ri 58 | 59 | 60 | def opt_to_ri(f, res, nm): 61 | r"""Convert the OPT object function to refractive index 62 | 63 | In :abbr:`OPT (Optical Projection Tomography)`, the object function 64 | is computed from the raw phase data. This method converts phase data 65 | to refractive index data. 66 | 67 | .. math:: 68 | 69 | n(\mathbf{r}) = n_\mathrm{m} + 70 | \frac{f(\mathbf{r}) \cdot \lambda}{2 \pi} 71 | 72 | Parameters 73 | ---------- 74 | f: n-dimensional ndarray 75 | The reconstructed object function :math:`f(\mathbf{r})`. 76 | res: float 77 | The size of the vacuum wave length :math:`\lambda` in pixels. 78 | nm: float 79 | The refractive index of the medium :math:`n_\mathrm{m}` that 80 | surrounds the object in :math:`f(\mathbf{r})`. 81 | 82 | Returns 83 | ------- 84 | ri: n-dimensional ndarray 85 | The complex refractive index :math:`n(\mathbf{r})`. 86 | 87 | Notes 88 | ----- 89 | This function is not meant to be used with diffraction tomography 90 | data. For ODT, use :py:func:`odt_to_ri` instead. 91 | """ 92 | ri = nm + f / (2 * np.pi) * res 93 | return ri 94 | -------------------------------------------------------------------------------- /odtbrain/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def compute_angle_weights_1d(angles: np.ndarray) -> np.ndarray: 5 | """Compute angular weights for tomographic reconstruction 6 | 7 | This method aims to address the issue of unevenly distributed angles 8 | in tomographic reconstruction. For algorithms, such as filtered 9 | backprojection, weighting each backprojection with a factor 10 | proportional to the angular distance to its neighbors dramatically 11 | improves the reconstructed image. 12 | 13 | Weights computation also takes into account these special cases: 14 | 15 | - Same angle present multiple times 16 | - Angular coverage larger than 180° (PI) 17 | 18 | Parameters 19 | ---------- 20 | angles: 1d ndarray of length A 21 | Angles corresponding to the projections [rad] 22 | 23 | Returns 24 | ------- 25 | weights: 1d ndarray of length A 26 | Weights for each angle; the mean of `weights` is one. 27 | 28 | Notes 29 | ----- 30 | If one angle is passed multiple times `N` (e.g. `N=2`, 0° and 180° 31 | via `np.linspace(0, np.pi, 10, endpoint=True)`), then this angle will 32 | have a weight smaller than the other angles by a factor of `1/N`. 33 | 34 | This method is dupicated in the :ref:`radontea ` 35 | package. Even though, for ODT you normally need a coverage of 2 PI 36 | (instead of one PI in OPT), it makes sense here to wrap the coverage 37 | at PI. The idea is that opposite projections contribute similarly to 38 | the reconstruction and can be treated as PI-wrapped. 39 | """ 40 | # copy and modulo np.pi 41 | # This is an array with values in [0, np.pi) 42 | angles = (angles.flatten() - np.min(angles)) % np.pi 43 | 44 | # If we have duplicate entries, we need to take them into account 45 | unq_angles, unq_reverse, unq_counts = np.unique( 46 | angles, return_inverse=True, return_counts=True) 47 | 48 | # sort the array 49 | srt_idx = np.argsort(unq_angles) 50 | srt_angles = unq_angles[srt_idx] 51 | srt_count = unq_counts[srt_idx] 52 | 53 | # compute weights for sorted angles 54 | da = (np.roll(srt_angles, -1) - np.roll(srt_angles, 1)) % np.pi 55 | sum_da = np.sum(da) 56 | 57 | # normalize with number of occurrences 58 | da /= srt_count 59 | srt_weights = da / sum_da * angles.size 60 | 61 | # Sort everything back where it belongs 62 | unq_weights = np.zeros_like(srt_weights) 63 | unq_weights[srt_idx] = srt_weights 64 | 65 | # Set the weights for each item in the original angles 66 | weights = unq_weights[unq_reverse] 67 | return weights 68 | -------------------------------------------------------------------------------- /odtbrain/warn.py: -------------------------------------------------------------------------------- 1 | class DataUndersampledWarning(UserWarning): 2 | pass 3 | 4 | 5 | class ImplementationAmbiguousWarning(UserWarning): 6 | pass 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # Defined by PEP 518: 3 | requires = [ 4 | # for version management 5 | "setuptools>=45", 6 | "setuptools_scm[toml]>=6.2", 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [project] 11 | name = "ODTbrain" 12 | authors = [ 13 | # In alphabetical order. 14 | { name = "Paul Müller" }, 15 | ] 16 | maintainers = [ 17 | { name = "Paul Müller", email = "dev@craban.de" }, 18 | ] 19 | description = "Algorithms for diffraction tomography" 20 | readme = "README.rst" 21 | requires-python = ">=3.5, <4" 22 | keywords = ["odt", "opt", "diffraction", "born", "rytov", "radon", 23 | "backprojection", "backpropagation", "inverse problem", 24 | "Fourier diffraction theorem", "Fourier slice theorem"] 25 | classifiers = [ 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python :: 3", 28 | "Topic :: Scientific/Engineering :: Visualization", 29 | "Intended Audience :: Science/Research" 30 | ] 31 | license = {file = "LICENSE"} 32 | dependencies = [ 33 | "numexpr", 34 | "numpy>=1.7.0", 35 | "pyfftw>=0.9.2,<1", 36 | "scikit-image>=0.21.0,<1", 37 | "scipy>=1.4.0,<2" 38 | ] 39 | dynamic = ["version"] 40 | 41 | [project.urls] 42 | source = "https://github.com/RI-imaging/ODTbrain" 43 | tracker = "https://github.com/RI-imaging/ODTbrain/Issues" 44 | 45 | [tool.setuptools_scm] 46 | write_to = "odtbrain/_version.py" 47 | version_scheme = "post-release" 48 | 49 | [tool.setuptools.packages.find] 50 | where = ["."] 51 | include = ["odtbrain"] 52 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | ### Test Scripts 2 | 3 | 4 | Execute all tests using `setup.py` in the parent directory: 5 | 6 | python setup.py test 7 | 8 | 9 | ### Running single tests 10 | 11 | Directly execute the scripts, e.g. 12 | 13 | 14 | python test_simple.py 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/common_methods.py: -------------------------------------------------------------------------------- 1 | """Test helper functions""" 2 | import pathlib 3 | import tempfile 4 | import warnings 5 | import zipfile 6 | 7 | import numpy as np 8 | from scipy.ndimage import rotate 9 | 10 | 11 | def create_test_sino_2d(A=9, N=22, max_phase=5.0, 12 | ampl_range=(1.0, 1.0)): 13 | """ 14 | Creates 2D test sinogram for optical diffraction tomography. 15 | The sinogram is generated from a Gaussian that is shifted 16 | according to the rotational position of a non-centered 17 | object. 18 | 19 | Parameters 20 | ---------- 21 | A : int 22 | Number of angles of the sinogram. 23 | N : int 24 | Size of one acquisition. 25 | max_phase : float 26 | Phase normalization. If this is greater than 27 | 2PI, then it also tests the unwrapping 28 | capabilities of the reconstruction algorithm. 29 | ampl_range : tuple of floats 30 | Determines the min/max range of the amplitude values. 31 | Equal values means constant amplitude. 32 | """ 33 | # initiate array 34 | resar = np.zeros((A, N), dtype=np.complex128) 35 | # 2pi coverage 36 | angles = np.linspace(0, 2*np.pi, A, endpoint=False) 37 | # x-values of Gaussian 38 | x = np.linspace(-N/2, N/2, N, endpoint=True) 39 | # SD of Gaussian 40 | dev = np.sqrt(N/2) 41 | # Off-centered rotation: 42 | off = N/7 43 | for ii in range(A): 44 | # Gaussian distribution sinogram 45 | x0 = np.cos(angles[ii])*off 46 | phase = np.exp(-(x-x0)**2/dev**2) 47 | phase = normalize(phase, vmax=max_phase) 48 | if ampl_range[0] == ampl_range[1]: 49 | # constant amplitude 50 | ampl = ampl_range[0] 51 | else: 52 | # ring 53 | ampldev = dev/5 54 | amploff = off*.3 55 | ampl1 = np.exp(-(x-x0-amploff)**2/ampldev**2) 56 | ampl2 = np.exp(-(x-x0+amploff)**2/ampldev**2) 57 | ampl = ampl1+ampl2 58 | ampl = normalize(ampl, vmin=ampl_range[0], vmax=ampl_range[1]) 59 | resar[ii] = ampl*np.exp(1j*phase) 60 | return resar, angles 61 | 62 | 63 | def create_test_sino_3d(A=9, Nx=22, Ny=22, max_phase=5.0, 64 | ampl_range=(1.0, 1.0)): 65 | """ 66 | Creates 3D test sinogram for optical diffraction tomography. 67 | The sinogram is generated from a Gaussian that is shifted 68 | according to the rotational position of a non-centered 69 | object. The simulated rotation is about the second (y)/[1] 70 | axis. 71 | 72 | Parameters 73 | ---------- 74 | A : int 75 | Number of angles of the sinogram. 76 | Nx : int 77 | Size of the first axis. 78 | Ny : int 79 | Size of the second axis. 80 | max_phase : float 81 | Phase normalization. If this is greater than 82 | 2PI, then it also tests the unwrapping 83 | capabilities of the reconstruction algorithm. 84 | ampl_range : tuple of floats 85 | Determines the min/max range of the amplitude values. 86 | Equal values means constant amplitude. 87 | 88 | Returns 89 | """ 90 | # initiate array 91 | resar = np.zeros((A, Ny, Nx), dtype=np.complex128) 92 | # 2pi coverage 93 | angles = np.linspace(0, 2*np.pi, A, endpoint=False) 94 | # x-values of Gaussian 95 | x = np.linspace(-Nx/2, Nx/2, Nx, endpoint=True).reshape(1, -1) 96 | y = np.linspace(-Ny/2, Ny/2, Ny, endpoint=True).reshape(-1, 1) 97 | # SD of Gaussian 98 | dev = min(np.sqrt(Nx/2), np.sqrt(Ny/2)) 99 | # Off-centered rotation about second axis: 100 | off = Nx/7 101 | for ii in range(A): 102 | # Gaussian distribution sinogram 103 | x0 = np.cos(angles[ii])*off 104 | phase = np.exp(-(x-x0)**2/dev**2) * np.exp(-(y)**2/dev**2) 105 | phase = normalize(phase, vmax=max_phase) 106 | if ampl_range[0] == ampl_range[1]: 107 | # constant amplitude 108 | ampl = ampl_range[0] 109 | else: 110 | # ring 111 | ampldev = dev/5 112 | amploff = off*.3 113 | ampl1 = np.exp(-(x-x0-amploff)**2/ampldev**2) 114 | ampl2 = np.exp(-(x-x0+amploff)**2/ampldev**2) 115 | ampl = ampl1+ampl2 116 | ampl = normalize(ampl, vmin=ampl_range[0], vmax=ampl_range[1]) 117 | resar[ii] = ampl*np.exp(1j*phase) 118 | return resar, angles 119 | 120 | 121 | def create_test_sino_3d_tilted(A=9, Nx=22, Ny=22, max_phase=5.0, 122 | ampl_range=(1.0, 1.0), 123 | tilt_plane=0.0): 124 | """ 125 | Creates 3D test sinogram for optical diffraction tomography. 126 | The sinogram is generated from a Gaussian that is shifted 127 | according to the rotational position of a non-centered 128 | object. The simulated rotation is about the second (y)/[1] 129 | axis. 130 | 131 | Parameters 132 | ---------- 133 | A : int 134 | Number of angles of the sinogram. 135 | Nx : int 136 | Size of the first axis. 137 | Ny : int 138 | Size of the second axis. 139 | max_phase : float 140 | Phase normalization. If this is greater than 141 | 2PI, then it also tests the unwrapping 142 | capabilities of the reconstruction algorithm. 143 | ampl_range : tuple of floats 144 | Determines the min/max range of the amplitude values. 145 | Equal values means constant amplitude. 146 | tilt_plane : float 147 | Rotation tilt offset [rad]. 148 | 149 | Returns 150 | """ 151 | # initiate array 152 | resar = np.zeros((A, Ny, Nx), dtype=np.complex128) 153 | # 2pi coverage 154 | angles = np.linspace(0, 2*np.pi, A, endpoint=False) 155 | # x-values of Gaussain 156 | x = np.linspace(-Nx/2, Nx/2, Nx, endpoint=True).reshape(1, -1) 157 | y = np.linspace(-Ny/2, Ny/2, Ny, endpoint=True).reshape(-1, 1) 158 | # SD of Gaussian 159 | dev = min(np.sqrt(Nx/2), np.sqrt(Ny/2)) 160 | # Off-centered rotation about second axis: 161 | off = Nx/7 162 | for ii in range(A): 163 | # Gaussian distribution sinogram 164 | x0 = np.cos(angles[ii])*off 165 | phase = np.exp(-(x-x0)**2/dev**2) * np.exp(-(y)**2/dev**2) 166 | phase = normalize(phase, vmax=max_phase) 167 | if ampl_range[0] == ampl_range[1]: 168 | # constant amplitude 169 | ampl = np.ones((Nx, Ny))*ampl_range[0] 170 | else: 171 | # ring 172 | ampldev = dev/5 173 | amploff = off*.3 174 | ampl1 = np.exp(-(x-x0-amploff)**2/ampldev**2) 175 | ampl2 = np.exp(-(x-x0+amploff)**2/ampldev**2) 176 | ampl = ampl1+ampl2 177 | ampl = normalize(ampl, vmin=ampl_range[0], vmax=ampl_range[1]) 178 | 179 | # perform in-plane rotation 180 | ampl = rotate(ampl, np.rad2deg(tilt_plane), reshape=False, cval=1) 181 | phase = rotate(phase, np.rad2deg(tilt_plane), reshape=False, cval=0) 182 | resar[ii] = ampl*np.exp(1j*phase) 183 | 184 | return resar, angles 185 | 186 | 187 | def cutout(a): 188 | """ cut out circle/sphere from 2D/3D square/cubic array 189 | """ 190 | x = np.arange(a.shape[0]) 191 | c = a.shape[0] / 2 192 | 193 | if len(a.shape) == 2: 194 | x = x.reshape(-1, 1) 195 | y = x.reshape(1, -1) 196 | zero = ((x-c)**2 + (y-c)**2) < c**2 197 | elif len(a.shape) == 3: 198 | x = x.reshape(-1, 1, 1) 199 | y = x.reshape(1, -1, 1) 200 | z = x.reshape(1, -1, 1) 201 | zero = ((x-c)**2 + (y-c)**2 + (z-c)**2) < c**2 202 | else: 203 | raise ValueError("Cutout array must have dimension 2 or 3!") 204 | a *= zero 205 | return a 206 | 207 | 208 | def get_results(frame): 209 | """ Get the results from the frame of a method """ 210 | filen = frame.f_globals["__file__"] 211 | funcname = frame.f_code.co_name 212 | identifier = "{}__{}".format(filen.split("test_", 1)[1][:-3], 213 | funcname) 214 | wdir = pathlib.Path(__file__).parent / "data" 215 | zipf = wdir / (identifier + ".zip") 216 | text = "data.txt" 217 | tdir = tempfile.gettempdir() 218 | 219 | if zipf.exists(): 220 | with zipfile.ZipFile(str(zipf)) as arc: 221 | arc.extract(text, tdir) 222 | else: 223 | raise ValueError("No reference found for test: {}".format(text)) 224 | 225 | tfile = pathlib.Path(tdir) / text 226 | data = np.loadtxt(str(tfile)) 227 | tfile.unlink() 228 | return data 229 | 230 | 231 | def get_test_parameter_set(set_number=1): 232 | res = 2.1 233 | lD = 0 234 | nm = 1.333 235 | parameters = [] 236 | for _i in range(set_number): 237 | parameters.append({"res": res, 238 | "lD": lD, 239 | "nm": nm}) 240 | res += .1 241 | lD += np.pi 242 | nm *= 1.01 243 | return parameters 244 | 245 | 246 | def normalize(av, vmin=0., vmax=1.): 247 | """ 248 | normalize an array to the range vmin/vmax 249 | """ 250 | if vmin == vmax: 251 | return np.ones_like(av)*vmin 252 | elif vmax < vmin: 253 | warnings.warn("swapping vmin and vmax, because vmax < vmin.") 254 | vmin, vmax = vmax, vmin 255 | 256 | norm_one = (av - np.min(av))/(np.max(av)-np.min(av)) 257 | return norm_one * (vmax-vmin) + vmin 258 | 259 | 260 | def write_results(frame, r): 261 | """ 262 | Used for writing the results to zip-files in the current directory. 263 | If put in the directory "data", these files will be used for tests. 264 | """ 265 | # cast single precision to double precision 266 | if np.iscomplexobj(r): 267 | r = np.array(r, dtype=complex) 268 | else: 269 | r = np.array(r, dtype=float) 270 | data = np.array(r).flatten().view(float) 271 | filen = frame.f_globals["__file__"] 272 | funcname = frame.f_code.co_name 273 | identifier = "{}__{}".format(filen.split("test_", 1)[1][:-3], 274 | funcname) 275 | text = pathlib.Path("data.txt") 276 | zipf = pathlib.Path(identifier+".zip") 277 | # remove existing files 278 | if text.exists(): 279 | text.unlink() 280 | if zipf.exists(): 281 | zipf.unlink() 282 | # save text 283 | np.savetxt(str(text), data, fmt="%.10f") 284 | # make zip 285 | with zipfile.ZipFile(str(zipf), 286 | "w", 287 | compression=zipfile.ZIP_DEFLATED) as arc: 288 | arc.write(str(text)) 289 | text.unlink() 290 | -------------------------------------------------------------------------------- /tests/data/alg2d_bpp__test_2d_backprop_full.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg2d_bpp__test_2d_backprop_full.zip -------------------------------------------------------------------------------- /tests/data/alg2d_bpp__test_2d_backprop_phase.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg2d_bpp__test_2d_backprop_phase.zip -------------------------------------------------------------------------------- /tests/data/alg2d_fmp__test_2d_fmap.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg2d_fmp__test_2d_fmap.zip -------------------------------------------------------------------------------- /tests/data/alg2d_int__test_2d_integrate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg2d_int__test_2d_integrate.zip -------------------------------------------------------------------------------- /tests/data/alg3d_bpp__test_3d_backprop_nopadreal.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg3d_bpp__test_3d_backprop_nopadreal.zip -------------------------------------------------------------------------------- /tests/data/alg3d_bpp__test_3d_backprop_phase.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg3d_bpp__test_3d_backprop_phase.zip -------------------------------------------------------------------------------- /tests/data/alg3d_bpp__test_3d_mprotate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/alg3d_bpp__test_3d_mprotate.zip -------------------------------------------------------------------------------- /tests/data/apple__test_correct_reproduce.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/apple__test_correct_reproduce.zip -------------------------------------------------------------------------------- /tests/data/processing__test_odt_to_ri.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/processing__test_odt_to_ri.zip -------------------------------------------------------------------------------- /tests/data/processing__test_opt_to_ri.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/processing__test_opt_to_ri.zip -------------------------------------------------------------------------------- /tests/data/processing__test_sino_radon.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/processing__test_sino_radon.zip -------------------------------------------------------------------------------- /tests/data/processing__test_sino_rytov.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RI-imaging/ODTbrain/f7bb8b792bad8ae78ea885758a801c52dfea44ad/tests/data/processing__test_sino_rytov.zip -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /tests/test_alg2d_bpp.py: -------------------------------------------------------------------------------- 1 | """Test 2d backpropagation""" 2 | import sys 3 | 4 | import numpy as np 5 | import odtbrain 6 | 7 | from common_methods import create_test_sino_2d, cutout, \ 8 | get_test_parameter_set, write_results, get_results 9 | 10 | WRITE_RES = False 11 | 12 | 13 | def test_2d_backprop_phase(): 14 | myframe = sys._getframe() 15 | sino, angles = create_test_sino_2d() 16 | parameters = get_test_parameter_set(2) 17 | r = list() 18 | for p in parameters: 19 | f = odtbrain.backpropagate_2d(sino, angles, **p) 20 | r.append(cutout(f)) 21 | if WRITE_RES: 22 | write_results(myframe, r) 23 | assert np.allclose(np.array(r).flatten().view(float), get_results(myframe)) 24 | 25 | 26 | def test_2d_backprop_full(): 27 | myframe = sys._getframe() 28 | sino, angles = create_test_sino_2d(ampl_range=(0.9, 1.1)) 29 | parameters = get_test_parameter_set(2) 30 | r = list() 31 | for p in parameters: 32 | f = odtbrain.backpropagate_2d(sino, angles, **p) 33 | r.append(cutout(f)) 34 | if WRITE_RES: 35 | write_results(myframe, r) 36 | assert np.allclose(np.array(r).flatten().view(float), get_results(myframe)) 37 | 38 | 39 | def test_2d_backprop_real(): 40 | """ 41 | Check if the real reconstruction matches the real part 42 | of the complex reconstruction. 43 | """ 44 | sino, angles = create_test_sino_2d() 45 | parameters = get_test_parameter_set(2) 46 | # complex 47 | r = list() 48 | for p in parameters: 49 | f = odtbrain.backpropagate_2d(sino, angles, padval=0, **p) 50 | r.append(f) 51 | # real 52 | r2 = list() 53 | for p in parameters: 54 | f = odtbrain.backpropagate_2d(sino, angles, padval=0, 55 | onlyreal=True, **p) 56 | r2.append(f) 57 | assert np.allclose(np.array(r).real, np.array(r2)) 58 | 59 | 60 | if __name__ == "__main__": 61 | # Run all tests 62 | loc = locals() 63 | for key in list(loc.keys()): 64 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 65 | loc[key]() 66 | -------------------------------------------------------------------------------- /tests/test_alg2d_fmp.py: -------------------------------------------------------------------------------- 1 | """Test Fourier mapping algorithm""" 2 | import sys 3 | 4 | import numpy as np 5 | import odtbrain 6 | import pytest 7 | 8 | from common_methods import create_test_sino_2d, cutout, \ 9 | get_test_parameter_set, write_results, get_results 10 | 11 | WRITE_RES = False 12 | 13 | 14 | @pytest.mark.xfail(True, reason="Unexplained issue #13 and new algorithm #19") 15 | @pytest.mark.filterwarnings( 16 | "ignore::odtbrain.warn.ImplementationAmbiguousWarning") 17 | def test_2d_fmap(): 18 | myframe = sys._getframe() 19 | sino, angles = create_test_sino_2d() 20 | parameters = get_test_parameter_set(1) 21 | r = [] 22 | for p in parameters: 23 | f = odtbrain.fourier_map_2d(sino, angles, **p) 24 | r.append(cutout(f)) 25 | if WRITE_RES: 26 | write_results(myframe, r) 27 | assert np.allclose(np.array(r).flatten().view(float), get_results(myframe)) 28 | -------------------------------------------------------------------------------- /tests/test_alg2d_int.py: -------------------------------------------------------------------------------- 1 | """Test slow integration algorithm""" 2 | import sys 3 | 4 | import numpy as np 5 | import odtbrain 6 | 7 | import pytest 8 | 9 | from common_methods import create_test_sino_2d, cutout, \ 10 | get_test_parameter_set, write_results, get_results 11 | 12 | WRITE_RES = False 13 | 14 | 15 | @pytest.mark.filterwarnings( 16 | "ignore::odtbrain.warn.DataUndersampledWarning") 17 | def test_2d_integrate(): 18 | myframe = sys._getframe() 19 | sino, angles = create_test_sino_2d() 20 | parameters = get_test_parameter_set(2) 21 | r = list() 22 | 23 | for p in parameters: 24 | f = odtbrain.integrate_2d(sino, angles, **p) 25 | r.append(cutout(f)) 26 | 27 | if WRITE_RES: 28 | write_results(myframe, r) 29 | assert np.allclose(np.array(r).flatten().view(float), get_results(myframe)) 30 | 31 | 32 | if __name__ == "__main__": 33 | # Run all tests 34 | loc = locals() 35 | for key in list(loc.keys()): 36 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 37 | loc[key]() 38 | -------------------------------------------------------------------------------- /tests/test_alg3d_bpp.py: -------------------------------------------------------------------------------- 1 | """Test 3D backpropagation algorithm""" 2 | import ctypes 3 | import multiprocessing as mp 4 | import platform 5 | import sys 6 | 7 | import numpy as np 8 | 9 | import odtbrain 10 | from odtbrain import _alg3d_bpp 11 | 12 | from common_methods import create_test_sino_3d, cutout, \ 13 | get_test_parameter_set, write_results, get_results 14 | 15 | WRITE_RES = False 16 | 17 | 18 | def helper_3d_backprop_phase(): 19 | sino, angles = create_test_sino_3d() 20 | parameters = get_test_parameter_set(2) 21 | r = list() 22 | for p in parameters: 23 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 24 | dtype=float, **p) 25 | r.append(cutout(f)) 26 | data = np.array(r).flatten().view(float) 27 | return data 28 | 29 | 30 | def test_3d_backprop_phase(): 31 | myframe = sys._getframe() 32 | sino, angles = create_test_sino_3d() 33 | parameters = get_test_parameter_set(2) 34 | r = list() 35 | for p in parameters: 36 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 37 | dtype=float, **p) 38 | r.append(cutout(f)) 39 | if WRITE_RES: 40 | write_results(myframe, r) 41 | data = np.array(r).flatten().view(float) 42 | assert np.allclose(data, get_results(myframe)) 43 | 44 | 45 | def test_3d_backprop_nopadreal(): 46 | """ 47 | - no padding 48 | - only real result 49 | """ 50 | platform.system = lambda: "Windows" 51 | myframe = sys._getframe() 52 | sino, angles = create_test_sino_3d() 53 | parameters = get_test_parameter_set(2) 54 | r = list() 55 | for p in parameters: 56 | f = odtbrain.backpropagate_3d(sino, angles, padding=(False, False), 57 | dtype=float, onlyreal=True, **p) 58 | r.append(cutout(f)) 59 | if WRITE_RES: 60 | write_results(myframe, r) 61 | data = np.array(r).flatten().view(float) 62 | assert np.allclose(data, get_results(myframe)) 63 | 64 | 65 | def test_3d_backprop_windows(): 66 | """ 67 | We assume that we are not running these tests on windows. 68 | So we perform a test with fake windows to increase coverage. 69 | """ 70 | datalin = helper_3d_backprop_phase() 71 | real_system = platform.system 72 | datawin = helper_3d_backprop_phase() 73 | platform.system = real_system 74 | assert np.allclose(datalin, datawin) 75 | 76 | 77 | def test_3d_backprop_real(): 78 | """ 79 | Check if the real reconstruction matches the real part 80 | of the complex reconstruction. 81 | """ 82 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 83 | parameters = get_test_parameter_set(2) 84 | # complex 85 | r = list() 86 | for p in parameters: 87 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 88 | dtype=float, 89 | onlyreal=False, **p) 90 | r.append(f) 91 | # real 92 | r2 = list() 93 | for p in parameters: 94 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 95 | dtype=float, 96 | onlyreal=True, **p) 97 | r2.append(f) 98 | assert np.allclose(np.array(r).real, np.array(r2)) 99 | 100 | 101 | def test_3d_backprop_phase32(): 102 | sino, angles = create_test_sino_3d() 103 | parameters = get_test_parameter_set(2) 104 | r = list() 105 | for p in parameters: 106 | f = odtbrain.backpropagate_3d(sino, angles, 107 | dtype=np.float32, 108 | padval=0, 109 | **p) 110 | r.append(cutout(f)) 111 | data32 = np.array(r).flatten().view(np.float32) 112 | data64 = helper_3d_backprop_phase() 113 | assert np.allclose(data32, data64, atol=6e-7, rtol=0) 114 | 115 | 116 | def test_3d_mprotate(): 117 | myframe = sys._getframe() 118 | ln = 10 119 | ln2 = 2*ln 120 | initial_array = np.arange(ln2**3).reshape((ln2, ln2, ln2)) 121 | shared_array = mp.RawArray(ctypes.c_double, ln2 * ln2 * ln2) 122 | arr = np.frombuffer(shared_array).reshape(ln2, ln2, ln2) 123 | arr[:, :, :] = initial_array 124 | _alg3d_bpp.mprotate_dict["X"] = shared_array 125 | _alg3d_bpp.mprotate_dict["X_shape"] = (ln2, ln2, ln2) 126 | 127 | pool = mp.Pool(processes=mp.cpu_count(), 128 | initializer=_alg3d_bpp._init_worker, 129 | initargs=(shared_array, (ln2, ln2, ln2), np.dtype(float))) 130 | _alg3d_bpp._mprotate(2, ln, pool, 2) 131 | if WRITE_RES: 132 | write_results(myframe, arr) 133 | assert np.allclose(np.array(arr).flatten().view( 134 | float), get_results(myframe)) 135 | -------------------------------------------------------------------------------- /tests/test_alg3d_bppt.py: -------------------------------------------------------------------------------- 1 | """Test tilted backpropagation algorithm""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | 6 | from common_methods import create_test_sino_3d, create_test_sino_3d_tilted, \ 7 | cutout, get_test_parameter_set 8 | 9 | 10 | def test_3d_backprop_phase_real(): 11 | sino, angles = create_test_sino_3d() 12 | parameters = get_test_parameter_set(2) 13 | # reference 14 | rref = list() 15 | for p in parameters: 16 | fref = odtbrain.backpropagate_3d(sino, angles, padval=0, 17 | dtype=float, onlyreal=True, **p) 18 | rref.append(cutout(fref)) 19 | dataref = np.array(rref).flatten().view(float) 20 | 21 | r = list() 22 | for p in parameters: 23 | f = odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 24 | dtype=float, onlyreal=True, 25 | **p) 26 | r.append(cutout(f)) 27 | data = np.array(r).flatten().view(float) 28 | assert np.allclose(data, dataref) 29 | 30 | 31 | def test_3d_backprop_pad(): 32 | sino, angles = create_test_sino_3d() 33 | parameters = get_test_parameter_set(2) 34 | # reference 35 | rref = list() 36 | for p in parameters: 37 | fref = odtbrain.backpropagate_3d(sino, angles, padval="edge", 38 | dtype=float, onlyreal=False, **p) 39 | rref.append(cutout(fref)) 40 | dataref = np.array(rref).flatten().view(float) 41 | 42 | r = list() 43 | for p in parameters: 44 | f = odtbrain.backpropagate_3d_tilted(sino, angles, padval="edge", 45 | dtype=float, onlyreal=False, 46 | **p) 47 | r.append(cutout(f)) 48 | data = np.array(r).flatten().view(float) 49 | 50 | assert np.allclose(data, dataref) 51 | 52 | 53 | def test_3d_backprop_plane_rotation(): 54 | """ 55 | A very soft test to check if planar rotation works fine 56 | in the reconstruction with tilted angles. 57 | """ 58 | parameters = get_test_parameter_set(1) 59 | results = [] 60 | 61 | # These are specially selected angles that don't give high results. 62 | # Probably due to phase-wrapping, errors >2 may appear. Hence, we 63 | # call it a soft test. 64 | tilts = [1.1, 0.0, 0.234, 2.80922, -.29, 9.87] 65 | 66 | for angz in tilts: 67 | sino, angles = create_test_sino_3d_tilted(tilt_plane=angz, A=21) 68 | rotmat = np.array([ 69 | [np.cos(angz), -np.sin(angz), 0], 70 | [np.sin(angz), np.cos(angz), 0], 71 | [0, 0, 1], 72 | ]) 73 | # rotate `tilted_axis` onto the y-z plane. 74 | tilted_axis = np.dot(rotmat, [0, 1, 0]) 75 | 76 | rref = list() 77 | for p in parameters: 78 | fref = odtbrain.backpropagate_3d_tilted(sino, angles, 79 | padval="edge", 80 | tilted_axis=tilted_axis, 81 | padding=(False, False), 82 | dtype=float, 83 | onlyreal=False, 84 | **p) 85 | rref.append(cutout(fref)) 86 | data = np.array(rref).flatten().view(float) 87 | results.append(data) 88 | 89 | for ii in np.arange(len(results)): 90 | assert np.allclose(results[ii], results[ii-1], atol=.2, rtol=.2) 91 | 92 | 93 | def test_3d_backprop_plane_alignment_along_axes(): 94 | """ 95 | Tests whether the reconstruction is always aligned with 96 | the rotational axis (and not antiparallel). 97 | """ 98 | parameters = get_test_parameter_set(1) 99 | p = parameters[0] 100 | results = [] 101 | 102 | # These are specially selected angles that don't give high results. 103 | # Probably due to phase-wrapping, errors >2 may appear. Hence, we 104 | # call it a soft test. 105 | tilts = [0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi] 106 | 107 | for angz in tilts: 108 | sino, angles = create_test_sino_3d_tilted(tilt_plane=angz, A=21) 109 | rotmat = np.array([ 110 | [np.cos(angz), -np.sin(angz), 0], 111 | [np.sin(angz), np.cos(angz), 0], 112 | [0, 0, 1], 113 | ]) 114 | # rotate `tilted_axis` onto the y-z plane. 115 | tilted_axis = np.dot(rotmat, [0, 1, 0]) 116 | fref = odtbrain.backpropagate_3d_tilted(sino, angles, 117 | padval="edge", 118 | tilted_axis=tilted_axis, 119 | padding=(False, False), 120 | dtype=float, 121 | onlyreal=True, 122 | **p) 123 | results.append(fref) 124 | 125 | for ii in np.arange(len(results)): 126 | assert np.allclose(results[ii], results[ii-1], atol=.2, rtol=.2) 127 | 128 | 129 | if __name__ == "__main__": 130 | # Run all tests 131 | loc = locals() 132 | for key in list(loc.keys()): 133 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 134 | loc[key]() 135 | -------------------------------------------------------------------------------- /tests/test_angle_weights.py: -------------------------------------------------------------------------------- 1 | """Tests 1D angular weights""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | 6 | from common_methods import create_test_sino_2d, get_test_parameter_set 7 | 8 | 9 | def test_angle_offset(): 10 | """ 11 | Tests if things are still correct when there is a 2PI offset in the angles. 12 | """ 13 | sino, angles = create_test_sino_2d() 14 | parameters = get_test_parameter_set(2) 15 | # reference 16 | r1 = [] 17 | for p in parameters: 18 | f1 = odtbrain.backpropagate_2d(sino, angles, weight_angles=False, **p) 19 | r1.append(f1) 20 | # with offset 21 | angles[::2] += 2*np.pi*np.arange(angles[::2].shape[0]) 22 | r2 = [] 23 | for p in parameters: 24 | f2 = odtbrain.backpropagate_2d(sino, angles, weight_angles=False, **p) 25 | r2.append(f2) 26 | # with offset and weights 27 | r3 = [] 28 | for p in parameters: 29 | f3 = odtbrain.backpropagate_2d(sino, angles, weight_angles=True, **p) 30 | r3.append(f3) 31 | assert np.allclose(np.array(r1).flatten().view(float), 32 | np.array(r2).flatten().view(float)) 33 | assert np.allclose(np.array(r2).flatten().view(float), 34 | np.array(r3).flatten().view(float)) 35 | 36 | 37 | def test_angle_swap(): 38 | """ 39 | Test if everything still works, when angles are swapped. 40 | """ 41 | sino, angles = create_test_sino_2d() 42 | # remove elements so that we can see that weighting works 43 | angles = angles[:-2] 44 | sino = sino[:-2, :] 45 | parameters = get_test_parameter_set(2) 46 | # reference 47 | r1 = [] 48 | for p in parameters: 49 | f1 = odtbrain.backpropagate_2d(sino, angles, weight_angles=True, **p) 50 | r1.append(f1) 51 | # change order of angles 52 | order = np.argsort(angles % .5) 53 | angles = angles[order] 54 | sino = sino[order, :] 55 | r2 = [] 56 | for p in parameters: 57 | f2 = odtbrain.backpropagate_2d(sino, angles, weight_angles=True, **p) 58 | r2.append(f2) 59 | assert np.allclose(np.array(r1).flatten().view(float), 60 | np.array(r2).flatten().view(float)) 61 | 62 | 63 | if __name__ == "__main__": 64 | # Run all tests 65 | loc = locals() 66 | for key in list(loc.keys()): 67 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 68 | loc[key]() 69 | -------------------------------------------------------------------------------- /tests/test_apple.py: -------------------------------------------------------------------------------- 1 | """Test apple core correction""" 2 | import multiprocessing as mp 3 | import sys 4 | 5 | import numpy as np 6 | 7 | import odtbrain 8 | 9 | from common_methods import create_test_sino_3d, cutout, \ 10 | get_test_parameter_set, write_results, get_results 11 | 12 | 13 | WRITE_RES = False 14 | 15 | 16 | def test_apple_core_3d_values(): 17 | try: 18 | odtbrain.apple.apple_core_3d(shape=(10, 10, 5), 19 | res=.1, 20 | nm=1) 21 | except ValueError: 22 | pass 23 | else: 24 | assert False, "bad input shape should raise ValueError" 25 | 26 | 27 | def test_correct_counter(): 28 | count = mp.Value("I", lock=True) 29 | max_count = mp.Value("I", lock=True) 30 | 31 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 32 | p = get_test_parameter_set(1)[0] 33 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 34 | dtype=float, 35 | copy=False, **p) 36 | odtbrain.apple.correct(f=f, 37 | res=p["res"], 38 | nm=p["nm"], 39 | enforce_envelope=.95, 40 | max_iter=100, 41 | min_diff=0.01, 42 | count=count, 43 | max_count=max_count) 44 | 45 | assert count.value == max_count.value 46 | 47 | 48 | def test_correct_reproduce(): 49 | myframe = sys._getframe() 50 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 51 | p = get_test_parameter_set(1)[0] 52 | sryt = odtbrain.sinogram_as_rytov(uSin=sino, u0=1, align=False) 53 | f = odtbrain.backpropagate_3d(sryt, angles, padval=0, 54 | dtype=float, 55 | copy=False, **p) 56 | fc = odtbrain.apple.correct(f=f, 57 | res=p["res"], 58 | nm=p["nm"], 59 | enforce_envelope=.95, 60 | max_iter=100, 61 | min_diff=0.01) 62 | fo = cutout(fc) 63 | fo = np.array(fo, dtype=np.complex128) 64 | 65 | if WRITE_RES: 66 | write_results(myframe, fo) 67 | 68 | data = fo.flatten().view(float) 69 | assert np.allclose(data, get_results(myframe), atol=2.E-6) 70 | 71 | 72 | def test_correct_values(): 73 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 74 | p = get_test_parameter_set(1)[0] 75 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 76 | dtype=float, 77 | copy=False, **p) 78 | try: 79 | odtbrain.apple.correct(f=f, 80 | res=p["res"], 81 | nm=p["nm"], 82 | enforce_envelope=1.05, 83 | ) 84 | except ValueError: 85 | pass 86 | else: 87 | assert False, "`enforce_envelope` must be in [0, 1]" 88 | 89 | 90 | def test_envelope_gauss_shape(): 91 | """Make sure non-cubic input shape works""" 92 | # non-cubic reconstruction volume (1st and 3rd axis still have same length) 93 | shape = (60, 50, 60) 94 | ftdata = np.ones(shape) 95 | core = odtbrain.apple.apple_core_3d(shape=shape, res=.1, nm=1) 96 | envlp = odtbrain.apple.envelope_gauss(ftdata=ftdata, core=core) 97 | assert envlp.shape == shape 98 | 99 | 100 | if __name__ == "__main__": 101 | # Run all tests 102 | loc = locals() 103 | for key in list(loc.keys()): 104 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 105 | loc[key]() 106 | -------------------------------------------------------------------------------- /tests/test_copy.py: -------------------------------------------------------------------------------- 1 | """Test copying arrays""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | 6 | from common_methods import create_test_sino_3d, get_test_parameter_set 7 | 8 | 9 | def test_back3d(): 10 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 11 | p = get_test_parameter_set(1)[0] 12 | # complex 13 | f1 = odtbrain.backpropagate_3d(sino, angles, padval=0, 14 | dtype=float, 15 | copy=False, **p) 16 | f2 = odtbrain.backpropagate_3d(sino, angles, padval=0, 17 | dtype=float, 18 | copy=True, **p) 19 | assert np.allclose(f1, f2) 20 | 21 | 22 | def test_back3d_tilted(): 23 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 24 | p = get_test_parameter_set(1)[0] 25 | f1 = odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 26 | dtype=float, 27 | copy=False, **p) 28 | f2 = odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 29 | dtype=float, 30 | copy=True, **p) 31 | assert np.allclose(f1, f2) 32 | 33 | 34 | if __name__ == "__main__": 35 | # Run all tests 36 | loc = locals() 37 | for key in list(loc.keys()): 38 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 39 | loc[key]() 40 | -------------------------------------------------------------------------------- /tests/test_counters.py: -------------------------------------------------------------------------------- 1 | """Tests progress counters""" 2 | import multiprocessing as mp 3 | 4 | import pytest 5 | 6 | import odtbrain 7 | 8 | from common_methods import create_test_sino_2d, create_test_sino_3d, \ 9 | get_test_parameter_set 10 | 11 | 12 | @pytest.mark.filterwarnings( 13 | "ignore::odtbrain.warn.DataUndersampledWarning") 14 | def test_integrate_2d(): 15 | sino, angles = create_test_sino_2d(N=10) 16 | p = get_test_parameter_set(1)[0] 17 | # complex 18 | jmc = mp.Value("i", 0) 19 | jmm = mp.Value("i", 0) 20 | 21 | odtbrain.integrate_2d(sino, angles, 22 | count=jmc, 23 | max_count=jmm, 24 | **p) 25 | 26 | assert jmc.value == jmm.value 27 | assert jmc.value != 0 28 | 29 | 30 | @pytest.mark.filterwarnings( 31 | "ignore::odtbrain.warn.ImplementationAmbiguousWarning") 32 | def test_fmp_2d(): 33 | sino, angles = create_test_sino_2d(N=10) 34 | p = get_test_parameter_set(1)[0] 35 | # complex 36 | jmc = mp.Value("i", 0) 37 | jmm = mp.Value("i", 0) 38 | 39 | odtbrain.fourier_map_2d(sino, angles, 40 | count=jmc, 41 | max_count=jmm, 42 | **p) 43 | 44 | assert jmc.value == jmm.value 45 | assert jmc.value != 0 46 | 47 | 48 | def test_bpp_2d(): 49 | sino, angles = create_test_sino_2d(N=10) 50 | p = get_test_parameter_set(1)[0] 51 | # complex 52 | jmc = mp.Value("i", 0) 53 | jmm = mp.Value("i", 0) 54 | 55 | odtbrain.backpropagate_2d(sino, angles, padval=0, 56 | count=jmc, 57 | max_count=jmm, 58 | **p) 59 | assert jmc.value == jmm.value 60 | assert jmc.value != 0 61 | 62 | 63 | def test_back3d(): 64 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 65 | p = get_test_parameter_set(1)[0] 66 | # complex 67 | jmc = mp.Value("i", 0) 68 | jmm = mp.Value("i", 0) 69 | odtbrain.backpropagate_3d(sino, angles, padval=0, 70 | dtype=float, 71 | count=jmc, 72 | max_count=jmm, 73 | **p) 74 | assert jmc.value == jmm.value 75 | assert jmc.value != 0 76 | 77 | 78 | def test_back3d_tilted(): 79 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 80 | p = get_test_parameter_set(1)[0] 81 | # complex 82 | jmc = mp.Value("i", 0) 83 | jmm = mp.Value("i", 0) 84 | odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 85 | dtype=float, 86 | count=jmc, 87 | max_count=jmm, 88 | **p) 89 | assert jmc.value == jmm.value 90 | assert jmc.value != 0 91 | 92 | 93 | if __name__ == "__main__": 94 | # Run all tests 95 | loc = locals() 96 | for key in list(loc.keys()): 97 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 98 | loc[key]() 99 | -------------------------------------------------------------------------------- /tests/test_processing.py: -------------------------------------------------------------------------------- 1 | """Tests refractive index conversion techniques""" 2 | import sys 3 | 4 | import numpy as np 5 | 6 | import odtbrain 7 | from odtbrain._prepare_sino import divmod_neg 8 | 9 | from common_methods import write_results, get_results 10 | 11 | WRITE_RES = False 12 | 13 | 14 | def get_test_data_set(): 15 | """returns 3D array and parameters""" 16 | ln = 10 17 | f = np.arange(ln**3).reshape(ln, ln, ln) 18 | f = f + np.linspace(1, 2, ln) 19 | res = 7 20 | nm = 1.34 21 | return f, res, nm 22 | 23 | 24 | def get_test_data_set_sino(rytov=False): 25 | """returns 3D array""" 26 | ln = 10 27 | a = 2 28 | sino = np.arange(ln*ln*a).reshape(a, ln, ln) / (ln*ln*a) * \ 29 | np.exp(1j*np.arange(ln*ln*a).reshape(a, ln, ln)) 30 | if rytov: 31 | sino[0, 0, 0] = .1 32 | return sino 33 | 34 | 35 | def negative_modulo_rest(a, b): 36 | """returns modulo with closest result to zero""" 37 | q = np.array(a / b, dtype=int) 38 | r = a - b * q 39 | 40 | # make sure r is close to zero 41 | wrong = np.where(np.abs(r) > b/2) 42 | r[wrong] -= b * np.sign(r[wrong]) 43 | return r 44 | 45 | 46 | def negative_modulo_rest_imag(x, b): 47 | """only modulo the imaginary part""" 48 | a = x.imag 49 | return x.real + 1j*negative_modulo_rest(a, b) 50 | 51 | 52 | def test_odt_to_ri(): 53 | myframe = sys._getframe() 54 | f, res, nm = get_test_data_set() 55 | ri = odtbrain.odt_to_ri(f=f, res=res, nm=nm) 56 | if WRITE_RES: 57 | write_results(myframe, ri) 58 | assert np.allclose(np.array(ri).flatten().view( 59 | float), get_results(myframe)) 60 | # Also test 2D version 61 | ri2d = odtbrain.odt_to_ri(f=f[0], res=res, nm=nm) 62 | assert np.allclose(ri2d, ri[0]) 63 | 64 | 65 | def test_opt_to_ri(): 66 | myframe = sys._getframe() 67 | f, res, nm = get_test_data_set() 68 | ri = odtbrain.opt_to_ri(f=f, res=res, nm=nm) 69 | if WRITE_RES: 70 | write_results(myframe, ri) 71 | assert np.allclose(np.array(ri).flatten().view( 72 | float), get_results(myframe)) 73 | # Also test 2D version 74 | ri2d = odtbrain.opt_to_ri(f=f[0], res=res, nm=nm) 75 | assert np.allclose(ri2d, ri[0]) 76 | 77 | 78 | def test_sino_radon(): 79 | myframe = sys._getframe() 80 | sino = get_test_data_set_sino() 81 | rad = odtbrain.sinogram_as_radon(sino) 82 | twopi = 2*np.pi 83 | # When moving from unwrap to skimage, there was an offset introduced. 84 | # Since this particular array is not flat at the borders, there is no 85 | # correct way here. We just subtract 2PI. 86 | # 2019-04-18: It turns out that on Windows, this is not the case. 87 | # Hence, we only subtract 2PI if the minimum of the array is above 88 | # 2PI.. 89 | if rad.min() > twopi: 90 | rad -= twopi 91 | if WRITE_RES: 92 | write_results(myframe, rad) 93 | assert np.allclose(np.array(rad).flatten().view( 94 | float), get_results(myframe)) 95 | # Check the 3D result with the 2D result. They should be the same except 96 | # for a multiple of 2PI offset, because odtbrain._align_unwrapped 97 | # subtracts the background such that the minimum phase change is closest 98 | # to zero. 99 | # 2D A 100 | rad2d = odtbrain.sinogram_as_radon(sino[:, :, 0]) 101 | assert np.allclose(0, negative_modulo_rest( 102 | rad2d - rad[:, :, 0], twopi), atol=1e-6) 103 | # 2D B 104 | rad2d2 = odtbrain.sinogram_as_radon(sino[:, 0, :]) 105 | assert np.allclose(0, negative_modulo_rest( 106 | rad2d2 - rad[:, 0, :], twopi), atol=1e-6) 107 | 108 | 109 | def test_sino_rytov(): 110 | myframe = sys._getframe() 111 | sino = get_test_data_set_sino(rytov=True) 112 | ryt = odtbrain.sinogram_as_rytov(sino) 113 | twopi = 2*np.pi 114 | if WRITE_RES: 115 | write_results(myframe, ryt) 116 | # When moving from unwrap to skimage, there was an offset introduced. 117 | # Since this particular array is not flat at the borders, there is no 118 | # correct way here. We just subtract 2PI. 119 | # 2019-04-18: It turns out that on Windows, this is not the case. 120 | # Hence, we only subtract 2PI if the minimum of the array is above 121 | # 2PI.. 122 | if ryt.imag.min() > twopi: 123 | ryt.imag -= twopi 124 | assert np.allclose(np.array(ryt).flatten().view( 125 | float), get_results(myframe)) 126 | # Check the 3D result with the 2D result. They should be the same except 127 | # for a multiple of 2PI offset, because odtbrain._align_unwrapped 128 | # subtracts the background such that the median phase change is closest 129 | # to zero. 130 | # 2D A 131 | ryt2d = odtbrain.sinogram_as_rytov(sino[:, :, 0]) 132 | assert np.allclose(0, negative_modulo_rest_imag( 133 | ryt2d - ryt[:, :, 0], twopi).view(float), atol=1e-6) 134 | # 2D B 135 | ryt2d2 = odtbrain.sinogram_as_rytov(sino[:, 0, :]) 136 | assert np.allclose(0, negative_modulo_rest_imag( 137 | ryt2d2 - ryt[:, 0, :], twopi).view(float), atol=1e-6) 138 | 139 | 140 | def test_divmod_neg(): 141 | assert np.allclose(divmod_neg(0, 2*np.pi), (0, 0)) 142 | assert np.allclose(divmod_neg(-1e-17, 2*np.pi), (0, 0)) 143 | assert np.allclose(divmod_neg(1e-17, 2*np.pi), (0, 0)) 144 | assert np.allclose(divmod_neg(-.1, 2*np.pi), (0, -.1)) 145 | assert np.allclose(divmod_neg(.1, 2*np.pi), (0, .1)) 146 | assert np.allclose(divmod_neg(3*np.pi, 2*np.pi), (1, np.pi)) 147 | assert np.allclose(divmod_neg(-.99*np.pi, 2*np.pi), (0, -.99*np.pi)) 148 | assert np.allclose(divmod_neg(-1.01*np.pi, 2*np.pi), (-1, .99*np.pi)) 149 | 150 | 151 | if __name__ == "__main__": 152 | # Run all tests 153 | loc = locals() 154 | for key in list(loc.keys()): 155 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 156 | loc[key]() 157 | -------------------------------------------------------------------------------- /tests/test_rotation_matrices.py: -------------------------------------------------------------------------------- 1 | """Test 3D backpropagation with tilted axis of ration: matrices""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | import odtbrain._alg3d_bppt 6 | 7 | 8 | def test_rotate_points_to_axis(): 9 | # rotation of axis itself always goes to y-axis 10 | rot1 = odtbrain._alg3d_bppt.rotate_points_to_axis( 11 | points=[[1, 2, 3]], axis=[1, 2, 3]) 12 | assert rot1[0][0] < 1e-14 13 | assert rot1[0][2] < 1e-14 14 | rot2 = odtbrain._alg3d_bppt.rotate_points_to_axis( 15 | points=[[-3, .6, .1]], axis=[-3, .6, .1]) 16 | assert rot2[0][0] < 1e-14 17 | assert rot2[0][2] < 1e-14 18 | 19 | sq2 = np.sqrt(2) 20 | # rotation to 45deg about x 21 | points = [[0, 0, 1], [1, 0, 0], [1, 1, 0]] 22 | rot3 = odtbrain._alg3d_bppt.rotate_points_to_axis( 23 | points=points, axis=[0, 1, 1]) 24 | assert np.allclose(rot3[0], [0, 1/sq2, 1/sq2]) 25 | assert np.allclose(rot3[1], [1, 0, 0]) 26 | assert np.allclose(rot3[2], [1, 1/sq2, -1/sq2]) 27 | 28 | # rotation to 45deg about y 29 | points = [[0, 0, 1], [1, 0, 0], [0, -1, 0]] 30 | rot4 = odtbrain._alg3d_bppt.rotate_points_to_axis( 31 | points=points, axis=[1, 0, 1]) 32 | assert np.allclose(rot4[0], [-.5, 1/sq2, .5]) 33 | assert np.allclose(rot4[1], [.5, 1/sq2, -.5]) 34 | assert np.allclose(rot4[2], [1/sq2, 0, 1/sq2]) 35 | 36 | # Visualization 37 | # plt, Arrow3D = setup_mpl() 38 | # fig = plt.figure(figsize=(10,10)) 39 | # ax = fig.add_subplot(111, projection='3d') 40 | # for vec in points: 41 | # u,v,w = vec 42 | # a = Arrow3D([0,u],[0,v],[0,w], mutation_scale=20, 43 | # lw=1, arrowstyle="-|>", color="k") 44 | # ax.add_artist(a) 45 | # for vec in rot4: 46 | # u,v,w = vec 47 | # a = Arrow3D([0,u],[0,v],[0,w], mutation_scale=20, lw=1, 48 | # arrowstyle="-|>", color="b") 49 | # ax.add_artist(a) 50 | # radius=1 51 | # ax.set_xlabel('X') 52 | # ax.set_ylabel('Y') 53 | # ax.set_zlabel('Z') 54 | # ax.set_xlim(-radius*1.5, radius*1.5) 55 | # ax.set_ylim(-radius*1.5, radius*1.5) 56 | # ax.set_zlim(-radius*1.5, radius*1.5) 57 | # plt.tight_layout() 58 | # plt.show() 59 | 60 | # rotation to -90deg about z 61 | points = [[0, 0, 1], [1, 0, 1], [1, -1, 0]] 62 | rot4 = odtbrain._alg3d_bppt.rotate_points_to_axis( 63 | points=points, axis=[1, 0, 0]) 64 | assert np.allclose(rot4[0], [0, 0, 1]) 65 | assert np.allclose(rot4[1], [0, 1, 1]) 66 | assert np.allclose(rot4[2], [1, 1, 0]) 67 | 68 | # negative axes 69 | # In this case, everything is rotated in the y-z plane 70 | # (this case is not physical for tomogrpahy) 71 | points = [[0, 0, 1], [1, 0, 0], [1, -1, 0]] 72 | rot4 = odtbrain._alg3d_bppt.rotate_points_to_axis( 73 | points=points, axis=[0, -1, 0]) 74 | assert np.allclose(rot4[0], [0, 0, -1]) 75 | assert np.allclose(rot4[1], [1, 0, 0]) 76 | assert np.allclose(rot4[2], [1, 1, 0]) 77 | 78 | 79 | def test_rotation_matrix_from_point(): 80 | """ 81 | `rotation_matrix_from_point` generates a matrix that rotates a point at 82 | [0,0,1] to the position of the argument of the method. 83 | """ 84 | sq2 = np.sqrt(2) 85 | # identity 86 | m1 = odtbrain._alg3d_bppt.rotation_matrix_from_point([0, 0, 1]) 87 | assert np.allclose(np.dot(m1, [1, 2, 3]), [1, 2, 3]) 88 | assert np.allclose(np.dot(m1, [-3, .5, -.6]), [-3, .5, -.6]) 89 | 90 | # simple 91 | m2 = odtbrain._alg3d_bppt.rotation_matrix_from_point([0, 1, 1]) 92 | assert np.allclose(np.dot(m2, [0, 0, 1]), [0, 1/sq2, 1/sq2]) 93 | assert np.allclose(np.dot(m2, [0, 1, 1]), [0, sq2, 0]) 94 | assert np.allclose(np.dot(m2, [1, 0, 0]), [1, 0, 0]) 95 | 96 | # negative 97 | m3 = odtbrain._alg3d_bppt.rotation_matrix_from_point([-1, 1, 0]) 98 | assert np.allclose(np.dot(m3, [1, 0, 0]), [0, 0, -1]) 99 | assert np.allclose(np.dot(m3, [0, 1, 1]), [0, sq2, 0]) 100 | assert np.allclose(np.dot(m3, [0, -1/sq2, -1/sq2]), [0, -1, 0]) 101 | assert np.allclose(np.dot(m3, [0, 1/sq2, -1/sq2]), [-1, 0, 0]) 102 | assert np.allclose(np.dot(m3, [0, -1/sq2, 1/sq2]), [1, 0, 0]) 103 | 104 | 105 | def setup_mpl(): 106 | import matplotlib.pylab as plt 107 | from mpl_toolkits.mplot3d import Axes3D # noqa F01 108 | from matplotlib.patches import FancyArrowPatch 109 | from mpl_toolkits.mplot3d import proj3d 110 | 111 | class Arrow3D(FancyArrowPatch): 112 | def __init__(self, xs, ys, zs, *args, **kwargs): 113 | FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs) 114 | self._verts3d = xs, ys, zs 115 | 116 | def draw(self, renderer): 117 | xs3d, ys3d, zs3d = self._verts3d 118 | xs, ys, _zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M) 119 | self.set_positions((xs[0], ys[0]), (xs[1], ys[1])) 120 | FancyArrowPatch.draw(self, renderer) 121 | 122 | return plt, Arrow3D 123 | 124 | 125 | if __name__ == "__main__": 126 | # Run all tests 127 | loc = locals() 128 | for key in list(loc.keys()): 129 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 130 | loc[key]() 131 | 132 | import scipy.ndimage 133 | plt, Arrow3D = setup_mpl() 134 | 135 | # Testarray 136 | N = 50 137 | A = 41 138 | proj = np.zeros((N, N, N)) 139 | proj[(N)/2, (N)/2, :(N)/2] = np.abs(np.linspace(-10, 1, (N)/2)) 140 | 141 | # By default, the rotational axis in _Back_3D_tilted is the y-axis. 142 | # Define a rotational axis with a slight offset in x and in z. 143 | axis = np.array([0.0, 1, .1]) 144 | axis /= np.sqrt(np.sum(axis**2)) 145 | 146 | # Now, obtain the 3D angles that are equally distributed on the unit 147 | # sphere and correspond to the positions of projections that we would 148 | # measure. 149 | angles = np.linspace(0, 2*np.pi, A, endpoint=False) 150 | # The first point in that array will be in the x-z-plane. 151 | points = odtbrain._alg3d_bppt.sphere_points_from_angles_and_tilt( 152 | angles, axis) 153 | 154 | # The following steps are exactly those that are used in 155 | # odtbrain._alg3d_bppt.backpropagate_3d_tilted 156 | # to perform 3D reconstruction with tilted angles. 157 | u, v, w = axis 158 | theta = np.arccos(v) 159 | 160 | # We need three rotations. 161 | 162 | # IMPROTANT: 163 | # We perform the reconstruction such that the rotational axis 164 | # is equal to the y-axis! This is easier than implementing a 165 | # rotation about the rotational axis and tilting with theta 166 | # before and afterwards. 167 | 168 | # This is the rotation that tilts the projection in the 169 | # direction of the rotation axis (new y-axis). 170 | Rtilt = np.array([ 171 | [1, 0, 0], 172 | [0, np.cos(theta), np.sin(theta)], 173 | [0, -np.sin(theta), np.cos(theta)], 174 | ]) 175 | 176 | out = np.zeros((N, N, N)) 177 | 178 | vectors = [] 179 | 180 | for ang, pnt in zip(angles, points): 181 | Rcircle = np.array([ 182 | [np.cos(ang), 0, np.sin(ang)], 183 | [0, 1, 0], 184 | [-np.sin(ang), 0, np.cos(ang)], 185 | ]) 186 | 187 | DR = np.dot(Rtilt, Rcircle) 188 | 189 | # pnt are already rotated by R1 190 | vectors.append(np.dot(Rtilt, pnt)) 191 | 192 | # We need to give this rotation the correct offset 193 | c = 0.5*np.array(proj.shape) 194 | offset = c-c.dot(DR.T) 195 | rotate = scipy.ndimage.interpolation.affine_transform( 196 | proj, DR, offset=offset, 197 | mode="constant", cval=0, order=2) 198 | proj *= 0.98 199 | out += rotate 200 | 201 | # visualize the axes 202 | out[0, 0, 0] = np.max(out) # origin 203 | out[-1, 0, 0] = np.max(out)/2 # x 204 | out[0, -1, 0] = np.max(out)/3 # z 205 | # show arrows pointing at projection directions 206 | # (should form cone aligned with y) 207 | fig = plt.figure(figsize=(10, 10)) 208 | ax = fig.add_subplot(111, projection='3d') 209 | for vec in vectors: 210 | u, v, w = vec 211 | a = Arrow3D([0, u], [0, v], [0, w], 212 | mutation_scale=20, lw=1, arrowstyle="-|>") 213 | ax.add_artist(a) 214 | 215 | radius = 1 216 | ax.set_xlabel('X') 217 | ax.set_ylabel('Y') 218 | ax.set_zlabel('Z') 219 | ax.set_xlim(-radius*1.5, radius*1.5) 220 | ax.set_ylim(-radius*1.5, radius*1.5) 221 | ax.set_zlim(-radius*1.5, radius*1.5) 222 | plt.tight_layout() 223 | plt.show() 224 | -------------------------------------------------------------------------------- /tests/test_save_memory.py: -------------------------------------------------------------------------------- 1 | """Test save memory options""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | 6 | from common_methods import create_test_sino_3d, get_test_parameter_set 7 | 8 | 9 | def test_back3d(): 10 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 11 | parameters = get_test_parameter_set(2) 12 | # complex 13 | r = list() 14 | for p in parameters: 15 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 16 | dtype=float, 17 | save_memory=False, **p) 18 | r.append(f) 19 | # real 20 | r2 = list() 21 | for p in parameters: 22 | f = odtbrain.backpropagate_3d(sino, angles, padval=0, 23 | dtype=float, 24 | save_memory=True, **p) 25 | r2.append(f) 26 | assert np.allclose(np.array(r), np.array(r2)) 27 | 28 | 29 | def test_back3d_tilted(): 30 | sino, angles = create_test_sino_3d(Nx=10, Ny=10) 31 | parameters = get_test_parameter_set(2) 32 | # complex 33 | r = list() 34 | for p in parameters: 35 | f = odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 36 | dtype=float, 37 | save_memory=False, **p) 38 | r.append(f) 39 | # real 40 | r2 = list() 41 | for p in parameters: 42 | f = odtbrain.backpropagate_3d_tilted(sino, angles, padval=0, 43 | dtype=float, 44 | save_memory=True, **p) 45 | r2.append(f) 46 | 47 | assert np.allclose(np.array(r), np.array(r2)) 48 | 49 | 50 | if __name__ == "__main__": 51 | # Run all tests 52 | loc = locals() 53 | for key in list(loc.keys()): 54 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 55 | loc[key]() 56 | -------------------------------------------------------------------------------- /tests/test_spherecoords_from_angles_and_axis.py: -------------------------------------------------------------------------------- 1 | """3D backpropagation with tilted axis of rotation: sphere coordinates""" 2 | import numpy as np 3 | 4 | import odtbrain 5 | import odtbrain._alg3d_bppt 6 | 7 | 8 | def test_simple_sphere(): 9 | """simple geometrical tests""" 10 | angles = np.array([0, np.pi/2, np.pi]) 11 | axes = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1], [1, 0, 1], [1, 1, 1]] 12 | 13 | results = [] 14 | 15 | for tilted_axis in axes: 16 | angle_coords = odtbrain._alg3d_bppt.sphere_points_from_angles_and_tilt( 17 | angles, tilted_axis) 18 | results.append(angle_coords) 19 | 20 | s2 = 1/np.sqrt(2) 21 | correct = np.array([[[1, 0, 0], [1, 0, 0], [1, 0, 0]], 22 | [[0, 0, 1], [1, 0, 0], [0, 0, -1]], 23 | [[0, 0, 1], [0, 0, 1], [0, 0, 1]], 24 | [[0, 0, 1], [s2, .5, .5], [0, 1, 0]], 25 | [[s2, 0, s2], [s2, 0, s2], [s2, 0, s2]], 26 | [[s2, 0, s2], 27 | [0.87965281125489458, s2/3*2, 0.063156230327168605], 28 | [s2/3, s2/3*4, s2/3]], 29 | ]) 30 | assert np.allclose(correct, np.array(results)) 31 | 32 | 33 | if __name__ == "__main__": 34 | # Run all tests 35 | loc = locals() 36 | for key in list(loc.keys()): 37 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 38 | loc[key]() 39 | 40 | import matplotlib.pylab as plt 41 | from mpl_toolkits.mplot3d import Axes3D # noqa: F401 42 | from matplotlib.patches import FancyArrowPatch 43 | from mpl_toolkits.mplot3d import proj3d 44 | 45 | class Arrow3D(FancyArrowPatch): 46 | def __init__(self, xs, ys, zs, *args, **kwargs): 47 | FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs) 48 | self._verts3d = xs, ys, zs 49 | 50 | def draw(self, renderer): 51 | xs3d, ys3d, zs3d = self._verts3d 52 | xs, ys, _zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M) 53 | self.set_positions((xs[0], ys[0]), (xs[1], ys[1])) 54 | FancyArrowPatch.draw(self, renderer) 55 | 56 | axes = [[0, 1, 0], [0, 1, 0.1], [0, 1, -1], [1, 0.1, 0]] 57 | colors = ["k", "blue", "red", "green"] 58 | angles = np.linspace(0, 2*np.pi, 100) 59 | 60 | fig = plt.figure(figsize=(10, 10)) 61 | ax = fig.add_subplot(111, projection='3d') 62 | for i in range(len(axes)): 63 | tilted_axis = axes[i] 64 | color = colors[i] 65 | tilted_axis = np.array(tilted_axis) 66 | tilted_axis = tilted_axis/np.sqrt(np.sum(tilted_axis**2)) 67 | 68 | angle_coords = odtbrain._alg3d_bppt.sphere_points_from_angles_and_tilt( 69 | angles, tilted_axis) 70 | 71 | u, v, w = tilted_axis 72 | a = Arrow3D([0, u], [0, v], [0, w], mutation_scale=20, 73 | lw=1, arrowstyle="-|>", color=color) 74 | ax.add_artist(a) 75 | ax.scatter(angle_coords[:, 0], angle_coords[:, 1], 76 | angle_coords[:, 2], c=color, marker='o') 77 | 78 | radius = 1 79 | ax.set_xlabel('X') 80 | ax.set_ylabel('Y') 81 | ax.set_zlabel('Z') 82 | ax.set_xlim(-radius*1.5, radius*1.5) 83 | ax.set_ylim(-radius*1.5, radius*1.5) 84 | ax.set_zlim(-radius*1.5, radius*1.5) 85 | plt.tight_layout() 86 | 87 | plt.show() 88 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import odtbrain.util as util 4 | 5 | 6 | def test_even(): 7 | angles = np.linspace(0, np.pi, 18, endpoint=False) 8 | res = util.compute_angle_weights_1d(angles) 9 | assert np.allclose(res, 1, rtol=0, atol=1e-14) 10 | 11 | 12 | def test_same_end_value(): 13 | angles = np.linspace(0, np.pi, 18, endpoint=True) 14 | res = util.compute_angle_weights_1d(angles) 15 | assert len(res) == len(angles) 16 | assert np.allclose(np.mean(res), 1, rtol=0, atol=1e-14) 17 | assert np.allclose(res[1:-1], 18 / 17, rtol=0, atol=1e-14) 18 | assert np.allclose(res[0], 18 / 17 / 2, rtol=0, atol=1e-14) 19 | assert np.allclose(res[-1], 18 / 17 / 2, rtol=0, atol=1e-14) 20 | 21 | 22 | def test_multiple_identical_angles(): 23 | angles_0 = np.linspace(0, np.pi, 14, endpoint=False) 24 | angles_1 = np.roll(angles_0, -3) 25 | angles_2 = np.concatenate((np.ones(4)*angles_1[0], angles_1)) 26 | angles = np.roll(angles_2, 3) 27 | res = util.compute_angle_weights_1d(angles) 28 | assert len(res) == len(angles) 29 | assert np.allclose(np.mean(res), 1, rtol=0, atol=1e-14) 30 | assert np.allclose(res[:3], 18 / 14, rtol=0, atol=1e-14) 31 | assert np.allclose(res[3+4+1:], 18 / 14, rtol=0, atol=1e-14) 32 | assert np.allclose(res[3:8], 18 / 14 / 5, rtol=0, atol=1e-14) 33 | -------------------------------------------------------------------------------- /tests/test_weighting.py: -------------------------------------------------------------------------------- 1 | """Test sinogram weighting""" 2 | import numpy as np 3 | import platform 4 | 5 | import odtbrain 6 | 7 | from common_methods import create_test_sino_3d, get_test_parameter_set 8 | 9 | 10 | def test_3d_backprop_weights_even(): 11 | """ 12 | even weights 13 | """ 14 | platform.system = lambda: "Windows" 15 | sino, angles = create_test_sino_3d() 16 | p = get_test_parameter_set(1)[0] 17 | f1 = odtbrain.backpropagate_3d(sino, angles, weight_angles=False, **p) 18 | f2 = odtbrain.backpropagate_3d(sino, angles, weight_angles=True, **p) 19 | data1 = np.array(f1).flatten().view(float) 20 | data2 = np.array(f2).flatten().view(float) 21 | assert np.allclose(data1, data2) 22 | 23 | 24 | def test_3d_backprop_tilted_weights_even(): 25 | """ 26 | even weights 27 | """ 28 | platform.system = lambda: "Windows" 29 | sino, angles = create_test_sino_3d() 30 | p = get_test_parameter_set(1)[0] 31 | f1 = odtbrain.backpropagate_3d_tilted( 32 | sino, angles, weight_angles=False, **p) 33 | f2 = odtbrain.backpropagate_3d_tilted( 34 | sino, angles, weight_angles=True, **p) 35 | data1 = np.array(f1).flatten().view(float) 36 | data2 = np.array(f2).flatten().view(float) 37 | assert np.allclose(data1, data2) 38 | 39 | 40 | if __name__ == "__main__": 41 | # Run all tests 42 | loc = locals() 43 | for key in list(loc.keys()): 44 | if key.startswith("test_") and hasattr(loc[key], "__call__"): 45 | loc[key]() 46 | --------------------------------------------------------------------------------