├── .github └── workflows │ ├── build-python-package.yml │ ├── github-deploy.yml │ └── pypi-publish.yml ├── .gitignore ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── INSTALLATION.rst ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── cgrappa.rst ├── cgsense.rst ├── conf.py ├── gfactor.rst ├── grappa.rst ├── grappaop.rst ├── grog.rst ├── hpgrappa.rst ├── igrappa.rst ├── index.rst ├── installation.rst ├── make.bat ├── mdgrappa.rst ├── nlgrappa_matlab.rst ├── pars.rst ├── pygrappa.rst ├── radialgrappaop.rst ├── references.rst ├── seggrappa.rst ├── sense1d.rst ├── slicegrappa.rst ├── splitslicegrappa.rst ├── tgrappa.rst ├── ttgrappa.rst ├── usage.rst └── windows_installation.rst ├── make_release.sh ├── meson.build ├── patch_mesonpep517.py ├── pygrappa ├── __init__.py ├── benchmarks │ ├── __init__.py │ ├── benchmark.py │ ├── meson.build │ └── run_all_examples.sh ├── cgsense.py ├── coils.py ├── examples │ ├── __init__.py │ ├── bart_kspa.py │ ├── bart_pars.py │ ├── basic_cgrappa.py │ ├── basic_cgsense.py │ ├── basic_gfactor.py │ ├── basic_grappa.py │ ├── basic_grappaop.py │ ├── basic_gridding.py │ ├── basic_hpgrappa.py │ ├── basic_igrappa.py │ ├── basic_mdgrappa.py │ ├── basic_ncgrappa.py │ ├── basic_nlgrappa.py │ ├── basic_nlgrappa_matlab.py │ ├── basic_pars.py │ ├── basic_radialgrappaop.py │ ├── basic_seggrappa.py │ ├── basic_sense1d.py │ ├── basic_slicegrappa.py │ ├── basic_splitslicegrappa.py │ ├── basic_tgrappa.py │ ├── basic_ttgrappa.py │ ├── basic_vcgrappa.py │ ├── inverse_grog.py │ ├── md_cgsense.py │ ├── meson.build │ ├── primefac_grog.py │ ├── primefac_grog_cardiac.py │ ├── tikhonov_regularization.py │ └── use_memmap.py ├── find_acs.py ├── gfactor.py ├── grappa.py ├── grappaop.py ├── grog.py ├── hpgrappa.py ├── igrappa.py ├── kernels.py ├── kspa.py ├── lustig_grappa.py ├── mdgrappa.py ├── meson.build ├── ncgrappa.py ├── nlgrappa.py ├── nlgrappa_matlab.py ├── pars.py ├── pruno.py ├── radialgrappaop.py ├── run_tests.py ├── seggrappa.py ├── sense1d.py ├── simple_pruno.py ├── slicegrappa.py ├── splitslicegrappa.py ├── src │ ├── __init__.py │ ├── _grog_powers_template.cpp │ ├── _grog_powers_template.h │ ├── cgrappa.pyx │ ├── get_sampling_patterns.cpp │ ├── get_sampling_patterns.h │ ├── grog_gridding.pyx │ ├── grog_powers.pyx │ ├── meson.build │ └── train_kernels.pyx ├── tests │ ├── __init__.py │ ├── helpers.py │ ├── meson.build │ ├── test_cgrappa.py │ ├── test_cgsense.py │ ├── test_grappa.py │ ├── test_hpgrappa.py │ ├── test_igrappa.py │ ├── test_mdgrappa.py │ └── test_vcgrappa.py ├── tgrappa.py ├── ttgrappa.py ├── utils │ ├── __init__.py │ ├── disjoint_csm.py │ ├── gaussian_csm.py │ ├── gridder.py │ └── meson.build └── vcgrappa.py └── pyproject.toml /.github/workflows/build-python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | python-version: ['3.9', '3.10', '3.11', '3.12'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Lint with flake8 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 . --count --exit-zero --max-line-length=127 --statistics 35 | - name: Create local wheel 36 | if: runner.os != 'Windows' 37 | run: | 38 | python -m pip install build 39 | python -m build --wheel . 40 | python -m pip install dist/*.whl 41 | - name: Enable Developer Command Prompt 42 | if: runner.os == 'Windows' 43 | uses: ilammy/msvc-dev-cmd@v1.12.0 44 | - name: Create local wheel (Windows) 45 | if: runner.os == 'Windows' 46 | run: | 47 | # install all build dependencies 48 | python -m pip install build 49 | python -m build --wheel . 50 | python -m pip install (get-item dist\*.whl).FullName 51 | - name: Test with pytest 52 | run: | 53 | python -m pip install pytest numpy scipy scikit-image tqdm phantominator 54 | mkdir tmp && cd tmp 55 | python -m pygrappa.run_tests 56 | -------------------------------------------------------------------------------- /.github/workflows/github-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload to PyPI 2 | 3 | # Build on every branch push, tag push, and pull request change: 4 | # on: [push, pull_request] 5 | on: workflow_dispatch 6 | # Alternatively, to publish when a (published) GitHub Release is created, use the following: 7 | # on: 8 | # push: 9 | # pull_request: 10 | # release: 11 | # types: 12 | # - published 13 | 14 | env: 15 | CIBW_SKIP: cp27-* pp27-* pp37-* pp38-* pp39-* pp310-* cp35-* cp36-* cp37-* cp38-* cp*-win32 cp*-manylinux_i686 cp*-musl* 16 | 17 | jobs: 18 | build_wheels: 19 | name: Build wheels on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macos-latest] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: actions/setup-python@v5 29 | name: Install Python 30 | with: 31 | python-version: '3.12' 32 | 33 | - name: Install cibuildwheel 34 | run: | 35 | python -m pip install --upgrade pip 36 | python -m pip install cibuildwheel 37 | 38 | - name: Enable Developer Command Prompt 39 | if: runner.os == 'Windows' 40 | uses: ilammy/msvc-dev-cmd@v1 41 | 42 | - name: Build wheels 43 | run: | 44 | python -m cibuildwheel --output-dir wheelhouse 45 | 46 | - uses: actions/upload-artifact@v3 47 | with: 48 | path: ./wheelhouse/*.whl 49 | 50 | build_sdist: 51 | name: Build source distribution 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | 56 | - uses: actions/setup-python@v5 57 | name: Install Python 58 | with: 59 | python-version: '3.12' 60 | 61 | - name: Install dependencies 62 | run: | 63 | python -m pip install --upgrade pip 64 | python -m pip install build 65 | python -m build 66 | 67 | - name: Build sdist 68 | run: python -m build . 69 | 70 | - uses: actions/upload-artifact@v3 71 | with: 72 | path: dist/*.tar.gz 73 | 74 | upload_pypi: 75 | needs: [build_wheels, build_sdist] 76 | runs-on: ubuntu-latest 77 | environment: release 78 | permissions: 79 | id-token: write 80 | if: github.event_name == 'workflow_dispatch' 81 | steps: 82 | - uses: actions/download-artifact@v3 83 | with: 84 | name: artifact 85 | path: dist 86 | 87 | - uses: pypa/gh-action-pypi-publish@release/v1 88 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install build 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python -m build . 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | build/ 4 | pygrappa.egg-info/ 5 | bin/ 6 | lib/ 7 | lib64 8 | pyvenv.cfg 9 | share/ 10 | *.so 11 | *.html 12 | .eggs/ 13 | .idea/ 14 | 15 | # tmp files 16 | *~ 17 | 18 | # docs 19 | docs/_build 20 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=C0103,W0621,R0205,R0914,R0902,R0915,R0912,R0201,W0511,R0903,W0108,C0302,R0913 3 | max-line-length=70 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nicholas.bgp at gmail dot com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /INSTALLATION.rst: -------------------------------------------------------------------------------- 1 | WARNING: this may contain outdated information. 2 | 3 | Windows 10 Installation 4 | ======================= 5 | 6 | If you are using Windows, then, first of all: sorry. This is not 7 | ideal, but I understand that it might not be your fault. I will 8 | assume you are trying to get pygrappa installed on Windows 10. I will 9 | further assume that you are using Python 3.7 64-bit build. We will 10 | need a C++ compiler to install pygrappa, so officially you should 11 | have "Microsoft Visual C++ Build Tools" installed. I haven't tried 12 | this, but it should work with VS build tools installed. 13 | 14 | However, if you are not able to install the build tools, we can do it 15 | using the MinGW compiler instead. It'll be a little more involved 16 | than a simple `pip install`, but that's what you get for choosing 17 | Windows. 18 | 19 | Steps: 20 | 21 | - | Download 64-bit fork of MinGW from 22 | | https://sourceforge.net/projects/mingw-w64/ 23 | - | Follow this guide: 24 | | https://github.com/orlp/dev-on-windows/wiki/Installing-GCC--&-MSYS2 25 | - Now you should be able to use gcc/g++/etc. from CMD-line 26 | - | Modify cygwinccompiler.py similar to 27 | | https://github.com/tgalal/yowsup/issues/2494#issuecomment-388439162 28 | | but using the version number `1916`: 29 | 30 | .. code-block:: python 31 | 32 | def get_msvcr(): 33 | """Include the appropriate MSVC runtime library if Python 34 | was built with MSVC 7.0 or later. 35 | """ 36 | msc_pos = sys.version.find('MSC v.') 37 | if msc_pos != -1: 38 | msc_ver = sys.version[msc_pos+6:msc_pos+10] 39 | if msc_ver == '1300': 40 | # MSVC 7.0 41 | return ['msvcr70'] 42 | elif msc_ver == '1310': 43 | # MSVC 7.1 44 | return ['msvcr71'] 45 | elif msc_ver == '1400': 46 | # VS2005 / MSVC 8.0 47 | return ['msvcr80'] 48 | elif msc_ver == '1500': 49 | # VS2008 / MSVC 9.0 50 | return ['msvcr90'] 51 | elif msc_ver == '1600': 52 | # VS2010 / MSVC 10.0 53 | return ['msvcr100'] 54 | elif msc_ver == '1916': # <- ADD THIS CONDITION 55 | # Visual Studio 2015 / Visual C++ 14.0 56 | return ['vcruntime140'] 57 | else: 58 | raise ValueError( 59 | "Unknown MS Compiler version %s " % msc_ver) 60 | 61 | - now run the command: 62 | 63 | .. code-block:: bash 64 | 65 | pip install --global-option build_ext --global-option \ 66 | --compiler=mingw32 --global-option -DMS_WIN64 pygrappa 67 | 68 | Hopefully this works for you. Refer to 69 | https://github.com/mckib2/pygrappa/issues/17 for a more detailed 70 | discussion. 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Nicholas McKibben 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Online documentation `here `_ 5 | 6 | GRAPPA is a popular parallel imaging reconstruction algorithm. 7 | Unfortunately there aren't a lot of easy to use Python 8 | implementations of it or its many variants available, so I decided to 9 | release this simple package. 10 | 11 | There are also a couple reference SENSE-like implementations that 12 | have made their way into the package. This is to be expected -- a 13 | lot of later parallel imaging algorithms have hints of both GRAPPA- 14 | and SENSE-like inspirations. 15 | 16 | Installation should be quick: 17 | 18 | .. code-block:: 19 | 20 | pip install pygrappa 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/cgrappa.rst: -------------------------------------------------------------------------------- 1 | .. cgrappa: 2 | 3 | pygrappa.cgrappa 4 | ================ 5 | 6 | .. automodule:: pygrappa.cgrappa 7 | :members: 8 | 9 | `cgrappa` is Cython implementation of GRAPPA. It is faster than its Python counterparts, but is known to have bugs. It is probably due for a rewrite in the style of `mdgrappa`. 10 | -------------------------------------------------------------------------------- /docs/cgsense.rst: -------------------------------------------------------------------------------- 1 | .. cgsense: 2 | 3 | pygrappa.cgsense 4 | ================ 5 | 6 | .. automodule:: pygrappa.cgsense 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../')) 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'pygrappa' 20 | copyright = '2020, Nicholas McKibben' 21 | author = 'Nicholas McKibben' 22 | 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Make sure ReadTheDocs doesn't choke on C extensions 27 | autodoc_mock_imports = [ 28 | 'pygrappa.cgrappa', 29 | 'pygrappa.grog_powers', 30 | 'pygrappa.grog_gridding', 31 | ] 32 | 33 | # Point to index instead of contents.rst 34 | master_doc = 'index' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.coverage', 42 | 'sphinx.ext.napoleon', 43 | 'sphinx.ext.intersphinx', 44 | 'sphinx.ext.viewcode', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'references.rst'] 54 | 55 | 56 | # -- Options for HTML output ------------------------------------------------- 57 | 58 | # The theme to use for HTML and HTML Help pages. See the documentation for 59 | # a list of builtin themes. 60 | # 61 | html_theme = 'sphinx_rtd_theme' 62 | 63 | # Add any paths that contain custom static files (such as style sheets) here, 64 | # relative to this directory. They are copied after the builtin static files, 65 | # so a file named "default.css" will overwrite the builtin "default.css". 66 | html_static_path = ['_static'] 67 | -------------------------------------------------------------------------------- /docs/gfactor.rst: -------------------------------------------------------------------------------- 1 | .. gfactor: 2 | 3 | pygrappa.gfactor 4 | ================ 5 | 6 | .. automodule:: pygrappa.gfactor 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/grappa.rst: -------------------------------------------------------------------------------- 1 | .. grappa: 2 | 3 | pygrappa.grappa 4 | =============== 5 | 6 | .. automodule:: pygrappa.grappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/grappaop.rst: -------------------------------------------------------------------------------- 1 | .. grappaop: 2 | 3 | pygrappa.grappaop 4 | ================= 5 | 6 | .. automodule:: pygrappa.grappaop 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/grog.rst: -------------------------------------------------------------------------------- 1 | .. grog: 2 | 3 | pygrappa.grog 4 | ============= 5 | 6 | .. automodule:: pygrappa.grog 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/hpgrappa.rst: -------------------------------------------------------------------------------- 1 | .. hpgrappa: 2 | 3 | pygrappa.hpgrappa 4 | ================= 5 | 6 | .. automodule:: pygrappa.hpgrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/igrappa.rst: -------------------------------------------------------------------------------- 1 | .. igrappa: 2 | 3 | pygrappa.igrappa 4 | ================ 5 | 6 | .. automodule:: pygrappa.igrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. index: 2 | 3 | ======== 4 | pygrappa 5 | ======== 6 | 7 | About 8 | ===== 9 | 10 | GRAPPA is a popular parallel imaging reconstruction algorithm. 11 | Unfortunately there aren't a lot of easy to use Python 12 | implementations of it or its many variants available, so I decided to 13 | release this simple package. 14 | 15 | There are also a couple reference SENSE-like implementations that 16 | have made their way into the package. This is to be expected -- a 17 | lot of later parallel imaging algorithms have hints of both GRAPPA- 18 | and SENSE-like inspirations. 19 | 20 | Installation 21 | ------------ 22 | 23 | .. toctree:: 24 | :hidden: 25 | :maxdepth: 1 26 | 27 | installation 28 | 29 | .. code-block:: python 30 | 31 | pip install pygrappa 32 | 33 | There are C/C++ extensions to be compiled, so you will need a compiler 34 | that supports either the C++11 or C++14 standard. 35 | See :doc:`installation` for more instructions. 36 | 37 | API Reference 38 | ------------- 39 | 40 | .. toctree:: 41 | :hidden: 42 | :maxdepth: 1 43 | 44 | pygrappa 45 | 46 | The exact API of all functions and classes, as given by the docstrings. The API 47 | documents expected types and allowed features for all functions, and all 48 | parameters available for the algorithms. 49 | 50 | A full catalog can be found in the :doc:`pygrappa` page. 51 | 52 | Usage 53 | ===== 54 | 55 | .. toctree:: 56 | :hidden: 57 | 58 | usage 59 | 60 | See the :doc:`usage` page. Also see the `examples` module. 61 | It has several scripts showing basic usage. Docstrings are also a 62 | great resource -- check them out for all possible arguments and 63 | usage info. 64 | 65 | You can run examples from the command line by calling them like this: 66 | 67 | .. code-block:: bash 68 | 69 | python -m pygrappa.examples.[example-name] 70 | 71 | # For example, if I wanted to try out TGRAPPA: 72 | python -m pygrappa.examples.basic_tgrappa 73 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. installation: 2 | 3 | Installation 4 | ============ 5 | 6 | .. toctree:: 7 | :hidden: 8 | :maxdepth: 1 9 | 10 | windows_installation 11 | 12 | This package is developed in Ubuntu 18.04 using Python 3.6.8. That's 13 | not to say it won't work on other things. You should submit an issue 14 | when it doesn't work like it says it should. The whole idea was to 15 | have an easy to use, pip-install-able GRAPPA module, so let's try to 16 | do that. 17 | 18 | In general, it's a good idea to work inside virtual environments. I 19 | create and activate mine like this: 20 | 21 | .. code-block:: bash 22 | 23 | python3 -m venv /venvs/pygrappa 24 | source /venvs/pygrappa/bin/activate 25 | 26 | More information can be found in the `venv documentation `_. 27 | 28 | Installation under a Unix-based platform should then be as easy as: 29 | 30 | .. code-block:: bash 31 | 32 | pip install pygrappa 33 | 34 | You will need a C/C++ compiler that supports the C++14 standard. 35 | See :doc:`windows_installation` for more info on installing under Windows. 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/mdgrappa.rst: -------------------------------------------------------------------------------- 1 | .. mdgrappa: 2 | 3 | pygrappa.mdgrappa 4 | ================= 5 | 6 | .. automodule:: pygrappa.mdgrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/nlgrappa_matlab.rst: -------------------------------------------------------------------------------- 1 | .. nlgrappa_matlab: 2 | 3 | pygrappa.nlgrappa_matlab 4 | ======================== 5 | 6 | .. automodule:: pygrappa.nlgrappa_matlab 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/pars.rst: -------------------------------------------------------------------------------- 1 | .. pars: 2 | 3 | pygrappa.pars 4 | ============= 5 | 6 | .. automodule:: pygrappa.pars 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/pygrappa.rst: -------------------------------------------------------------------------------- 1 | .. pygrappa: 2 | 3 | .. note:: 4 | 5 | The upcoming `1.0.0` release will make changes to the API and simplify 6 | the interface considerably. The plans are to collect all GRAPPA-like 7 | methods and SENSE-like methods in their own interfaces: 8 | 9 | .. code-block:: python 10 | 11 | pygrappa.grappa( 12 | kspace, calib=None, kernel_size=None, 13 | method='grappa', coil_axis=-1, options=None) 14 | pygrappa.sense(kspace, sens, coil_axis=-1, options) 15 | 16 | The `method` parameter will allow the `grappa` interface to call the 17 | existing methods such as `tgrappa`, `mdgrappa`, etc. under the hood. 18 | The dictionary `options` can be used to pass in method-specific 19 | parameters. The SENSE interface will behave similarly. 20 | 21 | The gridding interface is still an open question. 22 | 23 | Progress on the `1.0.0` release can be found 24 | `here `_ 25 | 26 | 27 | API Reference 28 | ============= 29 | 30 | .. toctree:: 31 | :maxdepth: 1 32 | 33 | grappa 34 | cgrappa 35 | mdgrappa 36 | igrappa 37 | hpgrappa 38 | seggrappa 39 | tgrappa 40 | slicegrappa 41 | splitslicegrappa 42 | grappaop 43 | radialgrappaop 44 | ttgrappa 45 | pars 46 | grog 47 | nlgrappa_matlab 48 | gfactor 49 | sense1d 50 | cgsense 51 | -------------------------------------------------------------------------------- /docs/radialgrappaop.rst: -------------------------------------------------------------------------------- 1 | .. radialgrappaop: 2 | 3 | pygrappa.radialgrappaop 4 | ======================= 5 | 6 | .. automodule:: pygrappa.radialgrappaop 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/references.rst: -------------------------------------------------------------------------------- 1 | 2 | .. references: 3 | 4 | .. [1] Griswold, Mark A., et al. "Generalized autocalibrating 5 | partially parallel acquisitions (GRAPPA)." Magnetic 6 | Resonance in Medicine: An Official Journal of the 7 | International Society for Magnetic Resonance in Medicine 8 | 47.6 (2002): 1202-1210. 9 | .. [2] Blaimer, Martin, et al. "Virtual coil concept for improved 10 | parallel MRI employing conjugate symmetric signals." 11 | Magnetic Resonance in Medicine: An Official Journal of the 12 | International Society for Magnetic Resonance in Medicine 13 | 61.1 (2009): 93-102. 14 | .. [3] Zhao, Tiejun, and Xiaoping Hu. "Iterative GRAPPA (iGRAPPA) 15 | for improved parallel imaging reconstruction." Magnetic 16 | Resonance in Medicine: An Official Journal of the 17 | International Society for Magnetic Resonance in Medicine 18 | 59.4 (2008): 903-907. 19 | .. [4] Huang, Feng, et al. "High‐pass GRAPPA: An image support 20 | reduction technique for improved partially parallel 21 | imaging." Magnetic Resonance in Medicine: An Official 22 | Journal of the International Society for Magnetic 23 | Resonance in Medicine 59.3 (2008): 642-649. 24 | .. [5] Park, Jaeseok, et al. "Artifact and noise suppression in 25 | GRAPPA imaging using improved k‐space coil calibration and 26 | variable density sampling." Magnetic Resonance in 27 | Medicine: An Official Journal of the International Society 28 | for Magnetic Resonance in Medicine 53.1 (2005): 186-193. 29 | .. [6] Breuer, Felix A., et al. "Dynamic autocalibrated parallel 30 | imaging using temporal GRAPPA (TGRAPPA)." Magnetic 31 | Resonance in Medicine: An Official Journal of the 32 | International Society for Magnetic Resonance in Medicine 33 | 53.4 (2005): 981-985. 34 | .. [7] Setsompop, Kawin, et al. "Blipped‐controlled aliasing in 35 | parallel imaging for simultaneous multislice echo planar 36 | imaging with reduced g‐factor penalty." Magnetic resonance 37 | in medicine 67.5 (2012): 1210-1224. 38 | .. [8] Cauley, Stephen F., et al. "Interslice leakage artifact 39 | reduction technique for simultaneous multislice 40 | acquisitions." Magnetic resonance in medicine 72.1 (2014): 41 | 93-102. 42 | .. [9] Griswold, Mark A., et al. "Parallel magnetic resonance 43 | imaging using the GRAPPA operator formalism." Magnetic 44 | resonance in medicine 54.6 (2005): 1553-1556. 45 | .. [10] Blaimer, Martin, et al. "2D‐GRAPPA‐operator for faster 3D 46 | parallel MRI." Magnetic Resonance in Medicine: An Official 47 | Journal of the International Society for Magnetic Resonance 48 | in Medicine 56.6 (2006): 1359-1364. 49 | .. [11] Seiberlich, Nicole, et al. "Improved radial GRAPPA 50 | calibration for real‐time free‐breathing cardiac imaging." 51 | Magnetic resonance in medicine 65.2 (2011): 492-505. 52 | .. [12] Yeh, Ernest N., et al. "3Parallel magnetic resonance 53 | imaging with adaptive radius in k‐space (PARS): 54 | Constrained image reconstruction using k‐space locality in 55 | radiofrequency coil encoded data." Magnetic Resonance in 56 | Medicine: An Official Journal of the International Society 57 | for Magnetic Resonance in Medicine 53.6 (2005): 1383-1392. 58 | .. [13] Seiberlich, Nicole, et al. "Self‐calibrating GRAPPA 59 | operator gridding for radial and spiral trajectories." 60 | Magnetic Resonance in Medicine: An Official Journal of the 61 | International Society for Magnetic Resonance in Medicine 62 | 59.4 (2008): 930-935. 63 | .. [14] Seiberlich, Nicole, et al. "Self‐calibrating GRAPPA 64 | operator gridding for radial and spiral trajectories." 65 | Magnetic Resonance in Medicine: An Official Journal of the 66 | International Society for Magnetic Resonance in Medicine 67 | 59.4 (2008): 930-935. 68 | .. [15] Chang, Yuchou, Dong Liang, and Leslie Ying. "Nonlinear 69 | GRAPPA: A kernel approach to parallel MRI reconstruction." 70 | Magnetic resonance in medicine 68.3 (2012): 730-740. 71 | .. [16] Pruessmann, Klaas P., et al. "SENSE: sensitivity encoding 72 | for fast MRI." Magnetic Resonance in Medicine: An Official 73 | Journal of the International Society for Magnetic 74 | Resonance in Medicine 42.5 (1999): 952-962. 75 | .. [17] Pruessmann, Klaas P., et al. "Advances in sensitivity 76 | encoding with arbitrary k‐space trajectories." Magnetic 77 | Resonance in Medicine: An Official Journal of the 78 | International Society for Magnetic Resonance in Medicine 79 | 46.4 (2001): 638-651. 80 | -------------------------------------------------------------------------------- /docs/seggrappa.rst: -------------------------------------------------------------------------------- 1 | .. seggrappa: 2 | 3 | pygrappa.seggrappa 4 | ================== 5 | 6 | .. automodule:: pygrappa.seggrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/sense1d.rst: -------------------------------------------------------------------------------- 1 | .. sense1d: 2 | 3 | pygrappa.sense1d 4 | ================ 5 | 6 | .. automodule:: pygrappa.sense1d 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/slicegrappa.rst: -------------------------------------------------------------------------------- 1 | .. slicegrappa: 2 | 3 | pygrappa.slicegrappa 4 | ==================== 5 | 6 | .. automodule:: pygrappa.slicegrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/splitslicegrappa.rst: -------------------------------------------------------------------------------- 1 | .. splitslicegrappa: 2 | 3 | pygrappa.splitslicegrappa 4 | ========================= 5 | 6 | .. automodule:: pygrappa.splitslicegrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/tgrappa.rst: -------------------------------------------------------------------------------- 1 | .. tgrappa: 2 | 3 | pygrappa.tgrappa 4 | ================ 5 | 6 | .. automodule:: pygrappa.tgrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/ttgrappa.rst: -------------------------------------------------------------------------------- 1 | .. ttgrappa: 2 | 3 | pygrappa.ttgrappa 4 | ================= 5 | 6 | .. automodule:: pygrappa.ttgrappa 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/windows_installation.rst: -------------------------------------------------------------------------------- 1 | .. windows_installation: 2 | 3 | Windows 10 Installation 4 | ======================= 5 | 6 | If you are using Windows, then, first of all: sorry. This is not 7 | ideal, but I understand that it might not be your fault. I will 8 | assume you are trying to get pygrappa installed on Windows 10. I will 9 | further assume that you are using Python 3.7 64-bit build. We will 10 | need a C++ compiler to install pygrappa, so officially you should 11 | have "Microsoft Visual C++ Build Tools" installed. I haven't tried 12 | this, but it should work with VS build tools installed. 13 | 14 | However, if you are not able to install the build tools, we can do it 15 | using the MinGW compiler instead. It'll be a little more involved 16 | than a simple `pip install`, but that's what you get for choosing 17 | Windows. 18 | 19 | Steps: 20 | 21 | - | Download 64-bit fork of MinGW from 22 | | https://sourceforge.net/projects/mingw-w64/ 23 | - | Follow this guide: 24 | | https://github.com/orlp/dev-on-windows/wiki/Installing-GCC--&-MSYS2 25 | - Now you should be able to use gcc/g++/etc. from CMD-line 26 | - | Modify cygwinccompiler.py similar to 27 | | https://github.com/tgalal/yowsup/issues/2494#issuecomment-388439162 28 | | but using the version number `1916`: 29 | 30 | .. code-block:: python 31 | 32 | def get_msvcr(): 33 | """Include the appropriate MSVC runtime library if Python 34 | was built with MSVC 7.0 or later. 35 | """ 36 | msc_pos = sys.version.find('MSC v.') 37 | if msc_pos != -1: 38 | msc_ver = sys.version[msc_pos+6:msc_pos+10] 39 | if msc_ver == '1300': 40 | # MSVC 7.0 41 | return ['msvcr70'] 42 | elif msc_ver == '1310': 43 | # MSVC 7.1 44 | return ['msvcr71'] 45 | elif msc_ver == '1400': 46 | # VS2005 / MSVC 8.0 47 | return ['msvcr80'] 48 | elif msc_ver == '1500': 49 | # VS2008 / MSVC 9.0 50 | return ['msvcr90'] 51 | elif msc_ver == '1600': 52 | # VS2010 / MSVC 10.0 53 | return ['msvcr100'] 54 | elif msc_ver == '1916': # <- ADD THIS CONDITION 55 | # Visual Studio 2015 / Visual C++ 14.0 56 | return ['vcruntime140'] 57 | else: 58 | raise ValueError( 59 | "Unknown MS Compiler version %s " % msc_ver) 60 | 61 | - now run the command: 62 | 63 | .. code-block:: bash 64 | 65 | pip install --global-option build_ext --global-option \ 66 | --compiler=mingw32 --global-option -DMS_WIN64 pygrappa 67 | 68 | Hopefully this works for you. Refer to 69 | https://github.com/mckib2/pygrappa/issues/17 for a more detailed 70 | discussion. 71 | -------------------------------------------------------------------------------- /make_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## TO RUN: 4 | ## source path/to/venv/bin/activate 5 | ## source make_release.sh 6 | 7 | # Remove any existing distribution archives 8 | rm -rf dist 9 | mkdir dist 10 | 11 | # Make sure we have the latest Cython 12 | python -m pip install --upgrade Cython 13 | 14 | # Generate distribution archives 15 | python -m pip install --upgrade setuptools wheel 16 | python setup.py sdist # bdist_wheel 17 | 18 | # Upload 19 | python -m pip install --upgrade twine 20 | python -m twine upload dist/* 21 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'pygrappa', 3 | 'c', 'cpp', 'cython', 4 | version: '0.26.3', 5 | license: 'MIT', 6 | meson_version: '>= 1.5.0', 7 | default_options: [ 8 | 'buildtype=release', 9 | 'b_ndebug=if-release', 10 | 'c_std=c99', 11 | 'cpp_std=c++14', 12 | ], 13 | ) 14 | 15 | cc = meson.get_compiler('c') 16 | cpp = meson.get_compiler('cpp') 17 | cy = meson.get_compiler('cython') 18 | cython = find_program(cy.cmd_array()[0]) 19 | if not cy.version().version_compare('>=3.0.8') 20 | error('SciPy requires Cython >= 3.0.8') 21 | endif 22 | 23 | is_windows = host_machine.system() == 'windows' 24 | 25 | # https://mesonbuild.com/Python-module.html 26 | py3 = import('python').find_installation(pure: false) 27 | py3_dep = py3.dependency() 28 | 29 | # NumPy include directory - needed in all submodules 30 | incdir_numpy = run_command(py3, 31 | [ 32 | '-c', 33 | '''import os 34 | #os.chdir(os.path.join("..", "tools")) 35 | import numpy as np 36 | try: 37 | incdir = os.path.relpath(np.get_include()) 38 | except Exception: 39 | incdir = np.get_include() 40 | print(incdir) 41 | ''' 42 | ], 43 | check: true 44 | ).stdout().strip() 45 | message(incdir_numpy) 46 | 47 | inc_np = include_directories(incdir_numpy) 48 | numpy_nodepr_api = '-DNPY_NO_DEPRECATED_API=NPY_1_9_API_VERSION' 49 | 50 | cython_args = ['-3', '--fast-fail', '--output-file', '@OUTPUT@', '--include-dir', '@BUILD_ROOT@', '@INPUT@'] 51 | if cy.version().version_compare('>=3.1.0') 52 | cython_args += ['-Xfreethreading_compatible=True'] 53 | endif 54 | cython_cplus_args = ['--cplus'] + cython_args 55 | 56 | cython_gen_cpp = generator(cython, 57 | arguments : cython_cplus_args, 58 | output : '@BASENAME@.cpp', 59 | depends : []) 60 | 61 | # C warning flags 62 | Wno_maybe_uninitialized = cc.get_supported_arguments('-Wno-maybe-uninitialized') 63 | Wno_discarded_qualifiers = cc.get_supported_arguments('-Wno-discarded-qualifiers') 64 | Wno_empty_body = cc.get_supported_arguments('-Wno-empty-body') 65 | Wno_implicit_function_declaration = cc.get_supported_arguments('-Wno-implicit-function-declaration') 66 | Wno_parentheses = cc.get_supported_arguments('-Wno-parentheses') 67 | Wno_switch = cc.get_supported_arguments('-Wno-switch') 68 | Wno_unused_label = cc.get_supported_arguments('-Wno-unused-label') 69 | Wno_unused_variable = cc.get_supported_arguments('-Wno-unused-variable') 70 | 71 | # C++ warning flags 72 | _cpp_Wno_cpp = cpp.get_supported_arguments('-Wno-cpp') 73 | _cpp_Wno_deprecated_declarations = cpp.get_supported_arguments('-Wno-deprecated-declarations') 74 | _cpp_Wno_class_memaccess = cpp.get_supported_arguments('-Wno-class-memaccess') 75 | _cpp_Wno_format_truncation = cpp.get_supported_arguments('-Wno-format-truncation') 76 | _cpp_Wno_non_virtual_dtor = cpp.get_supported_arguments('-Wno-non-virtual-dtor') 77 | _cpp_Wno_sign_compare = cpp.get_supported_arguments('-Wno-sign-compare') 78 | _cpp_Wno_switch = cpp.get_supported_arguments('-Wno-switch') 79 | _cpp_Wno_terminate = cpp.get_supported_arguments('-Wno-terminate') 80 | _cpp_Wno_unused_but_set_variable = cpp.get_supported_arguments('-Wno-unused-but-set-variable') 81 | _cpp_Wno_unused_function = cpp.get_supported_arguments('-Wno-unused-function') 82 | _cpp_Wno_unused_local_typedefs = cpp.get_supported_arguments('-Wno-unused-local-typedefs') 83 | _cpp_Wno_unused_variable = cpp.get_supported_arguments('-Wno-unused-variable') 84 | _cpp_Wno_int_in_bool_context = cpp.get_supported_arguments('-Wno-int-in-bool-context') 85 | 86 | # Deal with M_PI & friends; add `use_math_defines` to c_args or cpp_args 87 | # Cython doesn't always get this right itself (see, e.g., gh-16800), so 88 | # explicitly add the define as a compiler flag for Cython-generated code. 89 | if is_windows 90 | use_math_defines = ['-D_USE_MATH_DEFINES'] 91 | else 92 | use_math_defines = [] 93 | endif 94 | 95 | # Suppress warning for deprecated Numpy API. 96 | # (Suppress warning messages emitted by #warning directives). 97 | # Replace with numpy_nodepr_api after Cython 3.0 is out 98 | cython_c_args = [_cpp_Wno_cpp, use_math_defines] 99 | cython_cpp_args = cython_c_args 100 | 101 | subdir('pygrappa') 102 | -------------------------------------------------------------------------------- /patch_mesonpep517.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import mesonpep517 3 | 4 | 5 | if __name__ == "__main__": 6 | buildapi = pathlib.Path(mesonpep517.__file__).parent / "buildapi.py" 7 | with open(buildapi, "r") as fp: 8 | contents = fp.read() 9 | contents = contents.replace(".decode('utf-8').strip('\\n')", ".decode('utf-8').strip('\\n\\r')") 10 | contents = contents.replace("abi = get_abi(python)", "abi = get_abi(sys.executable)") 11 | with open(buildapi, "w") as fp: 12 | fp.write(contents) 13 | print("Patched buildapi.py for Windows!") 14 | 15 | pyproject = pathlib.Path(__file__).parent / "pyproject.toml" 16 | with open(pyproject, "r") as fp: 17 | contents = fp.read() 18 | contents += "\n\n[tools.pip]\ndisable-isolated-build = true\n" 19 | with open(pyproject, "w") as fp: 20 | fp.write(contents) 21 | -------------------------------------------------------------------------------- /pygrappa/__init__.py: -------------------------------------------------------------------------------- 1 | """Bring functions up to the correct level.""" 2 | 3 | # GRAPPA 4 | from .mdgrappa import mdgrappa # NOQA 5 | from .cgrappa import cgrappa # pylint: disable=E0611 # NOQA 6 | from .lustig_grappa import lustig_grappa # NOQA 7 | from .grappa import grappa # NOQA 8 | from .tgrappa import tgrappa # NOQA 9 | from .slicegrappa import slicegrappa # NOQA 10 | from .splitslicegrappa import splitslicegrappa # NOQA 11 | from .vcgrappa import vcgrappa # NOQA 12 | from .igrappa import igrappa # NOQA 13 | from .hpgrappa import hpgrappa # NOQA 14 | from .seggrappa import seggrappa # NOQA 15 | from .grappaop import grappaop # NOQA 16 | from .ncgrappa import ncgrappa # NOQA 17 | from .ttgrappa import ttgrappa # NOQA 18 | from .pars import pars # NOQA 19 | from .radialgrappaop import radialgrappaop # NOQA 20 | from .grog import grog # NOQA 21 | # from .kspa import kspa # NOQA 22 | from .nlgrappa import nlgrappa # NOQA 23 | from .nlgrappa_matlab import nlgrappa_matlab # NOQA 24 | from .gfactor import gfactor, gfactor_single_coil_R2 # NOQA 25 | from .sense1d import sense1d # NOQA 26 | from .cgsense import cgsense # NOQA 27 | 28 | from .find_acs import find_acs # NOQA 29 | -------------------------------------------------------------------------------- /pygrappa/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckib2/pygrappa/539beda1e8881ff36797cb6612c2332e25463e17/pygrappa/benchmarks/__init__.py -------------------------------------------------------------------------------- /pygrappa/benchmarks/benchmark.py: -------------------------------------------------------------------------------- 1 | '''Compare performance of grappa with and without C implementation.''' 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | from phantominator import shepp_logan 7 | 8 | from pygrappa import cgrappa 9 | from pygrappa import grappa 10 | from pygrappa.utils import gaussian_csm 11 | 12 | if __name__ == '__main__': 13 | 14 | # Generate fake sensitivity maps: mps 15 | N = 512 16 | ncoils = 32 17 | mps = gaussian_csm(N, N, ncoils) 18 | 19 | # generate 4 coil phantom 20 | ph = shepp_logan(N) 21 | imspace = ph[..., None]*mps 22 | imspace = imspace.astype('complex') 23 | ax = (0, 1) 24 | kspace = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 25 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 26 | 27 | # crop 20x20 window from the center of k-space for calibration 28 | pd = 10 29 | ctr = int(N/2) 30 | calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() 31 | 32 | # calibrate a kernel 33 | kernel_size = (5, 5) 34 | 35 | # undersample by a factor of 2 in both x and y 36 | kspace[::2, 1::2, :] = 0 37 | kspace[1::2, ::2, :] = 0 38 | 39 | # Time both implementations 40 | t0 = time() 41 | recon0 = grappa(kspace, calib, (5, 5)) 42 | print(' GRAPPA: %g' % (time() - t0)) 43 | 44 | t0 = time() 45 | recon1 = cgrappa(kspace, calib, (5, 5)) 46 | print('CGRAPPA: %g' % (time() - t0)) 47 | 48 | assert np.allclose(recon0, recon1) 49 | -------------------------------------------------------------------------------- /pygrappa/benchmarks/meson.build: -------------------------------------------------------------------------------- 1 | py3.install_sources([ 2 | '__init__.py', 3 | 'benchmark.py' 4 | ], 5 | subdir: 'pygrappa/benchmarks' 6 | ) 7 | -------------------------------------------------------------------------------- /pygrappa/benchmarks/run_all_examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for f in pygrappa/examples/*.py; do 4 | echo "$f" 5 | python -m pygrappa.examples.$(basename "$f" .py) || break 6 | done 7 | -------------------------------------------------------------------------------- /pygrappa/cgsense.py: -------------------------------------------------------------------------------- 1 | """Python implementation of iterative and CG-SENSE.""" 2 | 3 | from time import time 4 | import logging 5 | 6 | import numpy as np 7 | from scipy.sparse.linalg import LinearOperator, cg 8 | 9 | 10 | def _fft(x0, axes=None): 11 | """Utility Forward FFT function. 12 | """ 13 | if axes is None: 14 | axes = np.arange(x0.ndim-1) 15 | return np.fft.fftshift(np.fft.fftn(np.fft.ifftshift( 16 | x0, axes=axes), axes=axes), axes=axes) 17 | 18 | 19 | def _ifft(x0, axes=None): 20 | """Utility Inverse FFT function. 21 | """ 22 | if axes is None: 23 | axes = np.arange(x0.ndim-1) 24 | return np.fft.ifftshift(np.fft.ifftn(np.fft.fftshift( 25 | x0, axes=axes), axes=axes), axes=axes) 26 | 27 | 28 | def cgsense(kspace, sens, coil_axis: int = -1): 29 | """Conjugate Gradient SENSE for arbitrary Cartesian acquisitions. 30 | 31 | Parameters 32 | ---------- 33 | kspace : array_like 34 | Undersampled kspace data with exactly 0 in place of missing 35 | samples. 36 | sens : array_like or callable. 37 | Coil sensitivity maps or a function that generates them with 38 | the following function signature: 39 | 40 | sens(kspace: np.ndarray, coil_axis: int) -> np.ndarray 41 | 42 | coil_axis : int, optional 43 | Dimension of kspace and sens holding the coil data. 44 | 45 | Returns 46 | ------- 47 | res : array_like 48 | Single coil unaliased estimate (imspace). 49 | 50 | Notes 51 | ----- 52 | Implements a Cartesian version of the iterative algorithm 53 | described in [1]_. It can handle arbitrary undersampling of 54 | Cartesian acquisitions and arbitrarily-dimensional 55 | datasets. All dimensions except ``coil_axis`` will be used 56 | for reconstruction. 57 | 58 | This implementation uses the scipy.sparse.linalg.cg() conjugate 59 | gradient algorithm to solve A^H A x = A^H b. 60 | 61 | References 62 | ---------- 63 | .. [1] Pruessmann, Klaas P., et al. "Advances in sensitivity 64 | encoding with arbitrary k‐space trajectories." Magnetic 65 | Resonance in Medicine: An Official Journal of the 66 | International Society for Magnetic Resonance in Medicine 67 | 46.4 (2001): 638-651. 68 | """ 69 | # Make sure coils are in the back 70 | kspace = np.moveaxis(kspace, coil_axis, -1) 71 | 72 | # Generate the coil sensitivities 73 | if callable(sens): 74 | imspace = _ifft(kspace) 75 | sens = sens(imspace, coil_axis=coil_axis) 76 | 77 | sens = np.moveaxis(sens, coil_axis, -1) 78 | tipe = kspace.dtype 79 | 80 | # Get the sampling mask: 81 | dims = kspace.shape[:-1] 82 | mask = np.abs(kspace[..., 0]) > 0 83 | 84 | # We are solving Ax = b where A takes the unaliased single coil 85 | # image x to the undersampled kspace data, b. Since A is usually 86 | # not square we'd need to use lsqr/lsmr which can take a while 87 | # and won't give great results. So we can make a sqaure encoding 88 | # matrix like this: 89 | # Ax = b 90 | # A^H A x = A^H b 91 | # E = A^H A is square! 92 | # So now we can solve using scipy's cg() method which luckily 93 | # accepts complex inputs! We will need to represent our data 94 | # and encoding matrices as vectors and matrices: 95 | # A : (sx*sy*nc, sx*sy) 96 | # x : (sx*sy,) 97 | # b : (sx*sy*nc,) 98 | # => E : (sx*sy, sx*sy) 99 | 100 | def _AH(x0): 101 | """kspace -> imspace""" 102 | x0 = np.reshape(x0, kspace.shape) 103 | res = np.sum(sens.conj()*_ifft(x0), axis=-1) 104 | return np.reshape(res, (-1,)) 105 | 106 | def _A(x0): 107 | """imspace -> kspace""" 108 | res = np.reshape(x0, dims) 109 | res = _fft(res[..., None]*sens)*mask[..., None] 110 | return np.reshape(res, (-1,)) 111 | 112 | # Make LinearOperator, A^H b, and use CG to solve 113 | def E(x0): 114 | return _AH(_A(x0)) 115 | AHA = LinearOperator( 116 | (np.prod(dims), np.prod(dims)), 117 | matvec=E, rmatvec=E) 118 | b = _AH(np.reshape(kspace, (-1,))) 119 | 120 | t0 = time() 121 | x, _info = cg(AHA, b, atol=0) 122 | logging.info('CG-SENSE took %g sec', (time() - t0)) 123 | 124 | return np.reshape(x, dims).astype(tipe) 125 | -------------------------------------------------------------------------------- /pygrappa/coils.py: -------------------------------------------------------------------------------- 1 | """Coil estimation strategies.""" 2 | 3 | import numpy as np 4 | from scipy.linalg import eigh 5 | from skimage.filters import threshold_li 6 | 7 | 8 | def walsh(imspace, mask=None, coil_axis: int = -1): 9 | """Stochastic matched filter coil combine. 10 | 11 | Parameters 12 | ---------- 13 | mask : array_like 14 | A mask indicating which pixels of the coil sensitivity mask 15 | should be computed. If ``None``, this will be computed by 16 | applying a threshold to the sum-of-squares coil combination. 17 | Must be the same shape as a single coil. 18 | coil_axis : int 19 | Dimension that has coils. 20 | 21 | Notes 22 | ----- 23 | Adapted from [1]_. Based on the paper [2]_. 24 | 25 | References 26 | ---------- 27 | .. [1] https://github.com/ismrmrd/ismrmrd-python-tools/ 28 | blob/master/ismrmrdtools/coils.py 29 | .. [2] Walsh, David O., Arthur F. Gmitro, and Michael W. 30 | Marcellin. "Adaptive reconstruction of phased array MR 31 | imagery." Magnetic Resonance in Medicine: An Official 32 | Journal of the International Society for Magnetic 33 | Resonance in Medicine 43.5 (2000): 682-690. 34 | """ 35 | imspace = np.moveaxis(imspace, coil_axis, -1) 36 | ncoils = imspace.shape[-1] 37 | ns = np.prod(imspace.shape[:-1]) 38 | 39 | if mask is None: 40 | sos = np.sqrt(np.sum(np.abs(imspace)**2, axis=-1)) 41 | thresh = threshold_li(sos) 42 | mask = (sos > thresh).flatten() 43 | else: 44 | mask = mask.flatten() 45 | assert mask.size == ns, 'mask must be the same size as a coil!' 46 | 47 | # Compute the sample auto-covariances pointwise, will be 48 | # Hermitian symmetric, only need lower triangular matrix 49 | Rs = np.empty((ncoils, ncoils, ns), dtype=imspace.dtype) 50 | for p in range(ncoils): 51 | for q in range(p): 52 | Rs[q, p, :] = (np.conj( 53 | imspace[..., p])*imspace[..., q]).flatten() 54 | 55 | # TODO: 56 | # # Smooth the covariance 57 | # for p in range(ncoils): 58 | # for q in range(ncoils): 59 | # Rs[p, q] = smooth(Rs[p, q, ...], smoothing) 60 | 61 | # At each point in the image, find the dominant eigenvector 62 | # and corresponding eigenvalue of the signal covariance 63 | # matrix using the power method 64 | csm = np.zeros((ns, ncoils), dtype=imspace.dtype) 65 | for ii in np.nonzero(mask)[0]: 66 | R = Rs[..., ii] 67 | v = eigh(R, lower=False, 68 | eigvals=(ncoils-1, ncoils-1))[1].squeeze() 69 | csm[ii, :] = v/np.linalg.norm(v) 70 | 71 | return np.moveaxis(np.reshape(csm, imspace.shape), -1, coil_axis) 72 | -------------------------------------------------------------------------------- /pygrappa/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckib2/pygrappa/539beda1e8881ff36797cb6612c2332e25463e17/pygrappa/examples/__init__.py -------------------------------------------------------------------------------- /pygrappa/examples/bart_kspa.py: -------------------------------------------------------------------------------- 1 | '''Do kSPA using BART stuff.''' 2 | 3 | # from time import time 4 | 5 | # import numpy as np 6 | # import matplotlib.pyplot as plt 7 | # from bart import bart # pylint: disable=E0401 8 | 9 | # from pygrappa import kspa 10 | # from utils import gridder 11 | 12 | if __name__ == '__main__': 13 | pass 14 | # # raise NotImplementedError('kSPA not ready yet, sorry...') 15 | # 16 | # sx, spokes, nc = 16, 16, 4 17 | # traj = bart(1, 'traj -r -x%d -y%d' % (sx, spokes)) 18 | # kx, ky = traj[0, ...].real.flatten(), traj[1, ...].real.flatten() 19 | # 20 | # # Use BART to get Shepp-Logan and sensitivity maps 21 | # t0 = time() 22 | # k = bart(1, 'phantom -k -s%d -t' % nc, traj).reshape((-1, nc)) 23 | # print('Took %g seconds to simulate %d coils' % (time() - t0, nc)) 24 | # sens = bart(1, 'phantom -S%d -x%d' % (nc, sx)).squeeze() 25 | # # ksens = bart(1, 'fft -u 3', sens) 26 | # 27 | # # Undersample 28 | # ku = k.copy() 29 | # # ku[::4] = 0 30 | # 31 | # # Reconstruct using kSPA 32 | # res = kspa(kx, ky, ku, sens) 33 | # # assert False 34 | # # fil = np.hamming(sx)[:, None]*np.hamming(sx)[None, :] 35 | # # res = res*fil 36 | # plt.imshow(np.abs(res)) 37 | # plt.show() 38 | # 39 | # # Take a looksie 40 | # sos = lambda x0: np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 41 | # ifft = lambda x0: np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 42 | # x0))) 43 | # plt.subplot(1, 3, 1) 44 | # plt.imshow(sos(gridder(kx, ky, k, sx, sx))) 45 | # plt.title('Truth') 46 | # 47 | # plt.subplot(1, 3, 2) 48 | # plt.imshow(sos(gridder(kx, ky, ku, sx, sx))) 49 | # plt.title('Undersampled') 50 | # 51 | # plt.subplot(1, 3, 3) 52 | # plt.imshow(np.abs(ifft(res))) 53 | # plt.title('kSPA') 54 | # 55 | # plt.show() 56 | -------------------------------------------------------------------------------- /pygrappa/examples/bart_pars.py: -------------------------------------------------------------------------------- 1 | '''Do PARS using BART stuff.''' 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | from pygrappa import pars 9 | from pygrappa.utils import gridder 10 | 11 | from bart import bart # pylint: disable=E0401 12 | 13 | 14 | def _sos(x0): 15 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 16 | 17 | 18 | if __name__ == '__main__': 19 | 20 | sx, spokes, nc = 256, 256, 8 21 | traj = bart(1, 'traj -r -x%d -y%d' % (sx, spokes)) 22 | kx, ky = traj[0, ...].real.flatten(), traj[1, ...].real.flatten() 23 | 24 | # Use BART to get Shepp-Logan and sensitivity maps 25 | t0 = time() 26 | k = bart(1, 'phantom -k -s%d -t' % nc, traj).reshape((-1, nc)) 27 | print('Took %g seconds to simulate %d coils' % (time() - t0, nc)) 28 | sens = bart(1, 'phantom -S%d -x%d' % (nc, sx)).squeeze() 29 | 30 | # Undersample 31 | ku = k.copy() 32 | ku[::2] = 0 33 | 34 | # Take a looksie 35 | plt.subplot(1, 3, 1) 36 | plt.imshow(_sos(gridder(kx, ky, k, sx, sx))) 37 | plt.title('Truth') 38 | 39 | plt.subplot(1, 3, 2) 40 | plt.imshow(_sos(gridder(kx, ky, ku, sx, sx))) 41 | plt.title('Undersampled') 42 | 43 | plt.subplot(1, 3, 3) 44 | res = pars(kx, ky, ku, sens, kernel_radius=.8) 45 | plt.imshow(_sos(res)) 46 | plt.title('PARS') 47 | 48 | plt.show() 49 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_cgrappa.py: -------------------------------------------------------------------------------- 1 | """Basic CGRAPPA example using Shepp-Logan phantom.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | 7 | from pygrappa import cgrappa 8 | 9 | 10 | if __name__ == '__main__': 11 | 12 | # Generate fake sensitivity maps: mps 13 | N = 128 14 | ncoils = 4 15 | xx = np.linspace(0, 1, N) 16 | x, y = np.meshgrid(xx, xx) 17 | mps = np.zeros((N, N, ncoils)) 18 | mps[..., 0] = x**2 19 | mps[..., 1] = 1 - x**2 20 | mps[..., 2] = y**2 21 | mps[..., 3] = 1 - y**2 22 | 23 | # generate 4 coil phantom 24 | ph = shepp_logan(N) 25 | imspace = ph[..., None]*mps 26 | imspace = imspace.astype('complex') 27 | imspace = imspace[:, :-4, :] 28 | ax = (0, 1) 29 | kspace = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 30 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 31 | 32 | # crop 20x20 window from the center of k-space for calibration 33 | pd = 10 34 | ctr = int(N/2) 35 | calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() 36 | 37 | # calibrate a kernel 38 | kernel_size = (4, 4) 39 | 40 | # undersample by a factor of 2 in both x and y 41 | kspace[::2, 1::2, :] = 0 42 | kspace[1::2, ::2, :] = 0 43 | 44 | # reconstruct: 45 | res = cgrappa( 46 | kspace, calib, kernel_size, coil_axis=-1, lamda=0.01) 47 | 48 | # Take a look 49 | res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( 50 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 51 | M, N = res.shape[:2] 52 | res0 = np.zeros((2*M, 2*N)) 53 | kk = 0 54 | for idx in np.ndindex((2, 2)): 55 | ii, jj = idx[:] 56 | res0[ii*M:(ii+1)*M, jj*N:(jj+1)*N] = res[..., kk] 57 | kk += 1 58 | plt.imshow(res0, cmap='gray') 59 | plt.show() 60 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_cgsense.py: -------------------------------------------------------------------------------- 1 | """Basic usage of CG-SENSE implementation.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from mpl_toolkits.axes_grid1 import make_axes_locatable 6 | from phantominator import shepp_logan 7 | 8 | from pygrappa import cgsense 9 | from pygrappa.utils import gaussian_csm 10 | 11 | 12 | if __name__ == '__main__': 13 | 14 | N, nc = 128, 4 15 | sens = gaussian_csm(N, N, nc) 16 | 17 | im = shepp_logan(N) + np.finfo('float').eps 18 | im = im[..., None]*sens 19 | kspace = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift( 20 | im, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 21 | 22 | # Undersample 23 | kspace[::2, 1::2, :] = 0 24 | kspace[1::2, ::2, :] = 0 25 | 26 | # SOS of the aliased image 27 | aliased = np.fft.ifftshift(np.fft.ifft2(np.fft.fftshift( 28 | kspace, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 29 | aliased = np.sqrt(np.sum(np.abs(aliased)**2, axis=-1)) 30 | 31 | # Reconstruct from undersampled data and coil sensitivities 32 | res = cgsense(kspace, sens, coil_axis=-1) 33 | 34 | # Take a look 35 | nx, ny = 1, 3 36 | plt.subplot(nx, ny, 1) 37 | plt.imshow(aliased) 38 | plt.title('Aliased') 39 | plt.axis('off') 40 | 41 | plt.subplot(nx, ny, 2) 42 | plt.imshow(np.abs(res)) 43 | plt.title('CG-SENSE') 44 | plt.axis('off') 45 | 46 | plt.subplot(nx, ny, 3) 47 | true = np.abs(shepp_logan(N)) 48 | true /= np.max(true) 49 | res = np.abs(res) 50 | res /= np.max(res) 51 | plt.imshow(true - res) 52 | plt.title('|True - CG-SENSE|') 53 | plt.axis('off') 54 | divider = make_axes_locatable(plt.gca()) 55 | cax = divider.append_axes("right", size="5%", pad=0.05) 56 | plt.colorbar(cax=cax) 57 | 58 | plt.show() 59 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_gfactor.py: -------------------------------------------------------------------------------- 1 | """Simple g-factor maps.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | from pygrappa import gfactor, gfactor_single_coil_R2 7 | from pygrappa.utils import gaussian_csm 8 | 9 | 10 | if __name__ == '__main__': 11 | 12 | # Make circle 13 | N, nc = 128, 8 14 | X, Y = np.meshgrid( 15 | np.linspace(-1, 1, N), 16 | np.linspace(-1, 1, N)) 17 | ph = X**2 + Y**2 < .9**2 18 | 19 | # Try single coil, R=2. For single coil, we'll need to add 20 | # background phase variation so we can pull pixels apart 21 | _, phi = np.meshgrid( 22 | np.linspace(0, np.pi, N), 23 | np.linspace(0, np.pi, N)) 24 | phi = np.exp(1j*phi) 25 | Rx, Ry = 2, 1 26 | g_c1_R2_analytical = gfactor_single_coil_R2(ph*phi, Rx=Rx, Ry=Ry) 27 | g_c1_R2 = gfactor((ph*phi)[..., None], Rx=Rx, Ry=Ry) 28 | 29 | # Try multicoil 30 | coils = ph[..., None]*gaussian_csm(N, N, nc) 31 | Rx, Ry = 1, 3 32 | g_c8_R3 = gfactor(coils, Rx=Rx, Ry=Ry) 33 | 34 | # Let's take a look 35 | nx, ny = 1, 3 36 | plt_args = { 37 | 'vmin': 0, 38 | 'vmax': np.max(np.concatenate( 39 | (g_c1_R2_analytical, g_c1_R2)).flatten()) 40 | } 41 | plt.subplot(nx, ny, 1) 42 | plt.imshow(g_c1_R2_analytical, **plt_args) 43 | plt.title('Single coil, Rx=2, Analytical') 44 | 45 | plt.subplot(nx, ny, 2) 46 | plt.imshow(g_c1_R2, **plt_args) 47 | plt.title('Single coil, Rx=2') 48 | 49 | plt.subplot(nx, ny, 3) 50 | plt.imshow(g_c8_R3) 51 | plt.title('%d coil, Rx/Ry=%d/%d' % (nc, Rx, Ry)) 52 | 53 | plt.show() 54 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_grappa.py: -------------------------------------------------------------------------------- 1 | """Basic GRAPPA example using Shepp-Logan phantom.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | 7 | from pygrappa import grappa 8 | 9 | 10 | if __name__ == '__main__': 11 | 12 | # Generate fake sensitivity maps: mps 13 | N = 128 14 | ncoils = 4 15 | xx = np.linspace(0, 1, N) 16 | x, y = np.meshgrid(xx, xx) 17 | mps = np.zeros((N, N, ncoils)) 18 | mps[..., 0] = x**2 19 | mps[..., 1] = 1 - x**2 20 | mps[..., 2] = y**2 21 | mps[..., 3] = 1 - y**2 22 | 23 | # generate 4 coil phantom 24 | ph = shepp_logan(N) 25 | imspace = ph[..., None]*mps 26 | imspace = imspace.astype('complex') 27 | ax = (0, 1) 28 | kspace = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 29 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 30 | 31 | # crop 20x20 window from the center of k-space for calibration 32 | pd = 10 33 | ctr = int(N/2) 34 | calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() 35 | 36 | # calibrate a kernel 37 | kernel_size = (5, 5) 38 | 39 | # undersample by a factor of 2 in both kx and ky 40 | kspace[::2, 1::2, :] = 0 41 | kspace[1::2, ::2, :] = 0 42 | 43 | # reconstruct: 44 | res = grappa( 45 | kspace, calib, kernel_size, coil_axis=-1, lamda=0.01, 46 | memmap=False) 47 | 48 | # Take a look 49 | res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( 50 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 51 | res0 = np.zeros((2*N, 2*N)) 52 | kk = 0 53 | for idx in np.ndindex((2, 2)): 54 | ii, jj = idx[:] 55 | res0[ii*N:(ii+1)*N, jj*N:(jj+1)*N] = res[..., kk] 56 | kk += 1 57 | plt.imshow(res0, cmap='gray') 58 | plt.show() 59 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_grappaop.py: -------------------------------------------------------------------------------- 1 | """Basic usage of the GRAPPA operator.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | try: 7 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 8 | except ImportError: 9 | from skimage.measure import compare_nrmse 10 | 11 | from pygrappa import mdgrappa, grappaop 12 | from pygrappa.utils import gaussian_csm 13 | 14 | 15 | def fft(x0): 16 | return np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 17 | x0, axes=ax), axes=ax), axes=ax) 18 | 19 | 20 | def sos(x0): 21 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 22 | 23 | 24 | def normalize(x0): 25 | return x0/np.max(x0.flatten()) 26 | 27 | 28 | if __name__ == '__main__': 29 | 30 | # Make a simple phantom -- note that GRAPPA operator only works 31 | # well with pretty well separated coil sensitivities, so using 32 | # these simple maps we don't expect GRAPPA operator to work as 33 | # well as GRAPPA when trying to do "GRAPPA" things 34 | N, nc = 256, 16 35 | ph = shepp_logan(N)[..., None]*gaussian_csm(N, N, nc) 36 | 37 | # Put into kspace 38 | ax = (0, 1) 39 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 40 | ph, axes=ax), axes=ax), axes=ax) 41 | 42 | # 20x20 calibration region 43 | ctr = int(N/2) 44 | pad = 10 45 | calib = kspace[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() 46 | 47 | # Undersample: R=4 48 | kspace4x1 = kspace.copy() 49 | kspace4x1[1::4, ...] = 0 50 | kspace4x1[2::4, ...] = 0 51 | kspace4x1[3::4, ...] = 0 52 | 53 | # Compare to regular ol' GRAPPA 54 | grecon4x1 = mdgrappa(kspace4x1, calib, kernel_size=(4, 5)) 55 | 56 | # Get a GRAPPA operator and do the recon 57 | Gx, Gy = grappaop(calib) 58 | recon4x1 = kspace4x1.copy() 59 | recon4x1[1::4, ...] = recon4x1[0::4, ...] @ Gx 60 | recon4x1[2::4, ...] = recon4x1[1::4, ...] @ Gx 61 | recon4x1[3::4, ...] = recon4x1[2::4, ...] @ Gx 62 | 63 | # Try different undersampling factors: Rx=2, Ry=2. Same Gx, Gy 64 | # will work since we're using the same calibration region! 65 | kspace2x2 = kspace.copy() 66 | kspace2x2[1::2, ...] = 0 67 | kspace2x2[:, 1::2, :] = 0 68 | grecon2x2 = mdgrappa(kspace2x2, calib, kernel_size=(4, 5)) 69 | recon2x2 = kspace2x2.copy() 70 | recon2x2[1::2, ...] = recon2x2[::2, ...] @ Gx 71 | recon2x2[:, 1::2, :] = recon2x2[:, ::2, :] @ Gy 72 | 73 | # Bring everything back into image space, coil combine, and 74 | # normalize for comparison 75 | ph = normalize(shepp_logan(N)) 76 | aliased4x1 = normalize(sos(fft(kspace4x1))) 77 | aliased2x2 = normalize(sos(fft(kspace2x2))) 78 | grappa4x1 = normalize(sos(fft(grecon4x1))) 79 | grappa2x2 = normalize(sos(fft(grecon2x2))) 80 | grappa_op4x1 = normalize(sos(fft(recon4x1))) 81 | grappa_op2x2 = normalize(sos(fft(recon2x2))) 82 | 83 | # Let's take a gander 84 | nx, ny = 2, 3 85 | plt.subplot(nx, ny, 1) 86 | plt.imshow(aliased4x1, cmap='gray') 87 | plt.title('Aliased') 88 | plt.ylabel('Rx=4') 89 | 90 | plt.subplot(nx, ny, 2) 91 | plt.imshow(grappa4x1, cmap='gray') 92 | plt.title('GRAPPA') 93 | plt.xlabel('NRMSE: %.4f' % compare_nrmse(ph, grappa4x1)) 94 | 95 | plt.subplot(nx, ny, 3) 96 | plt.imshow(grappa_op4x1, cmap='gray') 97 | plt.title('GRAPPA operator') 98 | plt.xlabel('NRMSE: %.4f' % compare_nrmse(ph, grappa_op4x1)) 99 | 100 | plt.subplot(nx, ny, 4) 101 | plt.imshow(aliased2x2, cmap='gray') 102 | plt.ylabel('Rx=2, Ry=2') 103 | 104 | plt.subplot(nx, ny, 5) 105 | plt.imshow(grappa2x2, cmap='gray') 106 | plt.xlabel('NRMSE: %.4f' % compare_nrmse(ph, grappa2x2)) 107 | 108 | plt.subplot(nx, ny, 6) 109 | plt.imshow(grappa_op2x2, cmap='gray') 110 | plt.xlabel('NRMSE: %.4f' % compare_nrmse(ph, grappa_op2x2)) 111 | 112 | plt.show() 113 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_gridding.py: -------------------------------------------------------------------------------- 1 | """Demonstrate how to grid non-Cartesian data. 2 | 3 | Notes 4 | ----- 5 | In general, you should probably be using a fast NUFFT implementation, 6 | such as that in BART or NFFT [1]_ [2]_. Unfortunately, most of these 7 | implementations do not work "out-of-the-box" with Python (i.e., can't 8 | pip install them) and/or they aren't cross-platform solutions (e.g., 9 | BART doesn't officially support Microsoft Windows). In the case of 10 | BART, I find the Python interface to be rather clumsy. Those that you 11 | can install through pip, e.g., pynufft, would be great alternatives, 12 | but since this package isn't meant to be a showcase of fast NDFTs, 13 | we're just going to use some simple interpolation methods provided by 14 | scipy. We just want to get a taste of what non-Cartesian datasets 15 | look like. 16 | 17 | References 18 | ---------- 19 | .. [1] Uecker, Martin, et al. "Berkeley advanced reconstruction 20 | toolbox." Proc. Intl. Soc. Mag. Reson. Med. Vol. 23. 2015. 21 | .. [2] Keiner, Jens, Stefan Kunis, and Daniel Potts. "Using 22 | NFFT 3---a software library for various nonequispaced fast 23 | Fourier transforms." ACM Transactions on Mathematical Software 24 | (TOMS) 36.4 (2009): 19. 25 | """ 26 | 27 | from time import time 28 | 29 | import numpy as np 30 | import matplotlib.pyplot as plt 31 | from scipy.cluster.vq import whiten 32 | from phantominator import kspace_shepp_logan, radial 33 | try: 34 | from bart import bart # pylint: disable=E0401 35 | FOUND_BART = True 36 | except ImportError: # ModuleNotFoundError: 37 | FOUND_BART = False 38 | 39 | from pygrappa import radialgrappaop, grog 40 | from pygrappa.utils import gridder 41 | 42 | 43 | # Helper functions for sum-of-squares coil combine and ifft2 44 | def sos(x0): 45 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 46 | 47 | 48 | def ifft(x0): 49 | return np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 50 | np.nan_to_num(x0), axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 51 | 52 | 53 | # Make a wrapper function for BART's nufft function, 54 | # assumes 2D 55 | def bart_nufft(x0): 56 | return bart( 57 | 1, 'nufft -i -t -d %d:%d:1' % (sx, sx), 58 | traj, x0.reshape((1, sx, spokes, nc))).squeeze() 59 | 60 | 61 | if __name__ == '__main__': 62 | 63 | # Demo params 64 | sx, spokes, nc = 128, 128, 8 65 | os = 2 # oversampling factor for gridding 66 | method = 'linear' # interpolation strategy for gridding 67 | 68 | # If you have BART installed, you could replicate this demo with 69 | # the following: 70 | if FOUND_BART: 71 | # Make a radial trajectory, we'll have to mess with it later 72 | # to get it to look like pygrappa usually assumes it is 73 | traj = bart(1, 'traj -r -x %d -y %d' % (sx, spokes)) 74 | 75 | # Multicoil Shepp-Logan phantom kspace measurements 76 | kspace = bart(1, 'phantom -k -s %d -t' % nc, traj) 77 | 78 | # Make kx, ky, k look like they do for pygrappa 79 | bart_kx = traj[0, ...].real.flatten() 80 | bart_ky = traj[1, ...].real.flatten() 81 | bart_k = kspace.reshape((-1, nc)) 82 | 83 | # Do the thing 84 | t0 = time() 85 | bart_imspace = bart_nufft(bart_k) 86 | bart_time = time() - t0 87 | 88 | # Check it out 89 | plt.figure() 90 | plt.imshow(sos(bart_imspace)) 91 | plt.title('BART NUFFT') 92 | plt.xlabel('Recon: %g sec' % bart_time) 93 | plt.show(block=False) 94 | 95 | # The phantominator module also supports arbitrary kspace 96 | # sampling for multiple coils: 97 | kx, ky = radial(sx, spokes) 98 | kx = np.reshape(kx, (sx, spokes), 'F').flatten() 99 | ky = np.reshape(ky, (sx, spokes), 'F').flatten() 100 | k = kspace_shepp_logan(kx, ky, ncoil=nc) 101 | k = whiten(k) 102 | 103 | # We will prefer a gridding approach to keep things simple. The 104 | # helper function gridder wraps scipy.interpolate.griddata(): 105 | t0 = time() 106 | grid_imspace = gridder(kx, ky, k, sx, sx, os=os, method=method) 107 | grid_time = time() - t0 108 | 109 | # Take a gander 110 | plt.figure() 111 | plt.imshow(sos(grid_imspace)) 112 | plt.title('scipy.interpolate.griddata') 113 | plt.xlabel('Recon: %g sec' % grid_time) 114 | plt.show(block=False) 115 | 116 | # We could also use GROG to grid 117 | t0 = time() 118 | Gx, Gy = radialgrappaop(kx, ky, k, nspokes=spokes) 119 | grog_res = grog(kx, ky, k, sx, sx, Gx, Gy) 120 | grid_time = time() - t0 121 | 122 | plt.figure() 123 | plt.imshow(sos(ifft(grog_res))) 124 | plt.title('GROG') 125 | plt.xlabel('Recon: %g sec' % grid_time) 126 | plt.show() 127 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_hpgrappa.py: -------------------------------------------------------------------------------- 1 | """Basic hp-GRAPPA usage.""" 2 | 3 | import numpy as np 4 | from mpl_toolkits.mplot3d import Axes3D # pylint: disable=W0611 # NOQA 5 | import matplotlib.pyplot as plt 6 | from phantominator import shepp_logan 7 | 8 | from pygrappa import hpgrappa, mdgrappa 9 | from pygrappa.utils import gaussian_csm 10 | 11 | 12 | if __name__ == '__main__': 13 | 14 | # The much abused Shepp-Logan phantom 15 | N, ncoil = 128, 5 16 | ph = shepp_logan(N)[..., None]*gaussian_csm(N, N, ncoil) 17 | fov = (10e-2, 10e-2) # 10cm x 10cm FOV 18 | 19 | # k-space-ify it 20 | ax = (0, 1) 21 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 22 | ph, axes=ax), axes=ax), axes=ax) 23 | 24 | # Get an ACS region 25 | pad = 12 26 | ctr = int(N/2) 27 | calib = kspace[ctr-pad:ctr+pad, ...].copy() 28 | 29 | # Undersample: R=3 30 | kspace[0::3, ...] = 0 31 | kspace[1::3, ...] = 0 32 | 33 | # Run hp-GRAPPA and GRAPPA to compare results 34 | res_hpgrappa, F2 = hpgrappa( 35 | kspace, calib, fov=fov, ret_filter=True) 36 | res_grappa = mdgrappa(kspace, calib) 37 | 38 | # Into image space 39 | imspace_hpgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 40 | res_hpgrappa, axes=ax), axes=ax), axes=ax) 41 | imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 42 | res_grappa, axes=ax), axes=ax), axes=ax) 43 | 44 | # Coil combine (sum-of-squares) 45 | cc_hpgrappa = np.sqrt( 46 | np.sum(np.abs(imspace_hpgrappa)**2, axis=-1)) 47 | cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) 48 | ph = shepp_logan(N) 49 | 50 | # Take a look 51 | fig = plt.figure() 52 | ax = fig.add_subplot(projection='3d') 53 | X, Y = np.meshgrid( 54 | np.linspace(-1, 1, N), 55 | np.linspace(-1, 1, N)) 56 | ax.plot_surface(X, Y, F2, linewidth=0, antialiased=False) 57 | plt.title('High Pass Filter') 58 | 59 | plt.figure() 60 | plt.subplot(1, 2, 1) 61 | plt.imshow(cc_hpgrappa, cmap='gray') 62 | plt.title('hp-GRAPPA') 63 | 64 | plt.subplot(1, 2, 2) 65 | plt.imshow(cc_grappa, cmap='gray') 66 | plt.title('GRAPPA') 67 | 68 | plt.show() 69 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_igrappa.py: -------------------------------------------------------------------------------- 1 | """Demonstrate usage of iGRAPPA.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | 7 | from pygrappa import igrappa, mdgrappa 8 | from pygrappa.utils import gaussian_csm 9 | 10 | 11 | if __name__ == '__main__': 12 | 13 | # Simple phantom 14 | N = 128 15 | ncoil = 5 16 | csm = gaussian_csm(N, N, ncoil) 17 | ph = shepp_logan(N)[..., None]*csm 18 | 19 | # Throw into k-space 20 | ax = (0, 1) 21 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 22 | ph, axes=ax), axes=ax), axes=ax) 23 | ref = kspace.copy() 24 | 25 | # Small ACS region: 4x4 26 | pad = 2 27 | ctr = int(N/2) 28 | calib = kspace[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() 29 | 30 | # R=2x2 31 | kspace[::2, 1::2, :] = 0 32 | kspace[1::2, ::2, :] = 0 33 | 34 | # Reconstruct using both GRAPPA and iGRAPPA 35 | res_grappa = mdgrappa(kspace, calib) 36 | res_igrappa, mse = igrappa(kspace, calib, ref=ref) 37 | 38 | # Bring back to image space 39 | imspace_igrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 40 | res_igrappa, axes=ax), axes=ax), axes=ax) 41 | imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 42 | res_grappa, axes=ax), axes=ax), axes=ax) 43 | 44 | # Coil combine (sum-of-squares) 45 | cc_igrappa = np.sqrt(np.sum(np.abs(imspace_igrappa)**2, axis=-1)) 46 | cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) 47 | ph = shepp_logan(N) 48 | 49 | # Take a look 50 | plt.subplot(2, 2, 1) 51 | plt.imshow(cc_igrappa, cmap='gray') 52 | plt.title('iGRAPPA') 53 | 54 | plt.subplot(2, 2, 2) 55 | plt.imshow(cc_grappa, cmap='gray') 56 | plt.title('GRAPPA') 57 | 58 | plt.subplot2grid((2, 2), (1, 0), colspan=2) 59 | plt.plot(np.arange(mse.size), mse) 60 | plt.title('iGRAPPA MSE vs iteration') 61 | plt.xlabel('iteration') 62 | plt.ylabel('MSE') 63 | 64 | plt.show() 65 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_mdgrappa.py: -------------------------------------------------------------------------------- 1 | """Basic usage of multidimensional GRAPPA.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from phantominator import shepp_logan 8 | 9 | from pygrappa import mdgrappa 10 | from pygrappa.utils import gaussian_csm 11 | 12 | 13 | if __name__ == '__main__': 14 | 15 | # Generate fake sensitivity maps: mps 16 | L, M, N = 160, 92, 8 17 | ncoils = 15 18 | mps = gaussian_csm(L, M, ncoils)[..., None, :] 19 | 20 | # generate 3D phantom 21 | ph = shepp_logan((L, M, N), zlims=(-.25, .25)) 22 | imspace = ph[..., None]*mps 23 | ax = (0, 1, 2) 24 | kspace = np.fft.fftshift(np.fft.fftn( 25 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 26 | 27 | # calibrate a kernel 28 | kernel_size = (5, 5, 5) 29 | 30 | # undersample by a factor of 2 in both kx and ky 31 | mask = np.ones(kspace.shape, dtype=bool) 32 | mask[::2, 1::2, ...] = False 33 | mask[1::2, ::2, ...] = False 34 | 35 | # Include calib in data: 20x20xN window at center of k-space for 36 | # calibration (use all z-axis) 37 | ctrs = [int(s/2) for s in kspace.shape[:2]] 38 | pds = [20, 8, 4] 39 | mask[tuple([slice(ctr-pd, ctr+pd) for ctr, pd in zip(ctrs, pds)] + 40 | [slice(None), slice(None)])] = True 41 | kspace *= mask 42 | 43 | # Do the recon 44 | t0 = time() 45 | res = mdgrappa(kspace, kernel_size=kernel_size) 46 | print(f'Took {time() - t0} sec') 47 | 48 | # Take a look at a single slice (z=-.25) 49 | res = np.abs(np.fft.fftshift(np.fft.ifftn( 50 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 51 | res = res[..., 0, :] 52 | res0 = np.zeros((2*L, 2*M)) 53 | kk = 0 54 | for idx in np.ndindex((2, 2)): 55 | ii, jj = idx[:] 56 | res0[ii*L:(ii+1)*L, jj*M:(jj+1)*M] = res[..., kk] 57 | kk += 1 58 | plt.imshow(res0, cmap='gray') 59 | plt.show() 60 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_ncgrappa.py: -------------------------------------------------------------------------------- 1 | '''Demo of Non-Cartesian GRAPPA.''' 2 | 3 | import numpy as np 4 | # import matplotlib.pyplot as plt 5 | 6 | from bart import bart # pylint: disable=E0401 7 | 8 | # from pygrappa import ncgrappa 9 | 10 | if __name__ == '__main__': 11 | 12 | # Get phantom from BART since phantominator doesn't have 13 | # arbitrary sampling yet... 14 | sx, spokes, nc = 128, 128, 8 15 | traj = bart(1, 'traj -r -x %d -y %d' % (sx, spokes)) 16 | kspace = bart(1, 'phantom -k -s %d -t' % nc, traj) 17 | 18 | # # Do inverse gridding with NUFFT so we can get fully sampled 19 | # # cartesian ACS 20 | # igrid = bart( 21 | # 1, 'nufft -i -t -d %d:%d:1' % (sx, sx), 22 | # traj, kspace).squeeze() 23 | # # plt.imshow(np.abs(igrid[..., 0])) 24 | # # plt.show() 25 | # ax = (0, 1) 26 | # igrid = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 27 | # igrid, axes=ax), axes=ax), axes=ax) 28 | # 29 | # # 20x20 calibration region at the center 30 | # ctr = int(sx/2) 31 | # pad = 10 32 | # calib = igrid[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() 33 | 34 | # Get the trajectory and kspace samples 35 | kx = traj[0, ...].real.flatten() 36 | ky = traj[1, ...].real.flatten() 37 | kx /= np.max(np.abs(kx)) 38 | ky /= np.max(np.abs(ky)) 39 | k = kspace.reshape((-1, nc)) 40 | 41 | # Get some calibration data 42 | r = .2 43 | cidx = np.argwhere( 44 | np.logical_and(np.abs(kx) < r, np.abs(ky) < r)).squeeze() 45 | cx = kx[cidx] 46 | cy = ky[cidx] 47 | calib = k[cidx, :] 48 | # plt.scatter(cx, cy) 49 | # plt.show() 50 | 51 | # Undersample by 2 52 | k[::2] = 0 53 | 54 | # plt.scatter(kx[0::2], ky[0::2], 1, label='Missing') 55 | # plt.scatter(kx[1::2], ky[1::2], 1, label='Acquired') 56 | # plt.legend() 57 | # plt.show() 58 | 59 | # Reconstruct with Non-Cartesian GRAPPA -- not working currently! 60 | # ncgrappa(kx, ky, k, cx, cy, calib, kernel_size=.1, coil_axis=-1) 61 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_nlgrappa.py: -------------------------------------------------------------------------------- 1 | """Basic usage of NL-GRAPPA.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | try: 6 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 7 | except ImportError: 8 | from skimage.measure import compare_nrmse 9 | from phantominator import shepp_logan 10 | 11 | from pygrappa import nlgrappa, mdgrappa 12 | from pygrappa.utils import gaussian_csm 13 | 14 | 15 | if __name__ == '__main__': 16 | N, nc = 256, 16 17 | ph = shepp_logan(N)[..., None]*gaussian_csm(N, N, nc) 18 | 19 | # Put into kspace 20 | ax = (0, 1) 21 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 22 | ph, axes=ax), axes=ax), axes=ax) 23 | 24 | # 20x20 calibration region 25 | ctr = int(N/2) 26 | pad = 20 27 | calib = kspace[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() 28 | 29 | # Undersample: R=3 30 | kspace3x1 = kspace.copy() 31 | kspace3x1[1::3, ...] = 0 32 | kspace3x1[2::3, ...] = 0 33 | 34 | # Reconstruct using both GRAPPA and VC-GRAPPA 35 | res_grappa = mdgrappa(kspace3x1.copy(), calib) 36 | res_nlgrappa = nlgrappa( 37 | kspace3x1.copy(), calib, ml_kernel='polynomial', 38 | ml_kernel_args={'cross_term_neighbors': 0}) 39 | 40 | # Bring back to image space 41 | imspace_nlgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 42 | res_nlgrappa, axes=ax), axes=ax), axes=ax) 43 | imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 44 | res_grappa, axes=ax), axes=ax), axes=ax) 45 | 46 | # Coil combine (sum-of-squares) 47 | cc_nlgrappa = np.sqrt( 48 | np.sum(np.abs(imspace_nlgrappa)**2, axis=-1)) 49 | cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) 50 | ph = shepp_logan(N) 51 | 52 | cc_nlgrappa /= np.max(cc_nlgrappa.flatten()) 53 | cc_grappa /= np.max(cc_grappa.flatten()) 54 | ph /= np.max(cc_grappa.flatten()) 55 | 56 | # Take a look 57 | plt.subplot(1, 2, 1) 58 | plt.imshow(cc_nlgrappa, cmap='gray') 59 | plt.title('NL-GRAPPA') 60 | plt.xlabel('NRMSE: %g' % compare_nrmse(ph, cc_nlgrappa)) 61 | 62 | plt.subplot(1, 2, 2) 63 | plt.imshow(cc_grappa, cmap='gray') 64 | plt.title('GRAPPA') 65 | plt.xlabel('NRMSE: %g' % compare_nrmse(ph, cc_grappa)) 66 | plt.show() 67 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_nlgrappa_matlab.py: -------------------------------------------------------------------------------- 1 | """Show basic usage of NL-GRAPPA MATLAB port.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | 7 | from pygrappa import nlgrappa_matlab 8 | from pygrappa.utils import gaussian_csm 9 | 10 | 11 | if __name__ == '__main__': 12 | 13 | # Generate data 14 | N, nc = 128, 8 15 | sens = gaussian_csm(N, N, nc) 16 | im = shepp_logan(N) 17 | im = im[..., None]*sens 18 | sos = np.sqrt(np.sum(np.abs(im)**2, axis=-1)) 19 | 20 | off = 0 # starting sampling location 21 | 22 | # The number of ACS lines 23 | R = 5 24 | nencode = 42 25 | 26 | # The convolution size 27 | num_block = 2 28 | num_column = 15 # make smaller to go quick during development 29 | 30 | # Obtain ACS data and undersampled data 31 | sx, sy, nc = im.shape[:] 32 | sx2 = int(sx/2) 33 | nencode2 = int(nencode/2) 34 | acs_line_loc = np.arange(sx2 - nencode2, sx2 + nencode2) 35 | calib = np.fft.fftshift(np.fft.fft2( 36 | im, axes=(0, 1)), axes=(0, 1))[acs_line_loc, ...].copy() 37 | 38 | # Obtain uniformly undersampled locations 39 | pe_loc = np.arange(off, sx-off, R) 40 | kspace_u = np.zeros((pe_loc.size, sy, nc), dtype=im.dtype) 41 | kspace_u = np.fft.fftshift(np.fft.fft2( 42 | im, axes=(0, 1)), axes=(0, 1))[pe_loc, ...].copy() # why do this? 43 | 44 | # Net reduction factor 45 | acq_idx = np.zeros(sx, dtype=bool) 46 | acq_idx[pe_loc] = True 47 | acq_idx[acs_line_loc] = True 48 | NetR = sx / np.sum(acq_idx) 49 | 50 | # Nonlinear GRAPPA Reconstruction 51 | times_comp = 3 # The number of times of the first-order terms 52 | full_fourier_data1, ImgRecon1, coef1 = nlgrappa_matlab( 53 | kspace_u, R, pe_loc, calib, acs_line_loc, num_block, 54 | num_column, times_comp) 55 | 56 | plt.figure() 57 | plt.imshow(np.abs(np.fft.fftshift(ImgRecon1, axes=(0, 1)))) 58 | plt.show() 59 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_pars.py: -------------------------------------------------------------------------------- 1 | """Demo of Non-Cartesian GRAPPA using PARS.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import radial, kspace_shepp_logan 6 | from phantominator.kspace import _kspace_ellipse_sens 7 | from phantominator.sens_coeffs import _sens_coeffs 8 | 9 | from pygrappa import pars 10 | from pygrappa.utils import gridder 11 | 12 | 13 | # Helper functions 14 | def ifft(x0): 15 | return np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 16 | x0, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 17 | 18 | 19 | def sos(x0): 20 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 21 | 22 | 23 | if __name__ == '__main__': 24 | 25 | # Simulate a radial trajectory 26 | sx, spokes, nc = 256, 256, 8 27 | kx, ky = radial(sx, spokes) 28 | 29 | # We reorder the samples like this for easier undersampling later 30 | kx = np.reshape(kx, (sx, spokes)).flatten('F') 31 | ky = np.reshape(ky, (sx, spokes)).flatten('F') 32 | 33 | # Sample Shepp-Logan at points (kx, ky) with nc coils: 34 | kspace = kspace_shepp_logan(kx, ky, ncoil=nc) 35 | k = kspace.copy() 36 | 37 | # Get some calibration data -- for PARS, we train using coil 38 | # sensitivity maps. Here's a hacky way to get those: 39 | coeffs = [] 40 | for ii in range(nc): 41 | coeffs.append(_sens_coeffs(ii)) 42 | coeffs = np.array(coeffs) 43 | tx, ty = np.meshgrid( 44 | np.linspace(np.min(kx), np.max(kx), sx), 45 | np.linspace(np.min(ky), np.max(ky), sx)) 46 | tx, ty = tx.flatten(), ty.flatten() 47 | calib = _kspace_ellipse_sens( 48 | tx/2 + 1j*ty/2, 0, 0, 1, .95, .95, 0, coeffs).T 49 | sens = ifft(calib.reshape((sx, sx, nc))) 50 | 51 | # BART's phantom function has a better way to simulate coil 52 | # sensitivity maps, see examples/bart_pars.py 53 | 54 | # Undersample: R=2 55 | k[::2] = 0 56 | 57 | # Reconstruct with PARS by setting kernel_radius 58 | res = pars(kx, ky, k, sens, kernel_radius=.8) 59 | 60 | # Let's take a look 61 | def gridder0(x0): 62 | return gridder(kx, ky, x0, sx=sx, sy=sx) 63 | 64 | plt.subplot(1, 3, 1) 65 | plt.imshow(sos(gridder0(kspace.reshape((-1, nc))))) 66 | plt.title('Truth') 67 | 68 | plt.subplot(1, 3, 2) 69 | plt.imshow(sos(gridder0(k))) 70 | plt.title('Undersampled') 71 | 72 | plt.subplot(1, 3, 3) 73 | plt.imshow(sos(res)) 74 | plt.title('PARS') 75 | plt.show() 76 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_radialgrappaop.py: -------------------------------------------------------------------------------- 1 | """Basic usage of Radial GRAPPA operator.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | from scipy.cluster.vq import whiten 7 | import matplotlib.pyplot as plt 8 | from phantominator import radial, kspace_shepp_logan 9 | try: 10 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 11 | from skimage.metrics import structural_similarity as compare_ssim # pylint: disable=E0611,E0401 12 | except ImportError: 13 | from skimage.measure import compare_nrmse, compare_ssim 14 | from skimage.morphology import convex_hull_image 15 | from skimage.filters import threshold_li 16 | 17 | from pygrappa import radialgrappaop, grog 18 | 19 | 20 | def ifft(x0): 21 | return np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 22 | x0, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 23 | 24 | 25 | def sos(x0): 26 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 27 | 28 | 29 | if __name__ == '__main__': 30 | 31 | # Radially sampled Shepp-Logan 32 | N, spokes, nc = 288, 72, 8 33 | kx, ky = radial(N, spokes) 34 | kx = np.reshape(kx, (N, spokes), 'F').flatten().astype(np.float32) 35 | ky = np.reshape(ky, (N, spokes), 'F').flatten().astype(np.float32) 36 | k = kspace_shepp_logan(kx, ky, ncoil=nc).astype(np.complex64) 37 | k = whiten(k) # whitening seems to help conditioning of Gx, Gy 38 | 39 | # # Instead of whitening, maybe you prefer to reduce coils: 40 | # nc = 4 41 | # U, S, Vh = np.linalg.svd(k, full_matrices=False) 42 | # k = U[:, :nc] @ np.diag(S[:nc]) @ Vh[:nc, :nc] 43 | 44 | # Take a look at the sampling pattern: 45 | plt.scatter(kx, ky, .1) 46 | plt.title('Radial Sampling Pattern') 47 | plt.show() 48 | 49 | # Get the GRAPPA operators! 50 | t0 = time() 51 | Gx, Gy = radialgrappaop(kx, ky, k, nspokes=spokes) 52 | print('Gx, Gy computed in %g seconds' % (time() - t0)) 53 | 54 | # Do GROG 55 | t0 = time() 56 | res, Dx, Dy = grog(kx, ky, k, N, N, Gx, Gy, ret_dicts=True) 57 | print('Gridded in %g seconds' % (time() - t0)) 58 | 59 | # We can do it faster again if we pass back in the dictionaries! 60 | # t0 = time() 61 | # res = grog(kx, ky, k, N, N, Gx, Gy, Dx=Dx, Dy=Dy) 62 | # print('Gridded in %g seconds' % (time() - t0)) 63 | 64 | # Get the Cartesian grid 65 | tx, ty = np.meshgrid( 66 | np.linspace(np.min(kx), np.max(kx), N), 67 | np.linspace(np.min(ky), np.max(ky), N)) 68 | tx, ty = tx.flatten(), ty.flatten() 69 | kc = kspace_shepp_logan(tx, ty, ncoil=nc) 70 | kc = whiten(kc) 71 | outside = np.argwhere( 72 | np.sqrt(tx**2 + ty**2) > np.max(kx)).squeeze() 73 | kc[outside] = 0 # keep region of support same as radial 74 | kc = np.reshape(kc, (N, N, nc), order='F') 75 | 76 | # Make sure we gridded something recognizable 77 | nx, ny = 1, 3 78 | plt.subplot(nx, ny, 1) 79 | true = sos(ifft(kc)) 80 | true /= np.max(true.flatten()) 81 | thresh = threshold_li(true) 82 | mask = convex_hull_image(true > thresh) 83 | true *= mask 84 | plt.imshow(true) 85 | plt.title('Cartesian Sampled') 86 | 87 | plt.subplot(nx, ny, 2) 88 | scgrog = sos(ifft(res)) 89 | scgrog /= np.max(scgrog.flatten()) 90 | scgrog *= mask 91 | plt.imshow(scgrog) 92 | plt.title('SC-GROG') 93 | 94 | plt.subplot(nx, ny, 3) 95 | plt.imshow(true - scgrog) 96 | plt.title('Residual') 97 | nrmse = compare_nrmse(true, scgrog) 98 | ssim = compare_ssim(true, scgrog, data_range=np.max(true.flatten()) - np.min(true.flatten())) 99 | plt.xlabel('NRMSE: %g, SSIM: %g' % (nrmse, ssim)) 100 | # print(nrmse, ssim) 101 | 102 | plt.show() 103 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_seggrappa.py: -------------------------------------------------------------------------------- 1 | '''Demonstrate how to use Segmented GRAPPA.''' 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | try: 7 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 8 | except ImportError: 9 | from skimage.measure import compare_nrmse 10 | 11 | from pygrappa import mdgrappa, seggrappa 12 | from pygrappa.utils import gaussian_csm 13 | 14 | if __name__ == '__main__': 15 | 16 | # Simple phantom 17 | N, ncoil = 128, 5 18 | ph = shepp_logan(N)[..., None]*gaussian_csm(N, N, ncoil) 19 | ax = (0, 1) 20 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 21 | ph, axes=ax), axes=ax), axes=ax) 22 | 23 | # Two different calibration regions not including the center 24 | offset = 10 25 | pad = 5 26 | ctr = int(N/2) 27 | calib_upper = kspace[ctr-pad+offset:ctr+pad+offset, ...].copy() 28 | calib_lower = kspace[ctr-pad-offset:ctr+pad-offset, ...].copy() 29 | 30 | # A single calibration region at the center for comparison 31 | pad_single = 2*pad 32 | calib = kspace[ctr-pad_single:ctr+pad_single, ...].copy() 33 | 34 | # Undersample kspace 35 | kspace[:, ::2, :] = 0 36 | 37 | # Reconstruct using segmented GRAPPA with separate ACS regions 38 | res_seg = seggrappa(kspace, [calib_lower, calib_upper]) 39 | 40 | # Reconstruct using single calibration region at the center 41 | res_grappa = mdgrappa(kspace, calib) 42 | 43 | # Into image space 44 | imspace_seg = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 45 | res_seg, axes=ax), axes=ax), axes=ax) 46 | imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 47 | res_grappa, axes=ax), axes=ax), axes=ax) 48 | 49 | # Coil combine (sum-of-squares) 50 | cc_seg = np.sqrt( 51 | np.sum(np.abs(imspace_seg)**2, axis=-1)) 52 | cc_grappa = np.sqrt( 53 | np.sum(np.abs(imspace_grappa)**2, axis=-1)) 54 | ph = shepp_logan(N) 55 | 56 | # Normalize 57 | cc_seg /= np.max(cc_seg.flatten()) 58 | cc_grappa /= np.max(cc_grappa.flatten()) 59 | ph /= np.max(ph.flatten()) 60 | 61 | # Take a look 62 | tx, ty = (0, 10) 63 | text_args = {'color': 'white'} 64 | plt.figure() 65 | plt.subplot(2, 2, 1) 66 | plt.imshow(cc_seg, cmap='gray') 67 | plt.title('Segmented GRAPPA') 68 | plt.text( 69 | tx, ty, 'MSE: %.2f' % compare_nrmse(ph, cc_seg), text_args) 70 | plt.axis('off') 71 | 72 | plt.subplot(2, 2, 2) 73 | plt.imshow(cc_grappa, cmap='gray') 74 | plt.title('GRAPPA') 75 | plt.text( 76 | tx, ty, 'MSE: %.4f' % compare_nrmse(ph, cc_grappa), text_args) 77 | plt.axis('off') 78 | 79 | plt.subplot(2, 2, 3) 80 | calib_region = np.zeros((N, N), dtype=bool) 81 | calib_region[ctr-pad+offset:ctr+pad+offset, ...] = True 82 | calib_region[ctr-pad-offset:ctr+pad-offset, ...] = True 83 | plt.imshow(calib_region) 84 | plt.title('Segmented GRAPPA ACS regions') 85 | plt.axis('off') 86 | 87 | plt.subplot(2, 2, 4) 88 | calib_region = np.zeros((N, N), dtype=bool) 89 | calib_region[ctr-pad_single:ctr+pad_single, ...] = True 90 | plt.imshow(calib_region) 91 | plt.title('GRAPPA ACS region') 92 | plt.axis('off') 93 | 94 | plt.show() 95 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_sense1d.py: -------------------------------------------------------------------------------- 1 | """Show basic usage of 1D SENSE.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | 7 | from pygrappa import sense1d 8 | from pygrappa.utils import gaussian_csm 9 | 10 | 11 | if __name__ == '__main__': 12 | N, nc = 128, 8 13 | im = shepp_logan(N) 14 | sens = gaussian_csm(N, N, nc) 15 | im = im[..., None]*sens 16 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 17 | im, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 18 | 19 | # Undersample 20 | R = 4 21 | kspace[::R, ...] = 0 22 | kspace[1::R, ...] = 0 23 | kspace[2::R, ...] = 0 24 | 25 | # Do the SENSE recon 26 | res = sense1d(kspace, sens, Rx=R, coil_axis=-1, imspace=False) 27 | plt.imshow(np.abs(res)) 28 | plt.show() 29 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_slicegrappa.py: -------------------------------------------------------------------------------- 1 | """Basic demo of Slice-GRAPPA.""" 2 | 3 | import numpy as np 4 | from phantominator import shepp_logan 5 | import matplotlib.pyplot as plt 6 | from matplotlib.animation import FuncAnimation 7 | 8 | from pygrappa import slicegrappa 9 | from pygrappa.utils import gaussian_csm 10 | 11 | 12 | if __name__ == '__main__': 13 | # Get slices of 3D Shepp-Logan phantom 14 | N = 128 15 | ns = 2 16 | ph = shepp_logan((N, N, ns), zlims=(-.3, 0)) 17 | 18 | # Apply some coil sensitivities 19 | ncoil = 8 20 | csm = gaussian_csm(N, N, ncoil) 21 | ph = ph[..., None, :]*csm[..., None] 22 | 23 | # Shift one slice FOV/2 (SMS-CAIPI) 24 | ph[..., -1] = np.fft.fftshift(ph[..., -1], axes=0) 25 | 26 | # Put into kspace 27 | ax = (0, 1) 28 | kspace = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift( 29 | ph, axes=ax), axes=ax), axes=ax) 30 | 31 | # Calibration data is individual slices 32 | calib = kspace.copy() 33 | 34 | # Simulate SMS by simply adding slices together 35 | kspace_sms = np.sum(kspace, axis=-1) 36 | 37 | # Make identical time frames 38 | nt = 5 39 | kspace_sms = np.tile(kspace_sms[..., None], (1, 1, 1, nt)) 40 | 41 | # Separate the slices using Slice-GRAPPA 42 | res = slicegrappa( 43 | kspace_sms, calib, kernel_size=(5, 5), prior='kspace') 44 | 45 | # IFFT and stitch slices together 46 | res = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 47 | res, axes=ax), axes=ax), axes=ax) 48 | res0 = np.zeros((ns*N, N, nt)) 49 | for ii in range(ns): 50 | res0[ii*N:(ii+1)*N, ...] = np.sqrt( 51 | np.sum(np.abs(res[..., ii])**2, axis=2)) 52 | 53 | # Some code to look at the animation 54 | fig = plt.figure() 55 | ax = plt.imshow(np.abs(res0[..., 0]), cmap='gray') 56 | 57 | def init(): 58 | """Initialize ax data.""" 59 | ax.set_array(np.abs(res0[..., 0])) 60 | return (ax,) 61 | 62 | def animate(frame): 63 | """Update frame.""" 64 | ax.set_array(np.abs(res0[..., frame])) 65 | return (ax,) 66 | 67 | anim = FuncAnimation( 68 | fig, animate, init_func=init, frames=nt, 69 | interval=40, blit=True) 70 | plt.show() 71 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_splitslicegrappa.py: -------------------------------------------------------------------------------- 1 | """Basic demo of Split-Slice-GRAPPA.""" 2 | 3 | import numpy as np 4 | from phantominator import shepp_logan 5 | import matplotlib.pyplot as plt 6 | from matplotlib.animation import FuncAnimation 7 | 8 | from pygrappa import splitslicegrappa 9 | from pygrappa.utils import gaussian_csm 10 | 11 | 12 | if __name__ == '__main__': 13 | # Get slices of 3D Shepp-Logan phantom 14 | N = 128 15 | ns = 2 16 | ph = shepp_logan((N, N, ns), zlims=(-.3, 0)) 17 | 18 | # Apply some coil sensitivities 19 | ncoil = 8 20 | csm = gaussian_csm(N, N, ncoil) 21 | ph = ph[..., None, :]*csm[..., None] 22 | 23 | # Shift one slice FOV/2 (SMS-CAIPI) 24 | ph[..., -1] = np.fft.fftshift(ph[..., -1], axes=0) 25 | 26 | # Put into kspace 27 | ax = (0, 1) 28 | kspace = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift( 29 | ph, axes=ax), axes=ax), axes=ax) 30 | 31 | # Calibration data is individual slices 32 | calib = kspace.copy() 33 | 34 | # Simulate SMS by simply adding slices together 35 | kspace_sms = np.sum(kspace, axis=-1) 36 | 37 | # Make identical time frames 38 | nt = 5 39 | kspace_sms = np.tile(kspace_sms[..., None], (1, 1, 1, nt)) 40 | 41 | # Separate the slices using Split-Slice-GRAPPA 42 | res = splitslicegrappa( 43 | kspace_sms, calib, kernel_size=(5, 5), prior='kspace') 44 | 45 | # IFFT and stitch slices together 46 | res = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 47 | res, axes=ax), axes=ax), axes=ax) 48 | res0 = np.zeros((ns*N, N, nt)) 49 | for ii in range(ns): 50 | res0[ii*N:(ii+1)*N, ...] = np.sqrt( 51 | np.sum(np.abs(res[..., ii])**2, axis=2)) 52 | 53 | # Some code to look at the animation 54 | fig = plt.figure() 55 | ax = plt.imshow(np.abs(res0[..., 0]), cmap='gray') 56 | 57 | def init(): 58 | """Initialize ax data.""" 59 | ax.set_array(np.abs(res0[..., 0])) 60 | return (ax,) 61 | 62 | def animate(frame): 63 | """Update frame.""" 64 | ax.set_array(np.abs(res0[..., frame])) 65 | return (ax,) 66 | 67 | anim = FuncAnimation( 68 | fig, animate, init_func=init, frames=nt, 69 | interval=40, blit=True) 70 | plt.show() 71 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_tgrappa.py: -------------------------------------------------------------------------------- 1 | """Example demonstrating how to use TGRAPPA.""" 2 | 3 | import numpy as np 4 | from phantominator import dynamic 5 | import matplotlib.pyplot as plt 6 | from matplotlib.animation import FuncAnimation 7 | 8 | from pygrappa import tgrappa 9 | from pygrappa.utils import gaussian_csm 10 | 11 | 12 | if __name__ == '__main__': 13 | 14 | # Simulation parameters 15 | N = 128 # in-plane resolution: (N, N) 16 | nt = 40 # number of time frames 17 | ncoil = 4 # number of coils 18 | 19 | # Make a simple phantom 20 | ph = dynamic(N, nt) 21 | 22 | # Apply coil sensitivities 23 | csm = gaussian_csm(N, N, ncoil) 24 | ph = ph[:, :, None, :]*csm[..., None] 25 | 26 | # Throw into kspace 27 | ax = (0, 1) 28 | kspace = np.fft.fftshift(np.fft.fft2(np.fft.fftshift( 29 | ph, axes=ax), axes=ax), axes=ax) 30 | 31 | # Undersample by factor 2 in both kx and ky, alternating with time 32 | kspace[0::2, 1::2, :, 0::2] = 0 33 | kspace[1::2, 0::2, :, 1::2] = 0 34 | 35 | # Reconstruct using TGRAPPA algorithm: 36 | # Use 20x20 calibration region 37 | # Kernel size: (4, 5) 38 | res = tgrappa(kspace, calib_size=(20, 20), kernel_size=(4, 5)) 39 | 40 | # IFFT and stitch coil images together 41 | res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( 42 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 43 | res0 = np.zeros((2*N, 2*N, nt)) 44 | kk = 0 45 | for idx in np.ndindex((2, 2)): 46 | ii, jj = idx[:] 47 | res0[ii*N:(ii+1)*N, jj*N:(jj+1)*N, :] = res[..., kk, :] 48 | kk += 1 49 | 50 | # Some code to look at the animation 51 | fig = plt.figure() 52 | ax = plt.imshow(np.abs(res0[..., 0]), cmap='gray') 53 | 54 | def init(): 55 | """Initialize ax data.""" 56 | ax.set_array(np.abs(res0[..., 0])) 57 | return (ax,) 58 | 59 | def animate(frame): 60 | """Update frame.""" 61 | ax.set_array(np.abs(res0[..., frame])) 62 | return (ax,) 63 | 64 | anim = FuncAnimation( 65 | fig, animate, init_func=init, frames=nt, 66 | interval=40, blit=True) 67 | plt.show() 68 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_ttgrappa.py: -------------------------------------------------------------------------------- 1 | """Demo of Non-Cartesian GRAPPA.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import radial, kspace_shepp_logan 6 | 7 | from pygrappa import ttgrappa 8 | from pygrappa.utils import gridder 9 | 10 | 11 | def _sos(x0): 12 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 13 | 14 | 15 | def _gridder0(x0): 16 | return gridder(kx, ky, x0, sx=sx, sy=sx) 17 | 18 | 19 | if __name__ == '__main__': 20 | 21 | # Simulate a radial trajectory 22 | sx, spokes, nc = 128, 128, 8 23 | kx, ky = radial(sx, spokes) 24 | 25 | # We reorder the samples like this for easier undersampling later 26 | kx = np.reshape(kx, (sx, spokes)).flatten('F') 27 | ky = np.reshape(ky, (sx, spokes)).flatten('F') 28 | 29 | # Sample Shepp-Logan at points (kx, ky) with nc coils: 30 | kspace = kspace_shepp_logan(kx, ky, ncoil=nc) 31 | k = kspace.copy() 32 | 33 | # Get some calibration data -- ideally we would want to simulate 34 | # something other than the image we're going to reconstruct, but 35 | # since this is just proof of concept, we'll go ahead 36 | cx = kx.copy() 37 | cy = ky.copy() 38 | calib = k.copy() 39 | # calib = np.tile(calib[:, None, :], (1, 2, 1)) 40 | calib = calib[:, None, :] # middle axis is the through-time dim 41 | 42 | # Undersample: R=2 43 | k[::2] = 0 44 | 45 | # Reconstruct with Non-Cartesian GRAPPA 46 | res = ttgrappa( 47 | kx, ky, k, cx, cy, calib, kernel_size=25, coil_axis=-1) 48 | 49 | # Let's take a look 50 | plt.subplot(1, 3, 1) 51 | plt.imshow(_sos(_gridder0(k))) 52 | plt.title('Undersampled') 53 | 54 | plt.subplot(1, 3, 2) 55 | plt.imshow(_sos(_gridder0(kspace.reshape((-1, nc))))) 56 | plt.title('True') 57 | 58 | plt.subplot(1, 3, 3) 59 | plt.imshow(_sos(_gridder0(res))) 60 | plt.title('Through-time GRAPPA') 61 | plt.show() 62 | -------------------------------------------------------------------------------- /pygrappa/examples/basic_vcgrappa.py: -------------------------------------------------------------------------------- 1 | """Demonstrate usage of VC-GRAPPA.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | try: 7 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 8 | except ImportError: 9 | from skimage.measure import compare_nrmse 10 | 11 | from pygrappa import vcgrappa, grappa 12 | from pygrappa.utils import gaussian_csm 13 | 14 | 15 | if __name__ == '__main__': 16 | 17 | # Simple phantom 18 | N = 128 19 | ncoil = 8 20 | _, phi = np.meshgrid( # background phase variation 21 | np.linspace(-np.pi, np.pi, N), 22 | np.linspace(-np.pi, np.pi, N)) 23 | phi = np.exp(1j*phi) 24 | csm = gaussian_csm(N, N, ncoil) 25 | ph = shepp_logan(N)*phi 26 | ph = ph[..., None]*csm 27 | 28 | # Throw into k-space 29 | ax = (0, 1) 30 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 31 | ph, axes=ax), axes=ax), axes=ax) 32 | 33 | # 24 ACS lines 34 | pad = 12 35 | ctr = int(N/2) 36 | calib = kspace[ctr-pad:ctr+pad, ...].copy() 37 | 38 | # R=4 39 | kspace[1::4, ...] = 0 40 | kspace[2::4, ...] = 0 41 | kspace[3::4, ...] = 0 42 | 43 | # Reconstruct using both GRAPPA and VC-GRAPPA 44 | res_grappa = grappa(kspace, calib) 45 | res_vcgrappa = vcgrappa(kspace, calib) 46 | 47 | # Bring back to image space 48 | imspace_vcgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 49 | res_vcgrappa, axes=ax), axes=ax), axes=ax) 50 | imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 51 | res_grappa, axes=ax), axes=ax), axes=ax) 52 | 53 | # Coil combine (sum-of-squares) 54 | cc_vcgrappa = np.sqrt( 55 | np.sum(np.abs(imspace_vcgrappa)**2, axis=-1)) 56 | cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1)) 57 | ph = shepp_logan(N) 58 | 59 | # Normalize 60 | cc_vcgrappa /= np.max(cc_vcgrappa.flatten()) 61 | cc_grappa /= np.max(cc_grappa.flatten()) 62 | ph /= np.max(ph.flatten()) 63 | 64 | # Take a look 65 | nx, ny = 2, 2 66 | plt.subplot(nx, ny, 1) 67 | plt.imshow(cc_vcgrappa, cmap='gray') 68 | plt.title('VC-GRAPPA') 69 | plt.xlabel('NRMSE: %g' % compare_nrmse(ph, cc_vcgrappa)) 70 | 71 | plt.subplot(nx, ny, 2) 72 | plt.imshow(cc_grappa, cmap='gray') 73 | plt.title('GRAPPA') 74 | plt.xlabel('NRMSE: %g' % compare_nrmse(ph, cc_grappa)) 75 | 76 | # Check residuals 77 | cc_vcgrappa_resid = ph - cc_vcgrappa 78 | cc_grappa_resid = ph - cc_grappa 79 | fac = np.max(np.concatenate( 80 | (cc_vcgrappa_resid, cc_grappa_resid)).flatten()) 81 | plt_args = { 82 | 'vmin': 0, 83 | 'vmax': fac, 84 | 'cmap': 'gray' 85 | } 86 | 87 | plt.subplot(nx, ny, 3) 88 | plt.imshow(np.abs(cc_vcgrappa_resid), **plt_args) 89 | plt.ylabel('Residuals (x%d)' % int(1/fac + .5)) 90 | 91 | plt.subplot(nx, ny, 4) 92 | plt.imshow(np.abs(cc_grappa_resid), **plt_args) 93 | 94 | plt.show() 95 | -------------------------------------------------------------------------------- /pygrappa/examples/inverse_grog.py: -------------------------------------------------------------------------------- 1 | """Do Cartesian to radial gridding.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | from scipy.cluster.vq import whiten 7 | import matplotlib.pyplot as plt 8 | from phantominator import radial, kspace_shepp_logan 9 | 10 | from pygrappa import radialgrappaop, grog 11 | from pygrappa.utils import gridder 12 | 13 | 14 | # Helpers 15 | def ifft(x0): 16 | return np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 17 | x0, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 18 | 19 | 20 | def sos(x0): 21 | return np.sqrt(np.sum(np.abs(x0)**2, axis=-1)) 22 | 23 | 24 | if __name__ == '__main__': 25 | 26 | # Radially sampled Shepp-Logan 27 | N, spokes, nc = 288, 72, 8 28 | kx, ky = radial(N, spokes) 29 | kx = np.reshape(kx, (N, spokes), 'F').flatten() 30 | ky = np.reshape(ky, (N, spokes), 'F').flatten() 31 | k = kspace_shepp_logan(kx, ky, ncoil=nc) 32 | k = whiten(k) # whitening seems to help conditioning of Gx, Gy 33 | 34 | # Get the GRAPPA operators 35 | t0 = time() 36 | Gx, Gy = radialgrappaop(kx, ky, k, nspokes=spokes) 37 | print('Gx, Gy computed in %g seconds' % (time() - t0)) 38 | 39 | # Do forward GROG (with oversampling) 40 | t0 = time() 41 | res_cart = grog(kx, ky, k, 2*N, 2*N, Gx, Gy) 42 | print('Gridded in %g seconds' % (time() - t0)) 43 | 44 | # Now back to radial (inverse GROG) 45 | res_radial = grog( 46 | kx, ky, np.reshape(res_cart, (-1, nc), order='F'), 2*N, 2*N, 47 | Gx, Gy, inverse=True) 48 | 49 | # Make sure we gridded something recognizable 50 | nx, ny = 1, 3 51 | plt.subplot(nx, ny, 1) 52 | plt.imshow(sos(gridder(kx, ky, k, N, N))) 53 | plt.title('Radial Truth') 54 | 55 | plt.subplot(nx, ny, 2) 56 | N2 = int(N/2) 57 | plt.imshow(sos(ifft(res_cart))[N2:-N2, N2:-N2]) 58 | plt.title('GROG Cartesian') 59 | 60 | plt.subplot(nx, ny, 3) 61 | plt.imshow(sos(gridder(kx, ky, res_radial, N, N))) 62 | plt.title('GROG Radial (Inverse)') 63 | 64 | plt.show() 65 | -------------------------------------------------------------------------------- /pygrappa/examples/md_cgsense.py: -------------------------------------------------------------------------------- 1 | """Multidimensional CG-SENSE.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from phantominator import shepp_logan 8 | 9 | from pygrappa import cgsense 10 | from pygrappa.utils import gaussian_csm 11 | 12 | 13 | if __name__ == '__main__': 14 | 15 | # Generate fake sensitivity maps: mps 16 | L, M, N = 128, 128, 32 17 | ncoils = 4 18 | mps = gaussian_csm(L, M, ncoils)[..., None, :] 19 | 20 | # generate 3D phantom 21 | ph = shepp_logan((L, M, N), zlims=(-.25, .25)) 22 | imspace = ph[..., None]*mps 23 | ax = (0, 1, 2) 24 | kspace = np.fft.fftshift(np.fft.fftn( 25 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 26 | 27 | # undersample by a factor of 2 in both kx and ky 28 | kspace[::2, 1::2, ...] = 0 29 | kspace[1::2, ::2, ...] = 0 30 | 31 | # Do the recon 32 | t0 = time() 33 | res = cgsense(kspace, mps) 34 | print('Took %g sec' % (time() - t0)) 35 | 36 | # Take a look at a single slice (z=-.25) 37 | plt.imshow(np.abs(res[..., 0]), cmap='gray') 38 | plt.show() 39 | -------------------------------------------------------------------------------- /pygrappa/examples/meson.build: -------------------------------------------------------------------------------- 1 | py3.install_sources([ 2 | '__init__.py', 3 | #'bart_kspa.py', 4 | 'bart_pars.py', 5 | 'basic_cgrappa.py', 6 | 'basic_cgsense.py', 7 | 'basic_gfactor.py', 8 | 'basic_grappa.py', 9 | 'basic_grappaop.py', 10 | 'basic_gridding.py', 11 | 'basic_hpgrappa.py', 12 | 'basic_igrappa.py', 13 | 'basic_mdgrappa.py', 14 | #'basic_ncgrappa.py', 15 | #'basic_nlgrappa.py', 16 | 'basic_nlgrappa_matlab.py', 17 | #'basic_pars.py', 18 | 'basic_radialgrappaop.py', 19 | 'basic_seggrappa.py', 20 | 'basic_sense1d.py', 21 | 'basic_slicegrappa.py', 22 | 'basic_splitslicegrappa.py', 23 | 'basic_tgrappa.py', 24 | 'basic_ttgrappa.py', 25 | 'basic_vcgrappa.py', 26 | 'inverse_grog.py', 27 | 'md_cgsense.py', 28 | 'primefac_grog.py', 29 | #'primefac_grog_cardiac.py', 30 | 'tikhonov_regularization.py', 31 | 'use_memmap.py' 32 | ], 33 | subdir: 'pygrappa/examples' 34 | ) 35 | -------------------------------------------------------------------------------- /pygrappa/examples/primefac_grog.py: -------------------------------------------------------------------------------- 1 | """ISMRM abstract code for prime factorization speed-up for SC-GROG.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from scipy.cluster.vq import whiten 8 | from phantominator import radial, kspace_shepp_logan 9 | try: 10 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 11 | except ImportError: 12 | from skimage.measure import compare_nrmse 13 | 14 | from pygrappa import grog, radialgrappaop 15 | 16 | if __name__ == '__main__': 17 | 18 | # Make sure we have the primefac-fork 19 | try: 20 | import primefac # pylint: disable=W0611 # NOQA 21 | except ImportError: 22 | raise ImportError('Need to install fork of primefac: ' 23 | 'https://github.com/elliptic-shiho/' 24 | 'primefac-fork') 25 | 26 | # Radially sampled Shepp-Logan 27 | N, spokes, nc = 288, 72, 8 28 | kx, ky = radial(N, spokes) 29 | kx = np.reshape(kx, (N, spokes), 'F').flatten() 30 | ky = np.reshape(ky, (N, spokes), 'F').flatten() 31 | k = kspace_shepp_logan(kx, ky, ncoil=nc) 32 | k = whiten(k) # whitening seems to help conditioning of Gx, Gy 33 | 34 | # Put in correct shape for radialgrappaop 35 | k = np.reshape(k, (N, spokes, nc)) 36 | kx = np.reshape(kx, (N, spokes)) 37 | ky = np.reshape(ky, (N, spokes)) 38 | 39 | # Get the GRAPPA operators! 40 | t0 = time() 41 | Gx, Gy = radialgrappaop(kx, ky, k) 42 | print('Gx, Gy computed in %g seconds' % (time() - t0)) 43 | 44 | # Put in correct order for GROG 45 | kx = kx.flatten() 46 | ky = ky.flatten() 47 | k = np.reshape(k, (-1, nc)) 48 | 49 | # Do GROG without primefac 50 | t0 = time() 51 | res = grog(kx, ky, k, N, N, Gx, Gy, use_primefac=False) 52 | print('Gridded in %g seconds' % (time() - t0)) 53 | 54 | # Do GROG with primefac 55 | t0 = time() 56 | res_prime = grog(kx, ky, k, N, N, Gx, Gy, use_primefac=True) 57 | print('Gridded in %g seconds (primefac)' % (time() - t0)) 58 | 59 | res = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 60 | res, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 61 | res = np.sqrt(np.sum(np.abs(res)**2, axis=-1)) 62 | 63 | res_prime = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 64 | res_prime, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 65 | res_prime = np.sqrt(np.sum(np.abs(res_prime)**2, axis=-1)) 66 | 67 | nx, ny = 1, 3 68 | plt_opts = { 69 | 'vmin': 0, 70 | 'vmax': np.max(np.concatenate((res, res_prime)).flatten()) 71 | } 72 | fig = plt.figure() 73 | plt.subplot(nx, ny, 1) 74 | plt.imshow(res, **plt_opts) 75 | plt.title('SC-GROG') 76 | 77 | plt.subplot(nx, ny, 2) 78 | plt.imshow(res_prime, **plt_opts) 79 | plt.title('Proposed') 80 | 81 | residual = np.abs(res - res_prime) 82 | scale_fac = int(plt_opts['vmax']/np.max(residual.flatten()) + .5) 83 | 84 | plt.subplot(nx, ny, 3) 85 | plt.imshow(residual*scale_fac, **plt_opts) 86 | plt.title('Residual (x%d)' % scale_fac) 87 | 88 | msg0 = 'NRMSE: %.3e' % compare_nrmse(res, res_prime) 89 | plt.annotate( 90 | msg0, xy=(1, 0), xycoords='axes fraction', 91 | fontsize=10, xytext=(-5, 5), 92 | textcoords='offset points', color='white', 93 | ha='right', va='bottom') 94 | 95 | # Remove ticks 96 | allaxes = fig.get_axes() 97 | for ax in allaxes: 98 | ax.get_xaxis().set_ticks([]) 99 | ax.get_yaxis().set_ticks([]) 100 | plt.subplots_adjust(wspace=0, hspace=0) 101 | plt.show() 102 | -------------------------------------------------------------------------------- /pygrappa/examples/primefac_grog_cardiac.py: -------------------------------------------------------------------------------- 1 | '''ISMRM abstract code for prime factorization speed-up for SC-GROG. 2 | ''' 3 | 4 | from time import time 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | try: 9 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 10 | except ImportError: 11 | from skimage.measure import compare_nrmse 12 | 13 | from pygrappa import grog, radialgrappaop 14 | 15 | if __name__ == '__main__': 16 | 17 | # Load in cardiac data 18 | path = 'data/meas_MID34_CV_Radial7Off_2.4ml_FID1789_Kspace.npy.npz' 19 | data = np.load(path) 20 | 21 | time_pt, sl = 20, 0 22 | k = data['kSpace'][:, :, time_pt, :, sl] 23 | kx = data['kx'][..., time_pt].astype(np.float32) 24 | ky = data['ky'][..., time_pt].astype(np.float32) 25 | N, spokes, nc = k.shape[:] 26 | print(k.shape, kx.shape, ky.shape) 27 | 28 | # Get the GRAPPA operators! 29 | t0 = time() 30 | Gx, Gy = radialgrappaop(kx, ky, k) 31 | print('Gx, Gy computed in %g seconds' % (time() - t0)) 32 | 33 | # Put in correct order for GROG 34 | kx = kx.flatten() 35 | ky = ky.flatten() 36 | k = np.reshape(k, (-1, nc)) 37 | 38 | # Do GROG without primefac 39 | t0 = time() 40 | res = grog(kx, ky, k, N, N, Gx, Gy, use_primefac=False) 41 | print('Gridded in %g seconds' % (time() - t0)) 42 | 43 | # Do GROG with primefac 44 | t0 = time() 45 | res_prime = grog(kx, ky, k, N, N, Gx, Gy, use_primefac=True) 46 | print('Gridded in %g seconds (primefac)' % (time() - t0)) 47 | 48 | res = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 49 | res, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 50 | res = np.sqrt(np.sum(np.abs(res)**2, axis=-1)) 51 | 52 | res_prime = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 53 | res_prime, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 54 | res_prime = np.sqrt(np.sum(np.abs(res_prime)**2, axis=-1)) 55 | 56 | # So Ed doesn't get mad at me... 57 | res = np.flipud(np.fliplr(res)) 58 | res_prime = np.flipud(np.fliplr(res_prime)) 59 | 60 | nx, ny = 1, 3 61 | plt_opts = { 62 | 'vmin': 0, 63 | 'vmax': np.max(np.concatenate((res, res_prime)).flatten()), 64 | 'cmap': 'gray' 65 | } 66 | fig = plt.figure() 67 | plt.subplot(nx, ny, 1) 68 | plt.imshow(res, **plt_opts) 69 | plt.title('SC-GROG') 70 | 71 | plt.subplot(nx, ny, 2) 72 | plt.imshow(res_prime, **plt_opts) 73 | plt.title('Proposed') 74 | 75 | residual = np.abs(res - res_prime) 76 | scale_fac = int(plt_opts['vmax']/np.max(residual.flatten()) + .5) 77 | 78 | plt.subplot(nx, ny, 3) 79 | plt.imshow(residual*scale_fac, **plt_opts) 80 | plt.title('Residual (x%d)' % scale_fac) 81 | 82 | msg0 = 'NRMSE: %.3e' % compare_nrmse(res, res_prime) 83 | plt.annotate( 84 | msg0, xy=(1, 0), xycoords='axes fraction', 85 | fontsize=10, xytext=(-5, 5), 86 | textcoords='offset points', color='white', 87 | ha='right', va='bottom') 88 | 89 | # Remove ticks 90 | allaxes = fig.get_axes() 91 | for ax in allaxes: 92 | ax.get_xaxis().set_ticks([]) 93 | ax.get_yaxis().set_ticks([]) 94 | plt.subplots_adjust(wspace=0, hspace=0) 95 | plt.show() 96 | -------------------------------------------------------------------------------- /pygrappa/examples/tikhonov_regularization.py: -------------------------------------------------------------------------------- 1 | """Demonstrate the effect of Tikhonov regularization.""" 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from phantominator import shepp_logan 6 | try: 7 | from skimage.metrics import normalized_root_mse as compare_nrmse # pylint: disable=E0611,E0401 8 | except ImportError: 9 | from skimage.measure import compare_nrmse 10 | from tqdm import tqdm 11 | 12 | from pygrappa import mdgrappa as grappa 13 | from pygrappa.utils import gaussian_csm 14 | 15 | 16 | if __name__ == '__main__': 17 | 18 | # Simple phantom 19 | N = 128 20 | ncoils = 5 21 | csm = gaussian_csm(N, N, ncoils) 22 | ph = shepp_logan(N)[..., None]*csm 23 | 24 | # Put into k-space 25 | ax = (0, 1) 26 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 27 | ph, axes=ax), axes=ax), axes=ax) 28 | kspace_orig = kspace.copy() 29 | 30 | # 20x20 ACS region 31 | pad = 10 32 | ctr = int(N/2) 33 | calib = kspace[ctr-pad:ctr+pad, ctr-pad:ctr+pad, :].copy() 34 | 35 | # R=2x2 36 | kspace[::2, 1::2, :] = 0 37 | kspace[1::2, ::2, :] = 0 38 | 39 | # Find Tikhonov param that minimizes NRMSE 40 | nlam = 20 41 | lamdas = np.linspace(1e-9, 5e-4, nlam) 42 | mse = np.zeros(lamdas.shape) 43 | akspace = np.abs(kspace_orig) 44 | for ii, lamda in tqdm(enumerate(lamdas), total=nlam, leave=False): 45 | recon = grappa(kspace, calib, lamda=lamda) 46 | mse[ii] = compare_nrmse(akspace, np.abs(recon)) 47 | 48 | # Optimal param minimizes NRMSE 49 | idx = np.argmin(mse) 50 | 51 | # Take a look 52 | plt.plot(lamdas, mse) 53 | plt.plot(lamdas[idx], mse[idx], 'rx', label='Optimal lamda') 54 | plt.title('Tikhonov param vs NRMSE') 55 | plt.xlabel('lamda') 56 | plt.ylabel('NRMSE') 57 | plt.legend() 58 | plt.show() 59 | -------------------------------------------------------------------------------- /pygrappa/examples/use_memmap.py: -------------------------------------------------------------------------------- 1 | """Example demonstrating how process datasets stored in memmap.""" 2 | 3 | from tempfile import NamedTemporaryFile as NTF 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from phantominator import shepp_logan 8 | 9 | from pygrappa import grappa 10 | 11 | 12 | if __name__ == '__main__': 13 | 14 | # Generate fake sensitivity maps: mps 15 | N = 128 16 | ncoils = 4 17 | xx = np.linspace(0, 1, N) 18 | x, y = np.meshgrid(xx, xx) 19 | mps = np.zeros((N, N, ncoils)) 20 | mps[..., 0] = x**2 21 | mps[..., 1] = 1 - x**2 22 | mps[..., 2] = y**2 23 | mps[..., 3] = 1 - y**2 24 | 25 | # generate 4 coil phantom 26 | ph = shepp_logan(N) 27 | imspace = ph[..., None]*mps 28 | imspace = imspace.astype('complex') 29 | 30 | # Use NamedTemporaryFiles for kspace and reconstruction results 31 | with NTF() as kspace_file, NTF() as res_file: 32 | # Make a memmap 33 | kspace = np.memmap( 34 | kspace_file, mode='w+', shape=(N, N, ncoils), 35 | dtype='complex') 36 | 37 | # Fill the memmap with kspace data (remember the [:]!!!) 38 | ax = (0, 1) 39 | kspace[:] = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 40 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 41 | 42 | # crop 20x20 window from the center of k-space for calibration 43 | pd = 10 44 | ctr = int(N/2) 45 | calib = np.array(kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :]) 46 | # Make sure calib is not referencing kspace, it should be a 47 | # copy so it doesn't change in place! 48 | 49 | # undersample by a factor of 2 in both x and y 50 | kspace[::2, 1::2, :] = 0 51 | kspace[1::2, ::2, :] = 0 52 | 53 | # Close the memmap 54 | del kspace 55 | 56 | # ========================================================== # 57 | # Open up a new readonly memmap -- this is where you would 58 | # likely start with data you really wanted to process 59 | kspace = np.memmap( 60 | kspace_file, mode='r', shape=(N, N, ncoils), 61 | dtype='complex') 62 | 63 | # calibrate a kernel 64 | kernel_size = (5, 5) 65 | 66 | # reconstruct, write res out to a memmap with name res_file 67 | grappa( 68 | kspace, calib, kernel_size, coil_axis=-1, lamda=0.01, 69 | memmap=True, memmap_filename=res_file.name) 70 | 71 | # Take a look by opening up the memmap 72 | res = np.memmap( 73 | res_file, mode='r', shape=(N, N, ncoils), 74 | dtype='complex') 75 | res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( 76 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 77 | 78 | res0 = np.zeros((2*N, 2*N)) 79 | kk = 0 80 | for idx in np.ndindex((2, 2)): 81 | ii, jj = idx[:] 82 | res0[ii*N:(ii+1)*N, jj*N:(jj+1)*N] = res[..., kk] 83 | kk += 1 84 | plt.imshow(res0, cmap='gray') 85 | plt.show() 86 | -------------------------------------------------------------------------------- /pygrappa/find_acs.py: -------------------------------------------------------------------------------- 1 | """Automated location of a rectangular ACS.""" 2 | 3 | from time import time 4 | import logging 5 | 6 | import numpy as np 7 | 8 | 9 | def find_acs(kspace, coil_axis: int = -1): 10 | """Find the largest centered hyper-rectangle possible. 11 | 12 | Parameters 13 | ---------- 14 | kspace : array_like 15 | Measured undersampled complex k-space data. N-1 dimensions 16 | hold spatial frequency axes (kx, ky, kz, etc.). 1 dimension 17 | holds coil images (`coil_axis`). The missing entries should 18 | have exactly 0. 19 | coil_axis : int, optional 20 | Dimension holding coil images. 21 | 22 | Returns 23 | ------- 24 | calib : array_like 25 | Fully sampled calibration data extracted from the largest 26 | possible hypercube with origin at the center of k-space. 27 | 28 | Notes 29 | ----- 30 | This algorithm is not especially elegant, but works just fine 31 | with the assumption that the ACS region will be significantly 32 | smaller than the entirety of the data. It grows a hyper- 33 | rectangle from the center and checks to see if there are any 34 | new holes in the region each time it expands. 35 | """ 36 | 37 | kspace = np.moveaxis(kspace, coil_axis, -1) 38 | mask = np.abs(kspace[..., 0]) > 0 39 | 40 | # Start by finding the largest hypercube 41 | ctrs = [d // 2 for d in mask.shape] # assume ACS is at center 42 | slices = [[c, c+1] for c in ctrs] # start with 1 voxel region 43 | t0 = time() 44 | while (all(l > 0 and r < mask.shape[ii] for 45 | ii, (l, r) in enumerate(slices)) and # bounds check 46 | np.all(mask[tuple([slice(l-1, r+1) for 47 | l, r in slices])])): # hole check 48 | # expand isotropically until we can't no more 49 | slices = [[l0-1, r0+1] for l0, r0 in slices] 50 | logging.info('Took %g sec to find hyper-cube', (time() - t0)) 51 | 52 | # FOR DEBUG: 53 | # region = np.zeros(mask.shape, dtype=bool) 54 | # region[tuple([slice(l, r) for l, r in slices])] = True 55 | # import matplotlib.pyplot as plt 56 | # plt.imshow(region[..., 20]) 57 | # plt.show() 58 | 59 | # Stretch left/right in each dimension 60 | t0 = time() 61 | for dim in range(mask.ndim): 62 | # left: only check left condition on the current dimension 63 | while (slices[dim][0] > 0 and 64 | np.all(mask[tuple([slice(l-(dim == k), r) for 65 | k, (l, r) in enumerate(slices)])])): 66 | slices[dim][0] -= 1 67 | # right: only check right condition on the current dimension 68 | while (slices[dim][1] < mask.shape[dim] and 69 | np.all(mask[tuple([slice(l, r+(dim == k)) for 70 | k, (l, r) in enumerate(slices)])])): 71 | slices[dim][1] += 1 72 | logging.info('Took %g sec to find hyper-rect', (time() - t0)) 73 | 74 | return np.moveaxis( 75 | kspace[tuple([slice(l0, r0) for l0, r0 in slices] + 76 | [slice(None)])].copy(), # extra dim for coils 77 | -1, coil_axis) 78 | -------------------------------------------------------------------------------- /pygrappa/gfactor.py: -------------------------------------------------------------------------------- 1 | """Calculate g-factor maps.""" 2 | 3 | import numpy as np 4 | 5 | 6 | def gfactor(coils, Rx: int, Ry: int, coil_axis: int = -1, tol: float = 1e-6): 7 | """Compute g-factor map for coil sensitities and accelerations. 8 | 9 | Parameters 10 | ---------- 11 | C : array_like 12 | Array of coil sensitivities 13 | Ry : int, 14 | x acceleration 15 | Ry : int 16 | y acceleration 17 | coil_axis : int, optional 18 | Dimension holding coil data. 19 | tol : float, optional 20 | 21 | Returns 22 | ------- 23 | g : array_like 24 | g-factor map 25 | 26 | Notes 27 | ----- 28 | Adapted from John Pauly's MATLAB script found at [1]_. 29 | 30 | References 31 | ---------- 32 | .. [1] https://web.stanford.edu/class/ee369c/restricted/ 33 | Solutions/assignment_4_solns.pdf 34 | """ 35 | 36 | # Coils to da back 37 | coils = np.moveaxis(coils, coil_axis, -1) 38 | nx, ny, _nc = coils.shape[:] 39 | 40 | # Get a reference SOS image 41 | sos = np.sqrt(np.sum(np.abs(coils)**2, axis=-1)) 42 | 43 | nrx = nx/Rx 44 | nry = ny/Ry 45 | g = np.zeros((nx, ny)) 46 | for idx in np.ndindex((nx, ny)): 47 | ii, jj = idx[:] 48 | 49 | if sos[ii, jj] > tol: 50 | s = [] 51 | for LXLY in np.ndindex((Rx, Ry)): 52 | LX, LY = LXLY[:] 53 | 54 | ndx = int(np.mod(ii + LX*nrx, nx)) 55 | ndy = int(np.mod(jj + LY*nry, ny)) 56 | CT = coils[ndx, ndy, :] 57 | if (LX == 0) and (LY == 0): 58 | s.append(CT) 59 | elif sos[ndx, ndy] > tol: 60 | s.append(CT) 61 | 62 | s = np.array(s).T 63 | scs = (s.conj().T @ s).real 64 | scsi = np.linalg.pinv(scs) 65 | g[ii, jj] = np.sqrt(scs[0, 0]*scsi[0, 0]) 66 | 67 | return g 68 | 69 | 70 | def gfactor_single_coil_R2(coil, Rx: int = 2, Ry: int = 1): 71 | """Specific example of a single homogeneous coil, R=2. 72 | 73 | Parameters 74 | ---------- 75 | coil : array_like 76 | Single coil sensitivity. 77 | Ry : int, 78 | x acceleration 79 | Ry : int 80 | y acceleration 81 | 82 | Returns 83 | ------- 84 | g : array_like 85 | g-factor map 86 | 87 | Notes 88 | ----- 89 | Analytical solution for a single, homogeneous coil with an 90 | undersampling factor of R=2. Equation 11 in [2]_. 91 | 92 | Comparing head-to-head with pygrappa.gfactor(), this does 93 | produce different results. I don't know which one is more 94 | correct... 95 | 96 | References 97 | ---------- 98 | .. [2] Blaimer, Martin, et al. "Virtual coil concept for improved 99 | parallel MRI employing conjugate symmetric signals." 100 | Magnetic Resonance in Medicine: An Official Journal of the 101 | International Society for Magnetic Resonance in Medicine 102 | 61.1 (2009): 93-102. 103 | """ 104 | 105 | assert coil.ndim == 2, 'Must be single coil!' 106 | assert (Rx == 2 and Ry == 1) or (Rx == 1 and Ry == 2), ( 107 | 'Only one of Rx, Ry can be 2!') 108 | 109 | mask = np.abs(coil) > 0 110 | if Rx == 2: 111 | shifted = np.fft.fftshift(np.angle(coil), axes=0) 112 | else: 113 | shifted = np.fft.fftshift(np.angle(coil), axes=1) 114 | 115 | return mask/np.sin(np.abs(np.angle(coil) - shifted)) 116 | -------------------------------------------------------------------------------- /pygrappa/grappaop.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the GRAPPA operator formalism.""" 2 | 3 | import numpy as np 4 | 5 | 6 | def grappaop(calib, coil_axis: int = -1, lamda: int = 0.01): 7 | """GRAPPA operator for Cartesian calibration datasets. 8 | 9 | Parameters 10 | ---------- 11 | calib : array_like 12 | Calibration region data. Usually a small portion from the 13 | center of kspace. 14 | coil_axis : int, optional 15 | Dimension holding coil data. 16 | lamda : float, optional 17 | Tikhonov regularization parameter. Set to 0 for no 18 | regularization. 19 | 20 | Returns 21 | ------- 22 | Gx, Gy : array_like 23 | GRAPPA operators for both the x and y directions. 24 | 25 | Notes 26 | ----- 27 | Produces the unit operator described in [1]_. 28 | 29 | This seems to only work well when coil sensitivities are very 30 | well separated/distinct. If coil sensitivities are similar, 31 | operators perform poorly. 32 | 33 | References 34 | ---------- 35 | .. [1] Griswold, Mark A., et al. "Parallel magnetic resonance 36 | imaging using the GRAPPA operator formalism." Magnetic 37 | resonance in medicine 54.6 (2005): 1553-1556. 38 | """ 39 | 40 | # Coil axis in the back 41 | calib = np.moveaxis(calib, coil_axis, -1) 42 | _cx, _cy, nc = calib.shape[:] 43 | 44 | # We need sources (last source has no target!) 45 | Sx = np.reshape(calib[:-1, ...], (-1, nc)) 46 | Sy = np.reshape(calib[:, :-1, :], (-1, nc)) 47 | 48 | # And we need targets for an operator along each axis (first 49 | # target has no associated source!) 50 | Tx = np.reshape(calib[1:, ...], (-1, nc)) 51 | Ty = np.reshape(calib[:, 1:, :], (-1, nc)) 52 | 53 | # Train the operators: 54 | Sxh = Sx.conj().T 55 | lamda0 = lamda*np.linalg.norm(Sxh)/Sxh.shape[0] 56 | Gx = np.linalg.solve( 57 | Sxh @ Sx + lamda0*np.eye(Sxh.shape[0]), Sxh @ Tx) 58 | 59 | Syh = Sy.conj().T 60 | lamda0 = lamda*np.linalg.norm(Syh)/Syh.shape[0] 61 | Gy = np.linalg.solve( 62 | Syh @ Sy + lamda0*np.eye(Syh.shape[0]), Syh @ Ty) 63 | return Gx, Gy 64 | -------------------------------------------------------------------------------- /pygrappa/hpgrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of hp-GRAPPA.""" 2 | 3 | import numpy as np 4 | 5 | from pygrappa import mdgrappa 6 | 7 | 8 | def hpgrappa( 9 | kspace, calib, fov, kernel_size=(5, 5), w: float = None, c: float = None, 10 | ret_filter: bool = False, coil_axis: int = -1, lamda: float = 0.01): 11 | """High-pass GRAPPA. 12 | 13 | Parameters 14 | ---------- 15 | fov : tuple, (FOV_x, FOV_y) 16 | Field of view (in m). 17 | w : float, optional 18 | Filter parameter: determines the smoothness of the filter 19 | boundary. 20 | c : float, optional 21 | Filter parameter: sets the cutoff frequency. 22 | ret_filter : bool, optional 23 | Returns the high pass filter determined by (w, c). 24 | 25 | Notes 26 | ----- 27 | If w and/or c are None, then the closest values listed in 28 | Table 1 from [1]_ will be used. 29 | 30 | F2 described by Equation [2] in [1]_ is used to generate the 31 | high pass filter. 32 | 33 | References 34 | ---------- 35 | .. [1] Huang, Feng, et al. "High‐pass GRAPPA: An image support 36 | reduction technique for improved partially parallel 37 | imaging." Magnetic Resonance in Medicine: An Official 38 | Journal of the International Society for Magnetic 39 | Resonance in Medicine 59.3 (2008): 642-649. 40 | """ 41 | 42 | # Pass GRAPPA arguments forward 43 | grappa_args = { 44 | 'kernel_size': kernel_size, 45 | 'coil_axis': -1, 46 | 'lamda': lamda, 47 | } 48 | 49 | # Put the coil dim in the back 50 | kspace = np.moveaxis(kspace, coil_axis, -1) 51 | calib = np.moveaxis(calib, coil_axis, -1) 52 | kx, ky, nc = kspace.shape[:] 53 | cx, cy, nc = calib.shape[:] 54 | kx2, ky2 = int(kx/2), int(ky/2) 55 | cx2, cy2 = int(cx/2), int(cy/2) 56 | 57 | # Save the original type 58 | tipe = kspace.dtype 59 | 60 | # Get filter parameters if None provided 61 | if w is None or c is None: 62 | _w, _c = _filter_parameters(nc, np.min([cx, cy])) 63 | if w is None: 64 | w = _w 65 | if c is None: 66 | c = _c 67 | 68 | # We'll need the filter, seeing as this is high-pass GRAPPA 69 | fov_x, fov_y = fov[:] 70 | kxx, kyy = np.meshgrid( 71 | kx*np.linspace(-1, 1, ky)/(fov_x*2), # I think this gives 72 | ky*np.linspace(-1, 1, kx)/(fov_y*2)) # kspace FOV? 73 | F2 = (1 - 1/(1 + np.exp((np.sqrt(kxx**2 + kyy**2) - c)/w)) + 74 | 1/(1 + np.exp((np.sqrt(kxx**2 + kyy**2) + c)/w))) 75 | 76 | # Apply the filter to both kspace and calibration data 77 | kspace_fil = kspace*F2[..., None] 78 | calib_fil = calib*F2[kx2-cx2:kx2+cx2, ky2-cy2:ky2+cy2, None] 79 | 80 | # Do regular old GRAPPA on filtered data 81 | res = mdgrappa(kspace_fil, calib_fil, **grappa_args) 82 | 83 | # Inverse filter 84 | res = res/F2[..., None] 85 | 86 | # Restore measured data 87 | mask = np.abs(kspace[..., 0]) > 0 88 | res[mask, :] = kspace[mask, :] 89 | res[kx2-cx2:kx2+cx2, ky2-cy2:ky2+cy2, :] = calib 90 | res = np.moveaxis(res, -1, coil_axis) 91 | 92 | # Return the filter if user asked for it 93 | if ret_filter: 94 | return res.astype(tipe), F2 95 | return res.astype(tipe) 96 | 97 | 98 | def _filter_parameters(ncoils: int, num_acs_lines: int): 99 | """Table 1: predefined filter parameters from [1]_. 100 | 101 | Parameters 102 | ---------- 103 | ncoils : int 104 | Number of coil channels. 105 | num_acs_lines : {24, 32, 48, 64} 106 | Number of lines in the ACS region (i.e., number of PEs 107 | in [1]_). 108 | 109 | Returns 110 | ------- 111 | (w, c) : tuple 112 | Filter parameters. 113 | """ 114 | 115 | LESS_THAN_8 = True 116 | MORE_THAN_8 = False 117 | lookup = { 118 | # key: (num_acs_lines, ncoils <= 8), value: (w, c) 119 | (24, LESS_THAN_8): (12, 24), 120 | (32, LESS_THAN_8): (10, 24), 121 | (48, LESS_THAN_8): (8, 24), 122 | (64, LESS_THAN_8): (6, 24), 123 | 124 | (24, MORE_THAN_8): (2, 12), 125 | (32, MORE_THAN_8): (2, 14), 126 | (48, MORE_THAN_8): (2, 18), 127 | (64, MORE_THAN_8): (2, 24) 128 | } 129 | 130 | # If num_acs_lines is not in {24, 32, 48, 64}, find the closest 131 | # one and use that: 132 | valid = np.array([24, 32, 48, 64]) 133 | idx = np.argmin(np.abs(valid - num_acs_lines)) 134 | num_acs_lines = valid[idx] 135 | 136 | return lookup[num_acs_lines, ncoils <= 8] 137 | 138 | 139 | if __name__ == '__main__': 140 | pass 141 | -------------------------------------------------------------------------------- /pygrappa/igrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the iGRAPPA algorithm.""" 2 | 3 | import numpy as np 4 | from tqdm import trange 5 | try: 6 | from skimage.metrics import mean_squared_error as compare_mse # pylint: disable=E0611,E0401 7 | except ImportError: 8 | from skimage.measure import compare_mse 9 | 10 | from pygrappa import mdgrappa 11 | 12 | 13 | def igrappa( 14 | kspace, calib, kernel_size=(5, 5), k: float = 0.3, coil_axis: int = -1, 15 | lamda: float = 0.01, ref=None, niter: int = 10, silent: bool = True, backend=mdgrappa): 16 | """Iterative GRAPPA. 17 | 18 | Parameters 19 | ---------- 20 | kspace : array_like 21 | 2D multi-coil k-space data to reconstruct from. Make sure 22 | that the missing entries have exact zeros in them. 23 | calib : array_like 24 | Calibration data (fully sampled k-space). 25 | kernel_size : tuple, optional 26 | Size of the 2D GRAPPA kernel (kx, ky). 27 | k : float, optional 28 | Regularization parameter for iterative reconstruction. Must 29 | be in the interval (0, 1). 30 | coil_axis : int, optional 31 | Dimension holding coil data. The other two dimensions should 32 | be image size: (sx, sy). 33 | lamda : float, optional 34 | Tikhonov regularization for the kernel calibration. 35 | ref : array_like or None, optional 36 | Reference k-space data. This is the true data that we are 37 | attempting to reconstruct. If provided, MSE at each 38 | iteration will be returned. If None, only reconstructed 39 | kspace is returned. 40 | niter : int, optional 41 | Number of iterations. 42 | silent : bool, optional 43 | Suppress messages to user. 44 | backend : callable 45 | GRAPPA function to use during each iteration. Default is 46 | ``pygrappa.mdgrappa``. 47 | 48 | Returns 49 | ------- 50 | res : array_like 51 | k-space data where missing entries have been filled in. 52 | mse : array_like, optional 53 | MSE at each iteration. Returned if ref not None. 54 | 55 | Raises 56 | ------ 57 | AssertionError 58 | If regularization parameter k is not in the interval (0, 1). 59 | 60 | Notes 61 | ----- 62 | More or less implements the iterative algorithm described in [1]. 63 | 64 | References 65 | ---------- 66 | .. [1] Zhao, Tiejun, and Xiaoping Hu. "Iterative GRAPPA (iGRAPPA) 67 | for improved parallel imaging reconstruction." Magnetic 68 | Resonance in Medicine: An Official Journal of the 69 | International Society for Magnetic Resonance in Medicine 70 | 59.4 (2008): 903-907. 71 | """ 72 | 73 | # Make sure k has a reasonable value 74 | assert 0 < k < 1, 'Parameter k should be in (0, 1)!' 75 | 76 | # Collect arguments to pass to the backend grappa function: 77 | grappa_args = { 78 | 'kernel_size': kernel_size, 79 | 'coil_axis': -1, 80 | 'lamda': lamda, 81 | # 'silent': silent 82 | } 83 | 84 | # Put the coil dimension at the end 85 | kspace = np.moveaxis(kspace, coil_axis, -1) 86 | calib = np.moveaxis(calib, coil_axis, -1) 87 | kx, ky, _nc = kspace.shape[:] 88 | cx, cy, _nc = calib.shape[:] 89 | kx2, ky2 = int(kx/2), int(ky/2) 90 | cx2, cy2 = int(cx/2), int(cy/2) 91 | adjcx = np.mod(cx, 2) 92 | adjcy = np.mod(cy, 2) 93 | 94 | # Save original type and convert to complex double 95 | # or else Cython will flip the heck out 96 | tipe = kspace.dtype 97 | kspace = kspace.astype(np.complex128) 98 | calib = calib.astype(np.complex128) 99 | 100 | # Initial conditions 101 | kIm, W = backend(kspace, calib, ret_weights=True, **grappa_args) 102 | ax = (0, 1) 103 | Im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 104 | kIm, axes=ax), axes=ax), axes=ax) 105 | Fp = 1e6 # some large number to begin with 106 | 107 | # If user has provided reference, let's track the MSE 108 | if ref is not None: 109 | mse = np.zeros(niter) 110 | aref = np.abs(ref) 111 | 112 | # Turn off tqdm if running silently 113 | if silent: 114 | range_fun = range 115 | else: 116 | def range_fun(x): 117 | return trange(x, leave=False, desc='iGRAPPA') 118 | 119 | # Fixed number of iterations 120 | for ii in range_fun(niter): 121 | 122 | # Update calibration region -- now includes all estimated 123 | # lines plus unchanged calibration region 124 | calib0 = kIm.copy() 125 | calib0[kx2-cx2:kx2+cx2+adjcx, ky2-cy2:ky2+cy2+adjcy, :] = calib.copy() 126 | 127 | kTm, Wn = backend( 128 | kspace, calib0, ret_weights=True, **grappa_args) 129 | Tm = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 130 | kTm, axes=ax), axes=ax), axes=ax) 131 | 132 | # Estimate relative image intensity change 133 | l1_Tm = np.linalg.norm(Tm.flatten(), ord=1) 134 | l1_Im = np.linalg.norm(Im.flatten(), ord=1) 135 | Tp = np.abs(l1_Tm - l1_Im)/l1_Im 136 | 137 | # if there's no change, end 138 | if not Tp: 139 | break 140 | 141 | # Update weights 142 | p = Tp/(k*Fp) 143 | if p < 1: 144 | # Take this reconstruction and new weights 145 | Im = Tm 146 | kIm = kTm 147 | W = Wn 148 | else: 149 | # Modify weights to get new reconstruction 150 | p = 1/p 151 | for key in Wn: 152 | W[key] = (1 - p)*Wn[key] + p*W[key] 153 | 154 | # Need to be able to supply grappa with weights to use! 155 | kIm = backend(kspace, calib0, weights=W, **grappa_args) 156 | Im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 157 | kIm, axes=ax), axes=ax), axes=ax) 158 | 159 | # Update Fp 160 | Fp = Tp 161 | 162 | # Track MSE 163 | if ref is not None: 164 | mse[ii] = compare_mse(aref, np.abs(kIm)) 165 | 166 | # Return the reconstructed kspace and MSE if ref kspace provided, 167 | # otherwise, just return reconstruction 168 | kIm = np.moveaxis(kIm, -1, coil_axis) 169 | if ref is not None: 170 | return kIm.astype(tipe), mse 171 | return kIm.astype(tipe) 172 | -------------------------------------------------------------------------------- /pygrappa/kernels.py: -------------------------------------------------------------------------------- 1 | """Machine learning kernel functions.""" 2 | 3 | import numpy as np 4 | # from sklearn.preprocessing import PolynomialFeatures 5 | 6 | 7 | def polynomial_kernel(X, cross_term_neighbors: int = 2): 8 | """Computes polynomial kernel. 9 | 10 | Parameters 11 | ---------- 12 | X : array_like of shape (sx, sy, nc) 13 | Features to map to high dimensional feature-space. 14 | 15 | """ 16 | 17 | _sx, _sy, nc = X.shape[:] 18 | 19 | # Build up a new coil set 20 | res = [] 21 | 22 | # coil layer 23 | res.append(X*np.sqrt(2)) 24 | 25 | # This produces a strong PSF overlay on the recon: 26 | # # 1s layer 27 | # res.append(np.ones((_sx, _sy, 1))) 28 | 29 | # squared-coil layer 30 | res.append(np.abs(X)**2) 31 | 32 | # Cross term layer 33 | for ii in range(nc): 34 | for jj in range(nc): 35 | if ii == jj: 36 | continue 37 | if np.abs(ii - jj) > cross_term_neighbors: 38 | continue 39 | res.append( 40 | (X[..., ii]*np.conj(X[..., jj])*np.sqrt(2))[ 41 | ..., None]) 42 | 43 | return np.concatenate(res, axis=-1) 44 | # 45 | # # Real/Imag? 46 | # R = np.reshape(X.real, (-1, nc)) 47 | # I = np.reshape(X.imag, (-1, nc)) 48 | # poly_r = PolynomialFeatures(degree=2, include_bias=False) 49 | # poly_i = PolynomialFeatures(degree=2, include_bias=False) 50 | # res_r = poly_r.fit_transform(R) 51 | # res_i = poly_i.fit_transform(I) 52 | # 53 | # # Filter out cross term neighbors 54 | # print(np.sum(poly_r.powers_, axis=1)) 55 | # 56 | # # print(poly_r.powers_) 57 | # return np.reshape(res_r + 1j*res_i, (_sx, _sy, -1)) 58 | -------------------------------------------------------------------------------- /pygrappa/kspa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the kSPA algorithm.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | from scipy.spatial import cKDTree # pylint: disable=E0611 7 | from scipy.sparse import lil_matrix 8 | from scipy.interpolate import griddata 9 | 10 | 11 | def kspa( 12 | kx, ky, k, sens, coil_axis: int = -1, sens_coil_axis: int = -1): 13 | """Recon for arbitrary trajectories using k‐space sparse matrices. 14 | 15 | Parameters 16 | ---------- 17 | 18 | Returns 19 | ------- 20 | 21 | Notes 22 | ----- 23 | 24 | References 25 | ---------- 26 | .. [1] Liu, Chunlei, Roland Bammer, and Michael E. Moseley. 27 | "Parallel imaging reconstruction for arbitrary 28 | trajectories using k‐space sparse matrices (kSPA)." 29 | Magnetic Resonance in Medicine: An Official Journal of the 30 | International Society for Magnetic Resonance in Medicine 31 | 58.6 (2007): 1171-1181. 32 | """ 33 | 34 | # Move coils to the back 35 | sens = np.moveaxis(sens, sens_coil_axis, -1) 36 | k = np.moveaxis(k, coil_axis, -1) 37 | sx, sy, nc = sens.shape[:] 38 | 39 | # Create a k-d tree 40 | kxy = np.concatenate((kx[:, None], ky[:, None]), axis=-1) 41 | kdtree = cKDTree(kxy) 42 | 43 | # Find spectrum of coil sensitivities interpolated at acquired 44 | # points (kx, ky) 45 | ax = (0, 1) 46 | ksens = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 47 | sens, axes=ax), axes=ax), axes=ax)/np.sqrt(sx*sy) 48 | 49 | # Find kxy that satisfy || k_mu - k_rho ||_2 <= ws 50 | tx, ty = np.meshgrid( 51 | np.linspace(np.min(kx), np.max(kx), sx), 52 | np.linspace(np.min(ky), np.max(ky), sy)) 53 | tx, ty = tx.flatten(), ty.flatten() 54 | txy = np.concatenate((tx[:, None], ty[:, None]), axis=-1) 55 | 56 | # Build G 57 | nk, nc = k.shape[:] 58 | G = lil_matrix((nk*nc, tx.size), dtype=k.dtype) 59 | 60 | t0 = time() 61 | Ginterp = {} 62 | idx = {} 63 | sx2, sy2 = int(sx/2), int(sy/2) 64 | for ii in range(tx.size): 65 | for jj in range(nc): 66 | 67 | if jj not in idx: 68 | # Choose ws cutoff frequency to be when ksens 69 | # decreases to around %0.36 of its peak value 70 | ll = np.abs(ksens[sx2, sy2:, jj]) 71 | p = np.max(ll)*0.0036 72 | ws = np.argmin(np.abs(ll - p)) 73 | idx[jj] = kdtree.query_ball_point(txy, r=ws) 74 | 75 | if jj not in Ginterp: 76 | Ginterp[jj] = griddata( 77 | (tx, ty), ksens[..., jj].flatten(), (kx, ky), 78 | method='cubic') 79 | 80 | # Stick interpolated values in for this coil 81 | idx0 = np.array(idx[jj][ii]) + jj*nk 82 | G[idx0, ii] = Ginterp[jj][idx[jj][ii]] 83 | 84 | print('Took %g seconds to build G' % (time() - t0)) 85 | 86 | # import matplotlib.pyplot as plt 87 | # plt.imshow(np.abs(G).todense()) 88 | # plt.show() 89 | 90 | d = k.flatten('F') 91 | m = np.linalg.pinv(G.todense()) @ d 92 | return np.reshape(m, (sx, sy)) 93 | -------------------------------------------------------------------------------- /pygrappa/meson.build: -------------------------------------------------------------------------------- 1 | 2 | py3.install_sources([ 3 | '__init__.py', 4 | 'cgsense.py', 5 | 'coils.py', 6 | 'find_acs.py', 7 | 'gfactor.py', 8 | 'grappa.py', 9 | 'grappaop.py', 10 | 'grog.py', 11 | 'hpgrappa.py', 12 | 'igrappa.py', 13 | 'kernels.py', 14 | 'kspa.py', 15 | 'lustig_grappa.py', 16 | 'mdgrappa.py', 17 | 'ncgrappa.py', 18 | 'nlgrappa.py', 19 | 'nlgrappa_matlab.py', 20 | 'pars.py', 21 | 'pruno.py', 22 | 'radialgrappaop.py', 23 | 'run_tests.py', 24 | 'seggrappa.py', 25 | 'sense1d.py', 26 | 'slicegrappa.py', 27 | 'splitslicegrappa.py', 28 | 'tgrappa.py', 29 | 'ttgrappa.py', 30 | 'vcgrappa.py' 31 | ], 32 | subdir: 'pygrappa' 33 | ) 34 | 35 | subdir('src') 36 | subdir('examples') 37 | subdir('utils') 38 | subdir('tests') 39 | subdir('benchmarks') 40 | -------------------------------------------------------------------------------- /pygrappa/ncgrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of Non-Cartesian GRAPPA.""" 2 | 3 | import numpy as np 4 | from scipy.spatial import cKDTree # pylint: disable=E0611 5 | 6 | 7 | def ncgrappa(kx, ky, k, cx, cy, calib, kernel_size, coil_axis: int = -1): 8 | """Non-Cartesian GRAPPA. 9 | 10 | Parameters 11 | ---------- 12 | kx, ky : array_like 13 | k-space coordinates of kspace data, k. kx and ky are 1D 14 | arrays. 15 | k : array_like 16 | Complex kspace data corresponding the measurements at 17 | locations kx, ky. k has two dimensions: data and coil. The 18 | coil dimension will be assumed to be last unless coil_axis=0. 19 | Unsampled points should be exactly 0. 20 | calib : array_like 21 | 22 | kernel_size : float 23 | Radius of kernel. 24 | coil_axis : int, optional 25 | Dimension of calib holding coil data. 26 | 27 | Notes 28 | ----- 29 | Implements to the algorithm described in [1]_. 30 | 31 | References 32 | ---------- 33 | .. [1] Luo, Tianrui, et al. "A GRAPPA algorithm for arbitrary 34 | 2D/3D non‐Cartesian sampling trajectories with rapid 35 | calibration." Magnetic resonance in medicine 82.3 (2019): 36 | 1101-1112. 37 | """ 38 | 39 | # Assume k has coil at end unless user says it's upfront. We 40 | # want to assume that coils are in the back of calib and k: 41 | # calib = np.moveaxis(calib, coil_axis, -1) 42 | # if coil_axis == 0: 43 | # k = np.moveaxis(k, coil_axis, -1) 44 | 45 | # Find all sampled and unsampled points 46 | mask = np.abs(k[:, 0]) > 0 47 | idx_unsampled = np.argwhere(~mask).squeeze() 48 | idx_sampled = np.argwhere(mask).squeeze() 49 | 50 | # Identify all the constellations for calibration using the 51 | # sampled kspace points 52 | kxy = np.concatenate((kx[:, None], ky[:, None]), axis=-1) 53 | kdtree = cKDTree(kxy[idx_sampled, :]) 54 | 55 | # For each un‐sampled k‐space point, query the kd‐tree with the 56 | # prescribed distance (i.e., GRAPPA kernel size) 57 | constellations = kdtree.query_ball_point( 58 | kxy[idx_unsampled, :], r=kernel_size) 59 | 60 | # # Look at one to make sure we're doing what we think we're doing 61 | # import matplotlib.pyplot as plt 62 | # c = 500 63 | # plt.scatter( 64 | # kx[idx_sampled][constellations[c]], 65 | # ky[idx_sampled][constellations[c]]) 66 | # plt.plot(kx[idx_unsampled[c]], ky[idx_unsampled[c]], 'r.') 67 | # plt.show() 68 | 69 | # Make an interpolator for the calibration data 70 | from scipy.interpolate import CloughTocher2DInterpolator 71 | cxy = np.concatenate((cx[:, None], cy[:, None]), axis=-1) 72 | f = CloughTocher2DInterpolator(cxy, calib) 73 | 74 | # For each constellation, let's train weights and fill in a hole 75 | T = f([0, 0]).squeeze() 76 | for ii, con in enumerate(constellations): 77 | Txy = kxy[idx_unsampled[ii]] 78 | Sxy = kxy[idx_sampled][con] 79 | Pxy = Sxy - Txy 80 | 81 | S = f(Pxy) 82 | print(S.shape, T.shape) 83 | 84 | # T = W S 85 | # (8) = (1, 24) @ (24, 8) 86 | TSh = T @ S.conj().T 87 | SSh = S @ S.conj().T 88 | W = np.linalg.solve(SSh, TSh) 89 | print(T) 90 | print(W @ S) 91 | 92 | assert False 93 | 94 | # # Now we need to find all the unique constellations 95 | # P = dict() 96 | # for ii, con in enumerate(constellations): 97 | # T = kxy[idx_unsampled[ii]] 98 | # S = kxy[idx_sampled][con] 99 | # 100 | # # Move everything to be relative to the target 101 | # P0 = S - T 102 | 103 | # # Try to find the existing constellation 104 | # key = P0.tostring() 105 | # if key in P: 106 | # P[key].append(ii) 107 | # else: 108 | # P[key] = [ii] 109 | # 110 | # if ii == 500: 111 | # import matplotlib.pyplot as plt 112 | # plt.scatter(S[:, 0], S[:, 1]) 113 | # plt.plot(T[0], T[1], 'r.') 114 | # plt.show() 115 | # 116 | # keys = list(P.keys()) 117 | # print(len(keys)) 118 | -------------------------------------------------------------------------------- /pygrappa/nlgrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of Non-Linear GRAPPA.""" 2 | 3 | from functools import partial 4 | 5 | import numpy as np 6 | from pygrappa import mdgrappa 7 | from pygrappa.kernels import polynomial_kernel 8 | 9 | 10 | def nlgrappa( 11 | kspace, calib, kernel_size=(5, 5), ml_kernel: str = "polynomial", 12 | ml_kernel_args: dict = None, coil_axis: int = -1): 13 | """NL-GRAPPA. 14 | 15 | Parameters 16 | ---------- 17 | kspace : array_like 18 | calib : array_like 19 | kernel_size : tuple of int, optional 20 | ml_kernel : { 21 | 'linear', 'polynomial', 'sigmoid', 'rbf', 22 | 'laplacian', 'chi2'}, optional 23 | Kernel functions modeled on scikit-learn metrics.pairwise 24 | module but which can handle complex-valued inputs. 25 | ml_kernel_args : dict or None, optional 26 | Arguments to pass to kernel functions. 27 | coil_axis : int, optional 28 | Axis holding the coil data. 29 | 30 | Returns 31 | ------- 32 | res : array_like 33 | Reconstructed k-space. 34 | 35 | Notes 36 | ----- 37 | Implements the algorithm described in [1]_. 38 | 39 | Bias term is removed from polynomial kernel as it adds a PSF-like 40 | overlay onto the reconstruction. 41 | 42 | Currently only `polynomial` method is implemented. 43 | 44 | References 45 | ---------- 46 | .. [1] Chang, Yuchou, Dong Liang, and Leslie Ying. "Nonlinear 47 | GRAPPA: A kernel approach to parallel MRI reconstruction." 48 | Magnetic resonance in medicine 68.3 (2012): 730-740. 49 | """ 50 | 51 | raise NotImplementedError("NL-GRAPPA is not currently working!") 52 | 53 | # Coils to the back 54 | kspace = np.moveaxis(kspace, coil_axis, -1) 55 | calib = np.moveaxis(calib, coil_axis, -1) 56 | _kx, _ky, nc = kspace.shape[:] 57 | 58 | # Get the correct kernel: 59 | _phi = { 60 | # 'linear': linear_kernel, 61 | 'polynomial': polynomial_kernel, 62 | # 'sigmoid': sigmoid_kernel, 63 | # 'rbf': rbf_kernel, 64 | # 'laplacian': laplacian_kernel, 65 | # 'chi2': chi2_kernel, 66 | }[ml_kernel] 67 | 68 | # Get default args if none were passed in 69 | if ml_kernel_args is None: 70 | ml_kernel_args = { 71 | 'cross_term_neighbors': 1, 72 | } 73 | 74 | # Pass arguments to kernel function 75 | phi = partial(_phi, **ml_kernel_args) 76 | 77 | # Get the extra "virtual" channels using kernel function, phi 78 | vkspace = phi(kspace) 79 | vcalib = phi(calib) 80 | 81 | # Pass onto cgrappa for the heavy lifting 82 | return np.moveaxis( 83 | mdgrappa( 84 | vkspace, vcalib, kernel_size=kernel_size, coil_axis=-1, 85 | nc_desired=nc, lamda=0), 86 | -1, coil_axis) 87 | -------------------------------------------------------------------------------- /pygrappa/pars.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the PARS algorithm.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | from scipy.spatial import cKDTree # pylint: disable=E0611 7 | from scipy.ndimage import zoom 8 | from tqdm import tqdm 9 | 10 | 11 | def pars( 12 | kx, ky, k, sens, tx=None, ty=None, kernel_size: int = 25, 13 | kernel_radius: float = None, coil_axis: int = -1): 14 | """Parallel MRI with adaptive radius in k‐space. 15 | 16 | Parameters 17 | ---------- 18 | kx, ky : array_like 19 | Sample points in kspace corresponding to measurements k. 20 | kx, kx are 1D arrays. 21 | k : array_like 22 | Complex kspace coil measurements corresponding to points 23 | (kx, ky). 24 | sens : array_like 25 | Coil sensitivity maps with shape of desired reconstruction. 26 | tx, ty : array_like 27 | Sample points in kspace defining the grid of ifft2(sens). 28 | If None, then tx, ty will be generated from a meshgrid with 29 | endpoints [min(kx), max(kx), min(ky), max(ky)]. 30 | kernel_size : int, optional 31 | Number of nearest neighbors to use when interpolating kspace. 32 | kernel_radius : float, optional 33 | Raidus in kspace (units same as (kx, ky)) to select neighbors 34 | when training kernels. 35 | coil_axis : int, optional 36 | Dimension holding coil data. 37 | 38 | Returns 39 | ------- 40 | res : array_like 41 | Reconstructed image space on a Cartesian grid with the same 42 | shape as sens. 43 | 44 | Notes 45 | ----- 46 | Implements the algorithm described in [1]_. 47 | 48 | Using kernel_radius seems to perform better than kernel_size. 49 | 50 | References 51 | ---------- 52 | .. [1] Yeh, Ernest N., et al. "3Parallel magnetic resonance 53 | imaging with adaptive radius in k‐space (PARS): 54 | Constrained image reconstruction using k‐space locality in 55 | radiofrequency coil encoded data." Magnetic Resonance in 56 | Medicine: An Official Journal of the International Society 57 | for Magnetic Resonance in Medicine 53.6 (2005): 1383-1392. 58 | """ 59 | 60 | # Move coil axis to the back 61 | k = np.moveaxis(k, coil_axis, -1) 62 | sens = np.moveaxis(sens, coil_axis, -1) 63 | kxy = np.concatenate((kx[:, None], ky[:, None]), axis=-1) 64 | 65 | # Oversample the sensitivity maps by a factor of 2 66 | t0 = time() 67 | sensr = zoom(sens.real, (2, 2, 1), order=1) 68 | sensi = zoom(sens.imag, (2, 2, 1), order=1) 69 | sens = sensr + 1j*sensi 70 | print('Took %g seconds to upsample sens' % (time() - t0)) 71 | 72 | # We want to resample onto a Cartesian grid 73 | sx, sy, nc = sens.shape[:] 74 | if tx is None or ty is None: 75 | tx, ty = np.meshgrid( 76 | np.linspace(np.min(kx), np.max(kx), sx), 77 | np.linspace(np.min(ky), np.max(ky), sy)) 78 | tx, ty = tx.flatten(), ty.flatten() 79 | txy = np.concatenate((tx[:, None], ty[:, None]), axis=-1) 80 | 81 | # Make a kd-tree and find all point around targets 82 | t0 = time() 83 | kdtree = cKDTree(kxy) 84 | if kernel_radius is None: 85 | _, idx = kdtree.query(txy, k=kernel_size) 86 | else: 87 | idx = kdtree.query_ball_point(txy, r=kernel_radius) 88 | print('Took %g seconds to find nearest neighbors' % (time() - t0)) 89 | 90 | # Scale kspace coordinates to be within [-.5, .5] 91 | kxy0 = np.concatenate( 92 | (kx[:, None]/np.max(kx), ky[:, None]/np.max(ky)), axis=-1)/2 93 | txy0 = np.concatenate( 94 | (tx[:, None]/np.max(tx), ty[:, None]/np.max(ty)), axis=-1)/2 95 | 96 | # Encoding matrix is much too large to invert, so we'll go 97 | # kernel by kernel to grid/reconstruct kspace 98 | sens = np.reshape(sens, (-1, nc)) 99 | res = np.zeros(sens.shape, dtype=sens.dtype) 100 | t0 = time() 101 | for ii, idx0 in tqdm( 102 | enumerate(idx), total=idx.shape[0], leave=False, 103 | desc='PARS'): 104 | 105 | dk = kxy0[idx0, :] 106 | r = txy0[ii, :] 107 | 108 | # Create local encoding matrix and train weights 109 | E = np.exp(1j*(-dk @ r)) 110 | E = E[:, None]*sens[ii, :] 111 | W = sens[ii, :] @ E.conj().T @ np.linalg.pinv(E @ E.conj().T) 112 | 113 | # Grid the sample: 114 | res[ii, :] = W @ k[idx0, :] 115 | 116 | print('Took %g seconds to regrid' % (time() - t0)) 117 | 118 | # Return image at correct resolution with coil axis in right place 119 | ax = (0, 1) 120 | sx4 = int(sx/4) 121 | return np.moveaxis(np.fft.fftshift(np.fft.ifft2( 122 | np.fft.ifftshift(np.reshape(res, (sx, sy, nc), 'F'), axes=ax), 123 | axes=ax), axes=ax)[sx4:-sx4, sx4:-sx4, :], -1, coil_axis) 124 | -------------------------------------------------------------------------------- /pygrappa/pruno.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the PRUNO algorithm.""" 2 | 3 | import numpy as np 4 | from skimage.util import pad, view_as_windows 5 | from scipy.linalg import null_space 6 | from scipy.sparse.linalg import cg 7 | from scipy.sparse.linalg import LinearOperator 8 | from scipy.signal import convolve2d 9 | from scipy.signal import lfilter 10 | from tqdm import trange 11 | 12 | 13 | def pruno(kspace, calib, kernel_size=(5, 5), coil_axis: int = -1): 14 | """Parallel Reconstruction Using Null Operations (PRUNO). 15 | 16 | Parameters 17 | ---------- 18 | 19 | Returns 20 | ------- 21 | 22 | References 23 | ---------- 24 | .. [1] Zhang, Jian, Chunlei Liu, and Michael E. Moseley. 25 | "Parallel reconstruction using null operations." Magnetic 26 | resonance in medicine 66.5 (2011): 1241-1253. 27 | """ 28 | 29 | # Coils to da back 30 | kspace = np.moveaxis(kspace, coil_axis, -1) 31 | calib = np.moveaxis(calib, coil_axis, -1) 32 | nx, ny, _nc = kspace.shape[:] 33 | 34 | # Make a calibration matrix 35 | kx, ky = kernel_size[:] 36 | kx2, ky2 = int(kx/2), int(ky/2) 37 | nc = calib.shape[-1] 38 | 39 | # Pad and pull out calibration matrix 40 | kspace = pad( # pylint: disable=E1102 41 | kspace, ((kx2, kx2), (ky2, ky2), (0, 0)), mode='constant') 42 | calib = pad( # pylint: disable=E1102 43 | calib, ((kx2, kx2), (ky2, ky2), (0, 0)), mode='constant') 44 | C = view_as_windows( 45 | calib, (kx, ky, nc)).reshape((-1, kx*ky*nc)) 46 | 47 | # Get the nulling kernels 48 | n = null_space(C, rcond=1e-3) # TODO: automate selection of rcond 49 | print(n.shape) 50 | 51 | # Calculate composite kernels 52 | # TODO: not sure if this is doing the right thing... 53 | n = np.reshape(n, (-1, nc, n.shape[-1])) 54 | nconj = np.conj(n).T 55 | eta = np.zeros((nc, nc, n.shape[0]*2 - 1), dtype=kspace.dtype) 56 | print(n.shape) 57 | for ii in trange(nc): 58 | for jj in range(nc): 59 | eta[ii, jj, :] = np.sum(convolve2d( 60 | nconj[:, ii, :], n[:, jj, :], mode='full'), -1) 61 | print(eta.shape) 62 | 63 | # Solve for b (setting up Ax = b): 64 | # b = -Im @ NhN @ Ia @ d 65 | # Treat NhN as a filer using composite kernels as weights. 66 | # TODO: include ACS 67 | # TODO: Not sure if this is doing the right thing... 68 | b = np.zeros((np.prod(kspace.shape[:2]), nc), dtype=kspace.dtype) 69 | for ii in range(nc): 70 | res = np.zeros(b.shape[0], dtype=b.dtype) 71 | for jj in range(nc): 72 | res += lfilter( 73 | eta[ii, jj, :], a=1, x=kspace[..., jj].flatten()) 74 | b[:, ii] = res 75 | Im = (np.abs(kspace[..., 0]) == 0).flatten() 76 | b = -1*b*Im[..., None] 77 | b = b.flatten() 78 | 79 | # import matplotlib.pyplot as plt 80 | # b = np.reshape(b[:, 1], kspace.shape[:2]) 81 | # plt.imshow(np.abs(b)) 82 | # # im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 83 | # # b, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 84 | # # plt.imshow(np.abs(im)) 85 | # plt.show() 86 | 87 | # Initial guess is zeros 88 | # TODO: include ACS 89 | x0 = np.zeros(kspace.size, dtype=kspace.dtype) 90 | 91 | # Conjugate gradient iterations to solve. To use scipy's CG 92 | # solver, must be in the form Ax = b, A is a linear operator. 93 | # Need to do some stuff to create A since nulling operation 94 | # is filtering 95 | nx, ny = kspace.shape[:2] 96 | 97 | def _mv(v): 98 | v = np.reshape(v, (-1, nc)) 99 | res = np.zeros((v.shape[0], nc), dtype=v.dtype) 100 | for ii in range(nc): 101 | for jj in range(nc): 102 | res[..., ii] += lfilter( 103 | eta[ii, jj, :], a=1, x=v[:, jj]) 104 | res = res*Im[..., None] 105 | return res.flatten() 106 | 107 | A = LinearOperator( 108 | dtype=kspace.dtype, shape=(nc*nx*ny, nc*nx*ny), matvec=_mv) 109 | 110 | d, info = cg(A, b, x0=x0, maxiter=100) 111 | print(info) 112 | print(d.shape) 113 | 114 | return np.moveaxis(np.reshape(d, (nx, ny, nc)), -1, coil_axis) 115 | 116 | 117 | if __name__ == '__main__': 118 | 119 | import matplotlib.pyplot as plt 120 | from phantominator import shepp_logan 121 | from pygrappa.utils import gaussian_csm 122 | 123 | N, nc = 128, 8 124 | ph = shepp_logan(N)[..., None]*gaussian_csm(N, N, nc) 125 | kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 126 | ph, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 127 | 128 | # Get calibration region (20x20) 129 | pd = 10 130 | ctr = int(N/2) 131 | calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() 132 | 133 | # undersample by a factor of 2 in both kx and ky 134 | kspace[::2, 1::2, :] = 0 135 | kspace[1::2, ::2, :] = 0 136 | 137 | res = pruno(kspace, calib) 138 | 139 | im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 140 | res, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 141 | sos = np.sqrt(np.sum(np.abs(im)**2, axis=-1)) 142 | plt.imshow(sos) 143 | plt.show() 144 | -------------------------------------------------------------------------------- /pygrappa/radialgrappaop.py: -------------------------------------------------------------------------------- 1 | """Python implementation of Radial GRAPPA operator.""" 2 | 3 | import numpy as np 4 | from scipy.linalg import expm, logm 5 | 6 | 7 | def radialgrappaop( 8 | kx, ky, k, nspokes: int = None, spoke_axis: int = -2, coil_axis: int = -1, 9 | spoke_axis_coord: int = -1, lamda: float = 0.01, ret_lGtheta: bool = False, 10 | traj_warn: bool = True): 11 | """Non-Cartesian Radial GRAPPA operator. 12 | 13 | Parameters 14 | ---------- 15 | kx, ky: array_like 16 | k-space coordinates of kspace data, k. kx and ky are 2D 17 | arrays containing (sx, nr) : (number of samples along ray, 18 | number of rays). 19 | k : array_like 20 | Complex kspace data corresponding to the measurements at 21 | locations kx, ky. k has three dimensions: sx, nr, and coil. 22 | nspokes : int, optional 23 | Number of spokes. Used when (kx, ky) and k are given with 24 | flattened sample and spoke axes, i.e., (sx*nr, nc). 25 | spoke_axis : int, optional 26 | Axis of k that contains the spoke data. Not for kx, ky: see 27 | spoke_axis_coord to specify spoke axis for kx and ky. 28 | coil_axis : int, optional 29 | Axis of k that contains the coil data. 30 | spoke_axis_coord : int, optional 31 | Axis of kx and ky that hold the spoke data. 32 | lamda : float, optional 33 | Tikhonov regularization term used both for fitting Gtheta 34 | and log(Gx), log(Gy). 35 | ret_lGtheta : bool, optional 36 | Return log(Gtheta) instead of Gx, Gy. 37 | traj_warn : bool, optional 38 | Warn about potential inconsistencies in trajectory, e.g., 39 | not shaped correctly. 40 | 41 | Returns 42 | ------- 43 | Gx, Gy : array_like 44 | GRAPPA operators along the x and y axes. 45 | 46 | Raises 47 | ------ 48 | AssertionError 49 | If kx and ky do not have spokes along spoke_axis_coord or if 50 | the standard deviation of distance between spoke points is 51 | greater than or equal to 1e-10. 52 | 53 | Notes 54 | ----- 55 | Implements the radial training scheme for self calibrating GRAPPA 56 | operators in [1]_. Too many coils could lead to instability of 57 | matrix exponents and logarithms -- use PCA or other suitable 58 | coil combination technique to reduce dimensionality if needed. 59 | 60 | References 61 | ---------- 62 | .. [1] Seiberlich, Nicole, et al. "Self‐calibrating GRAPPA 63 | operator gridding for radial and spiral trajectories." 64 | Magnetic Resonance in Medicine: An Official Journal of the 65 | International Society for Magnetic Resonance in Medicine 66 | 59.4 (2008): 930-935. 67 | """ 68 | 69 | # Move coils and spoke_axis to the back: 70 | if k.ndim == 2: 71 | # assume we only have a coil axis 72 | k = np.moveaxis(k, coil_axis, -1) 73 | else: 74 | k = np.moveaxis(k, (spoke_axis, coil_axis), (-2, -1)) 75 | kx = np.moveaxis(kx, spoke_axis_coord, -1) 76 | ky = np.moveaxis(ky, spoke_axis_coord, -1) 77 | 78 | if k.ndim == 2 and nspokes is not None: 79 | nc = k.shape[-1] 80 | k = np.reshape(k, (-1, nspokes, nc)) 81 | sx, nr, nc = k.shape[:] 82 | 83 | if kx.ndim == 1 and nspokes is not None: 84 | kx = np.reshape(kx, (sx, nr)) 85 | ky = np.reshape(ky, (sx, nr)) 86 | 87 | # We can do a sanity check to make sure we do indeed have rays. 88 | # We should have very little variation in dx, dy along each ray: 89 | if traj_warn: 90 | tol = 1e-5 if kx.dtype == np.float32 else 1e-10 91 | assert np.all(np.std(np.diff(kx, axis=0), axis=0) < tol) 92 | assert np.all(np.std(np.diff(ky, axis=0), axis=0) < tol) 93 | 94 | # We need sources (last source has no target!) and targets (first 95 | # target has no associated source!) 96 | S = k[:-1, ...] 97 | T = k[1:, ...] 98 | 99 | # We need a single GRAPPA operator to relate sources and 100 | # targets for each spoke. We'll call it lGtheta. Loop through 101 | # all rays -- maybe a way to do this without for loop? 102 | lGtheta = np.zeros((nr, nc, nc), dtype=k.dtype) 103 | for ii in range(nr): 104 | Sh = S[:, ii, :].conj().T 105 | ShS = Sh @ S[:, ii, :] 106 | ShT = Sh @ T[:, ii, :] 107 | lamda0 = lamda*np.linalg.norm(ShS)/ShS.shape[0] 108 | res = np.linalg.solve( 109 | ShS + lamda0*np.eye(ShS.shape[0]), ShT) 110 | lGtheta[ii, ...] = logm(res) 111 | 112 | # If the user only asked for the lGthetas, give them back! 113 | if ret_lGtheta: 114 | return lGtheta 115 | 116 | # Otherwise, we now need Gx, Gy. 117 | # Some implementations I've seen of this assume the same interval 118 | # always along a single ray, i.e.: 119 | # dx = kx[1, :] - kx[0, :] 120 | # dy = ky[1, :] - ky[0, :] 121 | # I'm going to assume they are similar and take the average: 122 | dx = np.mean(np.diff(kx, axis=0), axis=0) 123 | dy = np.mean(np.diff(ky, axis=0), axis=0) 124 | dxy = np.concatenate((dx[:, None], dy[:, None]), axis=1) 125 | 126 | # Let's solve this equation: 127 | # lGtheta = dxy @ lGxy 128 | # (nr, nc^2) = (nr, 2) @ (2, nc^2) 129 | # dxy.T lGtheta = dxy.T @ dxy @ lGxy 130 | # (dxy.T @ dxy)^-1 @ dxy.T lGtheta = lGxy 131 | lGtheta = np.reshape(lGtheta, (nr, nc**2), 'F') 132 | RtR = dxy.T @ dxy 133 | RtG = dxy.T @ lGtheta 134 | lamda0 = lamda*np.linalg.norm(RtR)/RtR.shape[0] 135 | res = np.linalg.solve(RtR + lamda0*np.eye(RtR.shape[0]), RtG) 136 | lGx = np.reshape(res[0, :], (nc, nc)) 137 | lGy = np.reshape(res[1, :], (nc, nc)) 138 | 139 | # Take matrix exponential to get from (lGx, lGy) -> (Gx, Gy) 140 | # and we're done! 141 | return expm(lGx), expm(lGy) 142 | -------------------------------------------------------------------------------- /pygrappa/run_tests.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | 4 | import pytest 5 | 6 | 7 | if __name__ == "__main__": 8 | this_dir = pathlib.Path(__file__).parent.resolve() 9 | retcode = pytest.main([f"{this_dir / 'tests'}"]) 10 | sys.exit(retcode) 11 | -------------------------------------------------------------------------------- /pygrappa/seggrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of the Segmented GRAPPA algorithm.""" 2 | 3 | import numpy as np 4 | 5 | from pygrappa import mdgrappa 6 | 7 | 8 | def seggrappa(kspace, calibs, *args, **kwargs): 9 | """Segmented GRAPPA. 10 | 11 | See pygrappa.grappa() for full list of arguments. 12 | 13 | Parameters 14 | ---------- 15 | calibs : list of array_like 16 | List of calibration regions. 17 | 18 | Notes 19 | ----- 20 | A generalized implementation of the method described in [1]_. 21 | Multiple ACS regions can be supplied to function. GRAPPA is run 22 | for each ACS region and then averaged to produce the final 23 | reconstruction. 24 | 25 | References 26 | ---------- 27 | .. [1] Park, Jaeseok, et al. "Artifact and noise suppression in 28 | GRAPPA imaging using improved k‐space coil calibration and 29 | variable density sampling." Magnetic Resonance in 30 | Medicine: An Official Journal of the International Society 31 | for Magnetic Resonance in Medicine 53.1 (2005): 186-193. 32 | """ 33 | 34 | # Do the reconstruction for each of the calibration regions 35 | recons = [mdgrappa(kspace, c, *args, **kwargs) for c in calibs] 36 | 37 | # Average all the reconstructions 38 | return np.mean(recons, axis=0) 39 | -------------------------------------------------------------------------------- /pygrappa/sense1d.py: -------------------------------------------------------------------------------- 1 | """Python implementation of SENSE.""" 2 | 3 | from time import time 4 | 5 | import numpy as np 6 | 7 | 8 | def sense1d(im, sens, Rx: int = 1, Ry: int = 1, coil_axis: int = -1, imspace: bool = True): 9 | """Sensitivity Encoding for Fast MRI (SENSE) along one dimension. 10 | 11 | Parameters 12 | ---------- 13 | im : array_like 14 | Array of the aliased 2D multicoil coil image. If 15 | imspace=False, im is the undersampled k-space data. 16 | sens : array_like 17 | Complex coil sensitivity maps with the same dimensions as im. 18 | Rx, Ry : ints, optional 19 | Acceleration factor in x and y. One of Rx, Ry must be 1. If 20 | both are 1, then this is Roemer's optimal coil combination. 21 | coil_axis : int, optional 22 | Dimension holding coil data. 23 | imspace : bool, optional 24 | If im is image space or k-space data. 25 | 26 | Returns 27 | ------- 28 | res : array_like 29 | Unwrapped single coil reconstruction. 30 | 31 | Notes 32 | ----- 33 | Implements the algorithm first described in [1]_. This 34 | implementation is based on the MATLAB tutorial found in [2]_. 35 | 36 | This implementation handles only regular undersampling along a 37 | single dimension. Arbitrary undersampling is not supported by 38 | this function. 39 | 40 | Odd Rx, Ry seem to behave strangely, i.e. not as well as even 41 | factors. Right now I'm padding im and sens by 1 and removing at 42 | end. 43 | 44 | References 45 | ---------- 46 | .. [1] Pruessmann, Klaas P., et al. "SENSE: sensitivity encoding 47 | for fast MRI." Magnetic Resonance in Medicine: An Official 48 | Journal of the International Society for Magnetic 49 | Resonance in Medicine 42.5 (1999): 952-962. 50 | .. [2] https://users.fmrib.ox.ac.uk/~mchiew/docs/ 51 | SENSE_tutorial.html 52 | """ 53 | 54 | # We can only handle unwrapping one dimension: 55 | assert Rx == 1 or Ry == 1, 'One of Rx, Ry must be 1!' 56 | 57 | # Coils to da back 58 | im = np.moveaxis(im, coil_axis, -1) 59 | sens = np.moveaxis(sens, coil_axis, -1) 60 | 61 | # Assume the first dimension has the unwrapping, so move the 62 | # axis we want to operate on to the front 63 | flip_xy = False 64 | if Ry > 1: 65 | flip_xy = True 66 | im = np.moveaxis(im, 0, 1) 67 | sens = np.moveaxis(sens, 0, 1) 68 | Rx, Ry = Ry, Rx 69 | 70 | # Put kspace into image space if needed 71 | if not imspace: 72 | im = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 73 | im, axes=(0, 1)), axes=(0, 1)), axes=(0, 1)) 74 | 75 | # If undersampling factor is odd, pad the image 76 | if Rx % 2 > 0: 77 | im = np.pad(im, ((0, 1), (0, 0), (0, 0))) 78 | sens = np.pad(sens, ((0, 1), (0, 0), (0, 0))) 79 | 80 | nx, ny, _nc = im.shape[:] 81 | res = np.zeros((nx, ny), dtype=im.dtype) 82 | 83 | # loop over the top 1/R of the image, use einsum to get all the 84 | # inner loops where the subproblems are extracted and solved 85 | # in the least squares sense 86 | t0 = time() 87 | for x in range(int(nx/Rx)): 88 | x_idx = np.arange(x, nx, step=int(nx/Rx)) 89 | S = sens[x_idx, ...].transpose((1, 2, 0)) 90 | 91 | # Might be more efficient way then explicit pinv along axis? 92 | res[x_idx, :] = np.einsum( 93 | 'ijk,ik->ij', np.linalg.pinv(S), im[x, ...]).T 94 | print('Took %g sec for unwrapping' % (time() - t0)) 95 | 96 | # Remove pad if Rx is odd 97 | if Rx % 2 > 0: 98 | res = res[:-1, ...] 99 | 100 | # Put all the axes back where the user had them 101 | if flip_xy: 102 | res = np.moveaxis(res, 1, 0) 103 | return np.moveaxis(res, -1, coil_axis) 104 | -------------------------------------------------------------------------------- /pygrappa/simple_pruno.py: -------------------------------------------------------------------------------- 1 | """Naive implementation to make sure we know what's going on.""" 2 | 3 | import numpy as np 4 | from skimage.util import view_as_windows 5 | from scipy.linalg import null_space 6 | from scipy.signal import convolve2d 7 | 8 | import matplotlib.pyplot as plt 9 | from phantominator import shepp_logan 10 | 11 | 12 | def simple_pruno( 13 | kspace, calib, kernel_size=(5, 5), coil_axis: int = -1, 14 | sens=None, ph=None, kspace_ref=None): 15 | """PRUNO.""" 16 | 17 | # Coils to da back 18 | kspace = np.moveaxis(kspace, coil_axis, -1) 19 | calib = np.moveaxis(calib, coil_axis, -1) 20 | nx, ny, _nc = kspace.shape[:] 21 | 22 | # Make a calibration matrix 23 | kx, ky = kernel_size[:] 24 | # kx2, ky2 = int(kx/2), int(ky/2) 25 | nc = calib.shape[-1] 26 | 27 | # Pull out calibration matrix 28 | C = view_as_windows( 29 | calib, (kx, ky, nc)).reshape((-1, kx*ky*nc)) 30 | 31 | # Get the nulling kernels 32 | n = null_space(C, rcond=1e-3) 33 | print(n.shape) 34 | 35 | # Test to see if nulling kernels do indeed null 36 | if sens is not None: 37 | ws = 8 # full width of sensitivity map spectra 38 | wd = kx 39 | wm = wd + ws - 1 40 | 41 | # Choose a target 42 | xx, yy = int(nx/3), int(ny/3) 43 | 44 | # Get source 45 | wm2 = int(wm/2) 46 | S = ph[xx-wm2:xx+wm2, yy-wm2:yy+wm2].copy() 47 | assert (wm, wm) == S.shape 48 | 49 | sens_spect = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 50 | np.fft.ifftshift(sens, axes=(0, 1)), 51 | axes=(0, 1)), axes=(0, 1)) 52 | 53 | # Get the target 54 | ctr = int(sens_spect.shape[0]/2) 55 | ws2 = int(ws/2) 56 | T = [] 57 | for ii in range(nc): 58 | sens0 = sens_spect[ 59 | ctr-ws2:ctr+ws2, ctr-ws2:ctr+ws2, ii].copy() 60 | T.append(convolve2d(S, sens0, mode='valid')) 61 | T = np.moveaxis(np.array(T), 0, -1) 62 | assert (wd, wd, nc) == T.shape 63 | 64 | # Find local encoding matrix 65 | # E S = T 66 | # 67 | ShS = S.conj().T @ S 68 | print(ShS.shape, T.shape) 69 | ShT = S.conj().T @ T 70 | print(ShS.shape, ShT.shape) 71 | E = np.linalg.solve(ShS, ShT).T 72 | print(E.shape) 73 | 74 | 75 | if __name__ == '__main__': 76 | 77 | # Generate fake sensitivity maps: mps 78 | N = 32 79 | ncoils = 4 80 | xx = np.linspace(0, 1, N) 81 | x, y = np.meshgrid(xx, xx) 82 | mps = np.zeros((N, N, ncoils)) 83 | mps[..., 0] = x**2 84 | mps[..., 1] = 1 - x**2 85 | mps[..., 2] = y**2 86 | mps[..., 3] = 1 - y**2 87 | 88 | # generate 4 coil phantom 89 | ph = shepp_logan(N) 90 | imspace = ph[..., None]*mps 91 | imspace = imspace.astype('complex') 92 | ax = (0, 1) 93 | kspace = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 94 | np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax) 95 | kspace_ref = kspace.copy() 96 | 97 | ph = 1/np.sqrt(N**2)*np.fft.fftshift(np.fft.fft2( 98 | np.fft.ifftshift(ph, axes=ax), axes=ax), axes=ax) 99 | 100 | # crop 20x20 window from the center of k-space for calibration 101 | pd = 10 102 | ctr = int(N/2) 103 | calib = kspace[ctr-pd:ctr+pd, ctr-pd:ctr+pd, :].copy() 104 | 105 | # calibrate a kernel 106 | kernel_size = (5, 5) 107 | 108 | # undersample by a factor of 2 in both kx and ky 109 | kspace[::2, 1::2, :] = 0 110 | kspace[1::2, ::2, :] = 0 111 | 112 | # reconstruct: 113 | res = simple_pruno( 114 | kspace, calib, kernel_size, coil_axis=-1, 115 | sens=mps, ph=ph, kspace_ref=kspace_ref) 116 | assert False 117 | 118 | # Take a look 119 | res = np.abs(np.sqrt(N**2)*np.fft.fftshift(np.fft.ifft2( 120 | np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax)) 121 | res0 = np.zeros((2*N, 2*N)) 122 | kk = 0 123 | for idx in np.ndindex((2, 2)): 124 | ii, jj = idx[:] 125 | res0[ii*N:(ii+1)*N, jj*N:(jj+1)*N] = res[..., kk] 126 | kk += 1 127 | plt.imshow(res0, cmap='gray') 128 | plt.show() 129 | -------------------------------------------------------------------------------- /pygrappa/splitslicegrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of Split-Slice-GRAPPA.""" 2 | 3 | from pygrappa import slicegrappa 4 | 5 | 6 | def splitslicegrappa(*args, **kwargs): 7 | """Split-Slice-GRAPPA. 8 | 9 | Notes 10 | ----- 11 | This is an alias for pygrappa.slicegrappa(split=True). 12 | See pygrappa.slicegrappa() for more information. 13 | """ 14 | 15 | # Make sure that the 'split' argument is set to True 16 | if 'split' not in kwargs or not kwargs['split']: 17 | kwargs['split'] = True 18 | return slicegrappa(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /pygrappa/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckib2/pygrappa/539beda1e8881ff36797cb6612c2332e25463e17/pygrappa/src/__init__.py -------------------------------------------------------------------------------- /pygrappa/src/_grog_powers_template.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | template 6 | std::vector > _grog_powers_template( 7 | const T tx[], 8 | const T ty[], 9 | const T kx[], 10 | const T ky[], 11 | std::vector > idx, 12 | const int precision 13 | ) { 14 | /* Find unique fractional matrix powers. 15 | 16 | Parameters 17 | ---------- 18 | tx : const T[] 19 | Target x coordinates, 1d array. Same size as idx.size(). 20 | ty : const T[] 21 | Target y coordinates, 1d array. Same size as idx.size(). 22 | kx : const T[] 23 | Source x coordinates, 1d array. 24 | ky : const T[] 25 | Source x coordinates, 1d array. 26 | idx : vector > 27 | Indices of sources close to targets. 28 | precision : const int 29 | How many decimal points to round fractional matrix powers to. 30 | 31 | Returns 32 | ------- 33 | retVal : vector > with exactly 2 entries 34 | The fractional matrix powers for Gx (retVal[0]) and Gy 35 | (retVal[1]). 36 | 37 | Notes 38 | ----- 39 | All calculations are done in the template function (this one). 40 | Cython cannot resolve template function types, so wrapper 41 | functions are provided for both float and double coordinate 42 | arrays. 43 | */ 44 | 45 | std::unordered_set dx, dy; 46 | T pval = std::pow(10.0, precision); 47 | int ii = 0; 48 | for (auto idx_list : idx) { 49 | for (auto idx0 : idx_list) { 50 | dx.insert(std::round((tx[ii] - kx[idx0])*pval)/pval); 51 | dy.insert(std::round((ty[ii] - ky[idx0])*pval)/pval); 52 | } 53 | ii += 1; 54 | } 55 | auto retVal = std::vector > { dx, dy }; 56 | return retVal; 57 | } 58 | 59 | // We also need simple wrappers for double and float versions to 60 | // deal with Cython template limitations: 61 | std::vector > _grog_powers_double( 62 | const double tx[], 63 | const double ty[], 64 | const double kx[], 65 | const double ky[], 66 | std::vector > idx, 67 | const int precision 68 | ) { 69 | return _grog_powers_template(tx, ty, kx, ky, idx, precision); 70 | } 71 | 72 | std::vector > _grog_powers_float( 73 | const float tx[], 74 | const float ty[], 75 | const float kx[], 76 | const float ky[], 77 | std::vector > idx, 78 | const int precision 79 | ) { 80 | return _grog_powers_template(tx, ty, kx, ky, idx, precision); 81 | } 82 | -------------------------------------------------------------------------------- /pygrappa/src/_grog_powers_template.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef _GROG_POWERS_TEMPLATE_H 5 | #define _GROG_POWERS_TEMPLATE_H 6 | 7 | // Split into two functions instead of just using template function 8 | // because Cython can't resolve template functions! 9 | 10 | std::vector > _grog_powers_double( 11 | const double tx[], 12 | const double ty[], 13 | const double kx[], 14 | const double ky[], 15 | std::vector > idx, 16 | const int precision); 17 | 18 | std::vector > _grog_powers_float( 19 | const float tx[], 20 | const float ty[], 21 | const float kx[], 22 | const float ky[], 23 | std::vector > idx, 24 | const int precision); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /pygrappa/src/cgrappa.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=3 3 | 4 | from time import time 5 | 6 | import numpy as np 7 | cimport numpy as np 8 | from skimage.util import view_as_windows 9 | from libcpp.map cimport map 10 | from libcpp.vector cimport vector 11 | cimport cython 12 | from cython.operator cimport dereference, postincrement 13 | 14 | # Define a vector of uints for use in map definition 15 | ctypedef vector[unsigned int] vector_uint 16 | 17 | # This allows us to use the C function in this cython module 18 | cdef extern from "get_sampling_patterns.h": 19 | map[unsigned long long int, vector_uint] get_sampling_patterns( 20 | int*, unsigned int, unsigned int, 21 | unsigned int, unsigned int) 22 | 23 | @cython.boundscheck(False) 24 | @cython.wraparound(False) 25 | def cgrappa( 26 | kspace, calib, kernel_size=(5, 5), lamda=.01, 27 | int coil_axis=-1, silent=True, Wsupp=None, 28 | ret_weights=False, nc_desired=None): 29 | 30 | # Put coil axis in the back 31 | kspace = np.moveaxis(kspace, coil_axis, -1) 32 | calib = np.moveaxis(calib, coil_axis, -1) 33 | 34 | # Quick fix for Issue #41: cgrappa breaks for some nonsquare 35 | # matrices. Seems like the issue is incorrect indices returned 36 | # by src/get_sampling_patterns.cpp. Seems to work if first 37 | # dimension is < second dimension 38 | issue41_swap_dims = kspace.shape[0] > kspace.shape[1] 39 | if issue41_swap_dims: 40 | kspace = np.moveaxis(kspace, 1, 0) 41 | calib = np.moveaxis(calib, 1, 0) 42 | 43 | # Make sure we're contiguous 44 | kspace = np.ascontiguousarray(kspace) 45 | mask = np.ascontiguousarray( 46 | (np.abs(kspace[:, :, 0]) > 0).astype(np.int32)) 47 | 48 | # Let's define all the C types we'll be using 49 | cdef: 50 | Py_ssize_t kx, ky, nc 51 | Py_ssize_t cx, cy, 52 | Py_ssize_t ksx, ksy, ksx2, ksy2, 53 | Py_ssize_t adjx, adjy 54 | Py_ssize_t xx, yy 55 | Py_ssize_t ii 56 | Py_ssize_t[:] x 57 | Py_ssize_t[:] y 58 | # complex[:, :, ::1] kspace_memview = kspace 59 | int[:, ::1] mask_memview = mask 60 | map[unsigned long long int, vector_uint] res 61 | map[unsigned long long int, vector_uint].iterator it 62 | 63 | # Get size of arrays 64 | kx, ky, nc = kspace.shape[:] 65 | cx, cy, nc = calib.shape[:] 66 | ksx, ksy = kernel_size[:] 67 | ksx2, ksy2 = int(ksx/2), int(ksy/2) 68 | adjx = np.mod(ksx, 2) 69 | adjy = np.mod(ksy, 2) 70 | 71 | # We may want a different number of target coils than source 72 | # coils (e.g., NL-GRAPPA or VC-GRAPPA) 73 | if nc_desired is None: 74 | nc_desired = nc 75 | 76 | # Pad the arrays 77 | kspace = np.pad( 78 | kspace, ((ksx2, ksx2), (ksy2, ksy2), (0, 0)), mode='constant') 79 | calib = np.pad( 80 | calib, ((ksx2, ksx2), (ksy2, ksy2), (0, 0)), mode='constant') 81 | 82 | # Pass in arguments to C function, arrays pass pointer to start 83 | # of arrays, i.e., [x=0, y=0, coil=0]. 84 | t0 = time() 85 | res = get_sampling_patterns( 86 | &mask_memview[0, 0], 87 | kx, ky, 88 | ksx, ksy) 89 | if not silent: 90 | print('Find unique sampling patterns: %g' % (time() - t0)) 91 | 92 | # Get all overlapping patches of ACS 93 | t0 = time() 94 | A = view_as_windows( 95 | calib, (ksx, ksy, nc)).reshape((-1, ksx, ksy, nc)) 96 | cdef complex[:, :, :, ::1] A_memview = A 97 | if not silent: 98 | print('Make calibration patches: %g' % (time() - t0)) 99 | 100 | # Train and apply weights 101 | if ret_weights: # if the user wants weights, add 'em to the list 102 | Ws = [] 103 | t0 = time() 104 | it = res.begin() 105 | while(it != res.end()): 106 | 107 | # The key is a decimal number representing a binary number 108 | # whose bits describe the sampling mask. First convert to 109 | # binary with ksx*ksy bits, reverse (since lowest bit 110 | # is the upper left of the sampling pattern), convert to 111 | # boolean array, and then repmat to get the right number of 112 | # coils. 113 | P = format(dereference(it).first, 'b').zfill(ksx*ksy) 114 | P = (np.frombuffer(P[::-1].encode(), np.int8) - ord('0')).reshape( 115 | (ksx, ksy)).astype(bool) 116 | P = np.tile(P[..., None], (1, 1, nc)) 117 | 118 | # If the user supplied weights, let's use them; if not, train 119 | if not Wsupp: 120 | # Train the weights for this pattern 121 | S = A[:, P] 122 | T = A_memview[:, ksx2, ksy2, :nc_desired] 123 | ShS = S.conj().T @ S 124 | ShT = S.conj().T @ T 125 | lamda0 = lamda*np.linalg.norm(ShS)/ShS.shape[0] 126 | W = np.linalg.solve( 127 | ShS + lamda0*np.eye(ShS.shape[0]), ShT).T 128 | else: 129 | # Grab the next set of weights 130 | W = Wsupp.pop(0) 131 | 132 | if ret_weights: 133 | Ws.append(W) 134 | 135 | # For each hole that uses this pattern, fill in the recon 136 | idx = dereference(it).second 137 | x, y = np.unravel_index(idx, (kx, ky)) 138 | for ii in range(x.size): 139 | xx, yy = x[ii], y[ii] 140 | xx += ksx2 141 | yy += ksy2 142 | 143 | # Collect sources for this hole and apply weights 144 | S = kspace[xx-ksx2:xx+ksx2+adjx, yy-ksy2:yy+ksy2+adjy, :] 145 | S = S[P] 146 | kspace[xx, yy, :nc_desired] = (W @ S[:, None]).squeeze() 147 | 148 | # Move to the next sampling pattern 149 | postincrement(it) 150 | 151 | if not silent: 152 | print('Training and application of weights: %g' % ( 153 | time() - t0)) 154 | 155 | # Reverse axis swapping for Issue #41. First axis needed to be 156 | # larger than second, can put in correct places now that we're 157 | # done 158 | if issue41_swap_dims: 159 | kspace = np.moveaxis(kspace, 0, 1) 160 | if ret_weights: 161 | Ws = [np.moveaxis(Ws0, 0, 1) for Ws0 in Ws] 162 | 163 | # Give the user the weights if desired 164 | if ret_weights: 165 | return(np.moveaxis( 166 | kspace[ksx2:-ksx2, ksy2:-ksy2, :nc_desired], -1, coil_axis), Ws) 167 | return np.moveaxis( 168 | kspace[ksx2:-ksx2, ksy2:-ksy2, :nc_desired], -1, coil_axis) 169 | -------------------------------------------------------------------------------- /pygrappa/src/get_sampling_patterns.cpp: -------------------------------------------------------------------------------- 1 | #include "get_sampling_patterns.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | /* get_sampling_patterns: 8 | Given binary mask, find unique kernel-sized sampling patterns. 9 | 10 | From the sampling mask, map each unsampled k-space location to a 11 | unique kernel-sized sampling pattern. 12 | 13 | Parameters 14 | ---------- 15 | mask[kx, ky] : int 16 | Sampling mask. 17 | kx, ky : unsigned int 18 | Size of 2D k-space coil data array (and mask). 19 | ksx, ksy : unsigned int 20 | Size of kernel: (ksx, ksy). 21 | 22 | Returns 23 | ------- 24 | res : map > 25 | Maps vectors of indices to sampling patterns. 26 | 27 | Notes 28 | ----- 29 | Each sampling pattern is a ksx by ksy patch. We use a binary 30 | number to encode each pixel this patch. This number is an 31 | unsigned long long int, so if the patch size ksx*ksy > ULLONG_MAX, 32 | then we run into issues. Although unlikely, we check for this 33 | right at the start, and if it is an issue we use a default kernel 34 | size of (5, 5). 35 | 36 | */ 37 | std::map > get_sampling_patterns( 38 | int mask[], 39 | unsigned int kx, unsigned int ky, 40 | unsigned int ksx, unsigned int ksy) 41 | { 42 | 43 | // Check to make sure we're fine (we should be unless the user 44 | // tries something stupid): 45 | if ((unsigned long long int)(ksx)*(unsigned long long)(ksy) > ULLONG_MAX) { 46 | fprintf(stderr, "Something wild is happening with kernel size, choosing (ksx, ksy) = (5, 5).\n"); 47 | ksx = ksy = 5; 48 | } 49 | 50 | // Initializations 51 | std::multimap patterns; 52 | unsigned int ii, jj, idx; 53 | int ksx2, ksy2, adjx, adjy; 54 | ksx2 = ksx/2; 55 | ksy2 = ksy/2; 56 | adjx = ksx % 2; // same adjustment issue for even/odd kernel sizes 57 | adjy = ksy % 2; // see grappa.py source for complete discussion. 58 | 59 | // Iterate through all possible overlapping patches of the mask 60 | // to find unqiue sampling patterns. Assume zero-padded edges. 61 | for (ii = 0; ii < kx; ii++) { 62 | for (jj = 0; jj < ky; jj++) { 63 | 64 | // Find index of center patch 65 | idx = ii*kx + jj; 66 | 67 | // If the center is a hole, it might be valid patch! 68 | if (mask[idx] == 0) { 69 | // Look at the entire patch: if it's all zeros, we 70 | // don't care about it. Treat sum as a binary number 71 | // and flip the bits corresponding to filled locations 72 | // within the patch. Then sum also tells us what the 73 | // mask is, since the binary representation maps 74 | // directly to sampled and unsampled pixels. 75 | // Because sum is an unsigned long long, this will 76 | // break for inordinately large kernel sizes! 77 | unsigned long long int sum; 78 | unsigned int pos; 79 | int xx, yy; 80 | sum = 0; 81 | pos = 0; 82 | for (xx = -ksx2; xx < (int)(ksx2 + adjx); xx++) { 83 | for (yy = -ksy2; yy < (int)(ksy2 + adjy); yy++) { 84 | int wx, wy; 85 | wx = (int)ii + xx; 86 | wy = (int)jj + yy; 87 | // Make sure the index is within bounds: 88 | if ((wx >= 0) && (wy >= 0) && ((unsigned int)wx < kx) && ((unsigned int)wy < ky)) { 89 | if (mask[wx*kx + wy]) { 90 | sum += (1 << pos); 91 | } 92 | } 93 | pos++; 94 | } 95 | } 96 | 97 | // If we have samples, then we consider this a valid 98 | // patch and store the index corresponding to a unique 99 | // sampling pattern. 100 | if (sum) { 101 | patterns.insert(std::pair (sum, idx)); 102 | } 103 | } 104 | } 105 | } 106 | 107 | // For each unique sampling pattern we need to train a kernel! 108 | // Iterate through each unique key (sampling pattern) and store 109 | // the vector of indices that use that pattern as a map entry. 110 | std::map > res; 111 | typedef std::multimap::const_iterator iter_t; 112 | for (iter_t iter = patterns.begin(); iter != patterns.end(); iter = patterns.upper_bound(iter->first)) { 113 | std::vector idxs0; 114 | std::pair idxs = patterns.equal_range(iter->first); 115 | for (iter_t it = idxs.first; it != idxs.second; it++) { 116 | idxs0.push_back(it->second); 117 | } 118 | // res.emplace(iter->first, idxs0); // C++11 only 119 | res.insert(make_pair(iter->first, idxs0)); 120 | } 121 | 122 | return res; 123 | } 124 | -------------------------------------------------------------------------------- /pygrappa/src/get_sampling_patterns.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifndef GET_SAMPLING_PATTERNS_H 6 | #define GET_SAMPLING_PATTERNS_H 7 | 8 | std::map > get_sampling_patterns( 9 | int mask[], 10 | unsigned int kx, 11 | unsigned int ky, 12 | unsigned int ksx, 13 | unsigned int ksy); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /pygrappa/src/grog_gridding.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=3 3 | 4 | cimport numpy as np 5 | from libcpp.vector cimport vector 6 | cimport cython 7 | from cython cimport view 8 | 9 | cdef extern from "math.h": 10 | double round(double d) 11 | double round(float f) 12 | 13 | # Note: we have two identical functions except for types. 14 | 15 | @cython.boundscheck(False) 16 | @cython.wraparound(False) 17 | @cython.cdivision(True) 18 | def grog_gridding_double( 19 | const double[::1] tx, 20 | const double[::1] ty, 21 | const double[::1] kx, 22 | const double[::1] ky, 23 | np.ndarray[np.complex128_t, ndim=2] k, 24 | const vector[vector[int]] idx, 25 | np.ndarray[np.complex128_t, ndim=2] res, 26 | const long int[::1] inside, 27 | Dx, 28 | Dy, 29 | const int precision): 30 | '''Do GROG. 31 | 32 | Notes 33 | ----- 34 | `res` is modified in-place. 35 | ''' 36 | 37 | cdef: 38 | int ii, jj, N, M, idx0, in0 39 | double pfac, key_x, key_y 40 | 41 | pfac = 10.0**precision 42 | N = len(tx) 43 | for ii in range(N): 44 | M = idx[ii].size() 45 | in0 = inside[ii] 46 | for jj in range(M): 47 | idx0 = idx[ii][jj] 48 | key_x = round((tx[ii] - kx[idx0])*pfac)/pfac 49 | key_y = round((ty[ii] - ky[idx0])*pfac)/pfac 50 | Gxf = Dx[key_x] 51 | Gyf = Dy[key_y] 52 | 53 | res[in0, :] = res[in0, :] + Gxf @ Gyf @ k[idx0, :] 54 | 55 | # Finish the averaging (dividing step) 56 | if M: 57 | res[in0, :] = res[in0, :]/M 58 | 59 | @cython.boundscheck(False) 60 | @cython.wraparound(False) 61 | @cython.cdivision(True) 62 | def grog_gridding_float( 63 | const float[::1] tx, 64 | const float[::1] ty, 65 | const float[::1] kx, 66 | const float[::1] ky, 67 | np.ndarray[np.complex64_t, ndim=2] k, 68 | const vector[vector[int]] idx, 69 | np.ndarray[np.complex64_t, ndim=2] res, 70 | const long int[::1] inside, 71 | Dx, 72 | Dy, 73 | const int precision): 74 | '''Do GROG. 75 | 76 | Notes 77 | ----- 78 | `res` is modified in-place. 79 | ''' 80 | 81 | cdef: 82 | int ii, jj, N, M, idx0, in0 83 | float pfac, key_x, key_y 84 | 85 | pfac = 10.0**precision 86 | N = len(tx) 87 | for ii in range(N): 88 | M = idx[ii].size() 89 | in0 = inside[ii] 90 | for jj in range(M): 91 | idx0 = idx[ii][jj] 92 | key_x = round((tx[ii] - kx[idx0])*pfac)/pfac 93 | key_y = round((ty[ii] - ky[idx0])*pfac)/pfac 94 | Gxf = Dx[key_x] 95 | Gyf = Dy[key_y] 96 | 97 | res[in0, :] = res[in0, :] + Gxf @ Gyf @ k[idx0, :] 98 | 99 | # Finish the averaging (dividing step) 100 | if M: 101 | res[in0, :] = res[in0, :]/M 102 | -------------------------------------------------------------------------------- /pygrappa/src/grog_powers.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=3 3 | 4 | cimport numpy as np 5 | from libcpp.unordered_set cimport unordered_set 6 | from libcpp.vector cimport vector 7 | cimport cython 8 | 9 | cdef extern from "_grog_powers_template.h": 10 | vector[unordered_set[double]] _grog_powers_double( 11 | const double[] tx, 12 | const double[] ty, 13 | const double[] kx, 14 | const double[] ky, 15 | vector[vector[int]] idx, 16 | const int precision) 17 | 18 | vector[unordered_set[float]] _grog_powers_float( 19 | const float[] tx, 20 | const float[] ty, 21 | const float[] kx, 22 | const float[] ky, 23 | vector[vector[int]] idx, 24 | const int precision) 25 | 26 | @cython.boundscheck(False) 27 | @cython.wraparound(False) 28 | @cython.cdivision(True) 29 | def grog_powers_float( 30 | const float[::1] tx, 31 | const float[::1] ty, 32 | const float[::1] kx, 33 | const float[::1] ky, 34 | vector[vector[int]] idx, 35 | const int precision): 36 | return _grog_powers_float( 37 | &tx[0], &ty[0], &kx[0], &ky[0], idx, precision) 38 | 39 | @cython.boundscheck(False) 40 | @cython.wraparound(False) 41 | @cython.cdivision(True) 42 | def grog_powers_double( 43 | const double[::1] tx, 44 | const double[::1] ty, 45 | const double[::1] kx, 46 | const double[::1] ky, 47 | vector[vector[int]] idx, 48 | const int precision): 49 | return _grog_powers_double( 50 | &tx[0], &ty[0], &kx[0], &ky[0], idx, precision) 51 | -------------------------------------------------------------------------------- /pygrappa/src/meson.build: -------------------------------------------------------------------------------- 1 | pglib = py3.extension_module('train_kernels', 2 | cython_gen_cpp.process('train_kernels.pyx'), 3 | cpp_args: cython_cpp_args, 4 | include_directories: [inc_np], 5 | install: true, 6 | subdir: 'pygrappa' 7 | ) 8 | 9 | pglib = py3.extension_module('cgrappa', 10 | [ 11 | cython_gen_cpp.process('cgrappa.pyx'), 12 | 'get_sampling_patterns.cpp' 13 | ], 14 | cpp_args: cython_cpp_args, 15 | include_directories: [inc_np], 16 | install: true, 17 | subdir: 'pygrappa' 18 | ) 19 | 20 | pglib = py3.extension_module('grog_powers', 21 | [ 22 | cython_gen_cpp.process('grog_powers.pyx'), 23 | '_grog_powers_template.cpp' 24 | ], 25 | cpp_args: cython_cpp_args, 26 | include_directories: [inc_np], 27 | install: true, 28 | subdir: 'pygrappa' 29 | ) 30 | 31 | pglib = py3.extension_module('grog_gridding', 32 | cython_gen_cpp.process('grog_gridding.pyx'), 33 | cpp_args: cython_cpp_args, 34 | include_directories: [inc_np], 35 | install: true, 36 | subdir: 'pygrappa' 37 | ) 38 | -------------------------------------------------------------------------------- /pygrappa/src/train_kernels.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | # cython: language_level=3 3 | 4 | cimport cython 5 | 6 | cimport numpy as np 7 | import numpy as np 8 | 9 | @cython.boundscheck(False) # turn off bounds-checking for entire function 10 | @cython.wraparound(False) # turn off negative index wrapping for entire function 11 | def train_kernels( 12 | np.ndarray kspace, 13 | const Py_ssize_t nc, 14 | const np.complex[:, :, ::1] A, 15 | dict P, 16 | const size_t[::1] kernel_size, 17 | const size_t[::1] pads, 18 | const double lamda): 19 | 20 | # Train and apply kernels 21 | cdef Py_ssize_t ksize = nc 22 | cdef Py_ssize_t ii 23 | for ii in range(kernel_size.shape[0]): 24 | ksize *= kernel_size[ii] 25 | cdef complex[:, :, ::1] Ws = np.empty((len(P), ksize, nc), dtype=kspace.dtype) 26 | cdef complex[:, ::1] S = np.empty((A.shape[0], ksize), dtype=kspace.dtype, order='C') 27 | cdef Py_ssize_t ctr = np.ravel_multi_index([pd for pd in pads], dims=kernel_size) 28 | cdef complex[:, ::1] T = np.empty((A.shape[0], nc), dtype=kspace.dtype, order='C') 29 | cdef np.ndarray ShS = np.empty((ksize, ksize), dtype=kspace.dtype, order='C') 30 | cdef np.ndarray ShT = np.empty((ksize, nc), dtype=kspace.dtype, order='C') 31 | cdef int np0 32 | cdef double lamda0 33 | cdef Py_ssize_t jj, kk, aa, bb, cc 34 | cdef Py_ssize_t[::1] idx = np.empty(ksize, dtype=np.intp) 35 | cdef int M = A.shape[0] 36 | for ii, key in enumerate(P): 37 | np0 = 0 38 | for jj in range(len(key)): 39 | idx[np0] = jj 40 | np0 += key[jj] 41 | 42 | # gather sources 43 | for aa in range(M): 44 | for bb in range(np0): 45 | for cc in range(nc): 46 | S[aa, bb*nc + cc] = A[aa, idx[bb], cc] 47 | 48 | for jj in range(nc): 49 | T[:, jj] = A[:, ctr, jj] 50 | 51 | # construct square matrices 52 | np0 *= nc # consider all coil elements 53 | # TODO: use BLAS/LAPACK functions here 54 | ShS[:np0, :np0] = np.dot(np.conj(S[:M, :np0]).T, S[:M, :np0]) 55 | ShT[:np0, :] = np.dot(np.conj(S[:M, :np0]).T, T) 56 | 57 | # tik reg 58 | if lamda: 59 | lamda0 = lamda*np.linalg.norm(ShS[:np0, :np0])/np0 60 | for jj in range(np0): 61 | ShS[jj, jj] += lamda0 62 | 63 | # Solve the LS problem 64 | ShT[:np0, :] = np.linalg.solve(ShS[:np0, :np0], ShT[:np0, :]) 65 | 66 | for jj in range(np0): 67 | for kk in range(nc): 68 | Ws[ii, jj, kk] = ShT[jj, kk] 69 | 70 | return np.array(Ws) 71 | -------------------------------------------------------------------------------- /pygrappa/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mckib2/pygrappa/539beda1e8881ff36797cb6612c2332e25463e17/pygrappa/tests/__init__.py -------------------------------------------------------------------------------- /pygrappa/tests/meson.build: -------------------------------------------------------------------------------- 1 | py3.install_sources([ 2 | '__init__.py', 3 | 'helpers.py', 4 | 'test_cgrappa.py', 5 | 'test_cgsense.py', 6 | 'test_grappa.py', 7 | 'test_hpgrappa.py', 8 | 'test_igrappa.py', 9 | 'test_mdgrappa.py', 10 | 'test_vcgrappa.py' 11 | ], 12 | subdir: 'pygrappa/tests' 13 | ) 14 | -------------------------------------------------------------------------------- /pygrappa/tests/test_cgrappa.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for CGRAPPA.''' 2 | 3 | import unittest 4 | 5 | import numpy as np 6 | from pygrappa import cgrappa 7 | from .helpers import make_base_test_case_2d 8 | 9 | 10 | class TestCGRAPPA(make_base_test_case_2d(cgrappa, ssim_thresh=.92, types=[('complex128', np.complex128)])): 11 | pass 12 | 13 | 14 | if __name__ == '__main__': 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /pygrappa/tests/test_cgsense.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for CG-SENSE.''' 2 | 3 | import unittest 4 | 5 | from pygrappa import cgsense 6 | from .helpers import make_base_test_case_2d 7 | 8 | 9 | class TestCGSENSE2d(make_base_test_case_2d(cgsense, ssim_thresh=0.93, use_R3=True, is_sense=True, output_kspace=False)): 10 | pass 11 | 12 | 13 | if __name__ == '__main__': 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /pygrappa/tests/test_grappa.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for GRAPPA.''' 2 | 3 | import unittest 4 | 5 | from pygrappa import grappa 6 | from .helpers import make_base_test_case_2d 7 | 8 | 9 | class TestGRAPPA(make_base_test_case_2d(grappa, ssim_thresh=.92)): 10 | pass 11 | 12 | 13 | if __name__ == '__main__': 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /pygrappa/tests/test_hpgrappa.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for hp-GRAPPA.''' 2 | 3 | import unittest 4 | 5 | from pygrappa import hpgrappa 6 | from .helpers import make_base_test_case_2d 7 | 8 | 9 | class TesthpGRAPPA(make_base_test_case_2d(hpgrappa, ssim_thresh=.86, extra_args={'fov': (10e-2, 10e-2)})): 10 | # TODO: failing on some nonsymmetric cases; thresh could be higher 11 | 12 | @unittest.expectedFailure 13 | def test_recon_shepp_logan2d_M30_N32_nc7_calib2d_cM7_cN8_undersample_x2_complex128(self): 14 | super().test_recon_shepp_logan2d_M30_N32_nc7_calib2d_cM7_cN8_undersample_x2_complex128() 15 | 16 | @unittest.expectedFailure 17 | def test_recon_shepp_logan2d_M30_N32_nc7_calib2d_cM7_cN8_undersample_x2_complex64(self): 18 | super().test_recon_shepp_logan2d_M30_N32_nc7_calib2d_cM7_cN8_undersample_x2_complex64() 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /pygrappa/tests/test_igrappa.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for iGRAPPA.''' 2 | 3 | import unittest 4 | 5 | import numpy as np 6 | from pygrappa import igrappa 7 | from .helpers import make_base_test_case_2d 8 | 9 | 10 | # This runs really slow, so reduce number of tests 11 | class TestiGRAPPA(make_base_test_case_2d( 12 | igrappa, 13 | ssim_thresh=.92, 14 | Ms=[32, 30], 15 | Ns=[32], 16 | ncoils=[4], 17 | cMs=[1/3], 18 | cNs=[1/3], 19 | types=[('complex64', np.complex64)], 20 | )): 21 | # TODO: threshold could probably be higher 22 | pass 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /pygrappa/tests/test_mdgrappa.py: -------------------------------------------------------------------------------- 1 | '''Unit tests for multidimensional GRAPPA.''' 2 | 3 | import unittest 4 | 5 | from pygrappa import mdgrappa 6 | from .helpers import make_base_test_case_2d 7 | 8 | 9 | class TestMDGRAPPA(make_base_test_case_2d(mdgrappa, ssim_thresh=0.95)): 10 | pass 11 | 12 | 13 | if __name__ == '__main__': 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /pygrappa/tests/test_vcgrappa.py: -------------------------------------------------------------------------------- 1 | """Unit tests for VC-GRAPPA.""" 2 | 3 | import unittest 4 | 5 | from pygrappa import vcgrappa 6 | from pygrappa.tests.helpers import make_base_test_case_2d 7 | 8 | 9 | class TestVCGRAPPA(make_base_test_case_2d( 10 | vcgrappa, 11 | ssim_thresh=.90)): 12 | # TODO: adjust/improve ssim_thresh; right now less than mdgrappa 13 | pass 14 | 15 | 16 | if __name__ == '__main__': 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /pygrappa/tgrappa.py: -------------------------------------------------------------------------------- 1 | """TGRAPPA implementation.""" 2 | 3 | import numpy as np 4 | from tqdm import tqdm 5 | 6 | from pygrappa import mdgrappa as grappa 7 | 8 | 9 | def tgrappa( 10 | kspace, calib_size=(20, 20), kernel_size=(5, 5), 11 | coil_axis: int = -2, time_axis: int = -1): 12 | """Temporal GRAPPA. 13 | 14 | Parameters 15 | ---------- 16 | kspace : array_like 17 | 2+1D multi-coil k-space data to reconstruct from (total of 18 | 4 dimensions). Missing entries should have exact zeros in 19 | them. 20 | calib_size : array_like, optional 21 | Size of calibration region at the center of kspace. 22 | kernel_size : tuple, optional 23 | Desired shape of the in-plane calibration regions: (kx, ky). 24 | coil_axis : int, optional 25 | Dimension holding coil data. 26 | time_axis : int, optional 27 | Dimension holding time data. 28 | 29 | Returns 30 | ------- 31 | res : array_like 32 | Reconstructed k-space data. 33 | 34 | Raises 35 | ------ 36 | ValueError 37 | When no complete ACS region can be found. 38 | 39 | Notes 40 | ----- 41 | Implementation of the method proposed in [1]_. 42 | 43 | The idea is to form ACS regions using data from adjacent time 44 | frames. For example, in the case of 1D undersampling using 45 | undersampling factor R, at least R time frames must be merged to 46 | form a completely sampled ACS. Then we can simply supply the 47 | undersampled data and the synthesized ACS to GRAPPA. Thus the 48 | heavy lifting of this function will be in determining the ACS 49 | calibration region at each time frame. 50 | 51 | References 52 | ---------- 53 | .. [1] Breuer, Felix A., et al. "Dynamic autocalibrated parallel 54 | imaging using temporal GRAPPA (TGRAPPA)." Magnetic 55 | Resonance in Medicine: An Official Journal of the 56 | International Society for Magnetic Resonance in Medicine 57 | 53.4 (2005): 981-985. 58 | """ 59 | 60 | # Move coil and time axes to a place we can find them 61 | kspace = np.moveaxis(kspace, (coil_axis, time_axis), (-2, -1)) 62 | sx, sy, _sc, st = kspace.shape[:] 63 | cx, cy = calib_size[:] 64 | sx2, sy2 = int(sx/2), int(sy/2) 65 | cx2, cy2 = int(cx/2), int(cy/2) 66 | adjx, adjy = int(np.mod(cx, 2)), int(np.mod(cy, 2)) 67 | 68 | # Make sure that it's even possible to create complete ACS, we'll 69 | # have problems in the loop if we don't catch it here! 70 | if not np.all(np.sum(np.abs(kspace[ 71 | sx2-cx2:sx2+cx2+adjx, 72 | sy2-cy2:sy2+cy2+adjy, ...]), axis=-1)): 73 | raise ValueError('Full ACS region cannot be found!') 74 | 75 | # To avoid running GRAPPA more than once on one time frame, 76 | # we'll keep track of which frames have been reconstructed: 77 | completed_tframes = np.zeros(st, dtype=bool) 78 | 79 | # Initialize the progress bar 80 | pbar = tqdm(total=st, leave=False, desc='TGRAPPA') 81 | 82 | # Iterate through all time frames, construct ACS regions, and 83 | # run GRAPPA on each time slice 84 | res = np.zeros(kspace.shape, dtype=kspace.dtype) 85 | tt = 0 # time frame index 86 | done = False # True when all time frames have been consumed 87 | from_end = False # start at the end and go backwards 88 | while not done: 89 | 90 | # Find next feasible kernel -- Strategy: consume time frames 91 | # until all kernel elements have been filled. We won't 92 | # assume that overlaps will not happen frame to frame, so we 93 | # will appropriately average each kernel position by keeping 94 | # track of how many samples are in a position with 'counts' 95 | got_kernel = False 96 | calib = [] 97 | counts = np.zeros((cx, cy), dtype=int) 98 | tframes = [] # time frames over which the ACS is valid 99 | while not got_kernel: 100 | if not completed_tframes[tt]: 101 | tframes.append(tt) 102 | calib.append(kspace[ 103 | sx2-cx2:sx2+cx2+adjx, 104 | sy2-cy2:sy2+cy2+adjy, :, tt].copy()) 105 | counts += np.abs(calib[-1][..., 0]) > 0 106 | if np.all(counts > 0): 107 | got_kernel = True 108 | 109 | # Go to next time frame except maybe for the last ACS 110 | if not from_end: 111 | tt += 1 # Consume the next time frame 112 | else: 113 | tt -= 1 # Consume previous time frame 114 | 115 | # If we need more time frames than we have, then we need 116 | # to start from the end and come forward. This can only 117 | # happen on the last iteration of the outer loop 118 | if not got_kernel and tt == st: 119 | # Start at the end 120 | tt = st-1 121 | 122 | # Reset ACS, counts, and tframes 123 | calib = [] 124 | counts = np.zeros((cx, cy), dtype=int) 125 | tframes = [] 126 | 127 | # Let the loop know we want to reverse directions 128 | from_end = True 129 | 130 | # Now average over all time frames to get a single ACS 131 | calib = np.sum(calib, axis=0)/counts[..., None] 132 | 133 | # This ACS region is valid over all time frames used to 134 | # create it. Run GRAPPA on each valid time frame with calib: 135 | for t0 in tframes: 136 | res[..., t0] = grappa( 137 | kspace[..., t0], calib, kernel_size) 138 | completed_tframes[t0] = True 139 | pbar.update(1) 140 | 141 | # Stopping condition: end when all time frames are consumed 142 | if np.all(completed_tframes): 143 | done = True 144 | 145 | # Close out the progress bar 146 | pbar.close() 147 | 148 | # Move axes back to where the user had them 149 | return np.moveaxis(res, (-1, -2), (time_axis, coil_axis)) 150 | -------------------------------------------------------------------------------- /pygrappa/utils/__init__.py: -------------------------------------------------------------------------------- 1 | '''Bring functions to correct level for import.''' 2 | 3 | from .gaussian_csm import gaussian_csm # NOQA 4 | from .disjoint_csm import disjoint_csm # NOQA 5 | from .gridder import gridder # NOQA 6 | -------------------------------------------------------------------------------- /pygrappa/utils/disjoint_csm.py: -------------------------------------------------------------------------------- 1 | """Too-good-to-be-real coil sensitivity maps.""" 2 | 3 | import numpy as np 4 | 5 | 6 | def disjoint_csm(sx: int, sy: int, ncoil: int): 7 | """Make ncoil partitions of (sx, sy) box for coil sensitivities. 8 | 9 | Parameters 10 | ---------- 11 | sx, sy : int 12 | Height and width of coil images. 13 | ncoil : int 14 | Number of coils to be simulated. 15 | 16 | Returns 17 | ------- 18 | csm : array_like 19 | Simulated coil sensitivity maps. 20 | """ 21 | 22 | blocks = np.ones((sx, sy)) 23 | blocks = np.array_split(blocks, ncoil, axis=0) 24 | csm = np.zeros((sx, sy, ncoil)) 25 | idx = 0 26 | for ii in range(ncoil): 27 | sh = blocks[ii].shape[0] 28 | csm[idx:idx+sh, :, ii] = blocks[ii] 29 | idx += sh 30 | return csm 31 | -------------------------------------------------------------------------------- /pygrappa/utils/gaussian_csm.py: -------------------------------------------------------------------------------- 1 | """Simple coil sensitivity maps.""" 2 | 3 | import numpy as np 4 | from scipy.stats import multivariate_normal 5 | 6 | 7 | def gaussian_csm(sx: int, sy: int, ncoil: int, sigma: float = 1.0): 8 | """Make a 2D Gaussian walk in a circle for coil sensitivities. 9 | 10 | Parameters 11 | ---------- 12 | sx, sy : int 13 | Height and width of coil images. 14 | ncoil : int 15 | Number of coils to be simulated. 16 | sigma : float 17 | Diagonal entries in covariance matrix. 18 | 19 | Returns 20 | ------- 21 | csm : array_like 22 | Simulated coil sensitivity maps. 23 | """ 24 | 25 | X, Y = np.meshgrid( 26 | np.linspace(-1, 1, sy), np.linspace(-1, 1, sx)) 27 | pos = np.stack((X[..., None], Y[..., None]), axis=-1) 28 | csm = np.zeros((sx, sy, ncoil)) 29 | cov = [[sigma, 0], [0, sigma]] 30 | for ii in range(ncoil): 31 | mu = [np.cos(ii/ncoil*np.pi*2), np.sin(ii/ncoil*2*np.pi)] 32 | csm[..., ii] = multivariate_normal(mu, cov).pdf(pos) 33 | return csm + 1j*csm 34 | -------------------------------------------------------------------------------- /pygrappa/utils/gridder.py: -------------------------------------------------------------------------------- 1 | """Simple gridding for non-Cartesian kspace.""" 2 | 3 | import numpy as np 4 | from scipy.interpolate import griddata 5 | 6 | 7 | def gridder( 8 | kx, ky, k, sx: int, sy: int, coil_axis: int = -1, ifft: bool = True, os: float = 2.0, 9 | method: str = "linear"): 10 | """Helper function to grid non-Cartesian data. 11 | 12 | Parameters 13 | ---------- 14 | kx, ky : array_like 15 | 1D arrays of (kx, ky) coordinates corresponding to 16 | measurements, k. 17 | k : array_like 18 | k-space measurements corresponding to spatial frequencies 19 | (kx, ky). 20 | sx, sy : int 21 | Size of gridded kspace. 22 | coil_axis : int, optional 23 | Dimension of k that holds the coil data. 24 | ifft : bool, optional 25 | Perform inverse FFT on gridded data and remove oversampling 26 | factor before returning. 27 | os : float, optional 28 | Oversampling factor for gridding. 29 | method : str, optional 30 | Strategy for interpolation used by 31 | scipy.interpolate.griddata(). See scipy docs for complete 32 | list of options. 33 | 34 | Returns 35 | ------- 36 | imspace : array_like, optional 37 | If ifft=True. 38 | kspace : array_like, optional 39 | If ifft=False. 40 | """ 41 | 42 | # Move coil data to the back 43 | k = np.moveaxis(k, coil_axis, -1) 44 | 45 | yy, xx = np.meshgrid( 46 | np.linspace(np.min(kx), np.max(kx), sx*os), 47 | np.linspace(np.min(ky), np.max(ky), sy*os)) 48 | grid_kspace = griddata((kx, ky), k, (xx, yy), method=method) 49 | 50 | if ifft: 51 | padx = int(sx*(os - 1)/2) 52 | pady = int(sy*(os - 1)/2) 53 | return np.fft.fftshift(np.fft.ifft2( 54 | np.fft.ifftshift(np.nan_to_num(grid_kspace), axes=(0, 1)), 55 | axes=(0, 1)), axes=(0, 1))[padx:-padx, pady:-pady, :] 56 | return grid_kspace 57 | -------------------------------------------------------------------------------- /pygrappa/utils/meson.build: -------------------------------------------------------------------------------- 1 | py3.install_sources([ 2 | '__init__.py', 3 | 'disjoint_csm.py', 4 | 'gaussian_csm.py', 5 | 'gridder.py' 6 | ], 7 | subdir: 'pygrappa/utils' 8 | ) 9 | -------------------------------------------------------------------------------- /pygrappa/vcgrappa.py: -------------------------------------------------------------------------------- 1 | """Python implementation of VC-GRAPPA.""" 2 | 3 | import numpy as np 4 | 5 | from pygrappa import mdgrappa as grappa 6 | 7 | 8 | def vcgrappa(kspace, calib, *args, coil_axis: int = -1, **kwargs): 9 | """Virtual Coil GRAPPA. 10 | 11 | See pygrappa.grappa() for argument list. 12 | 13 | Notes 14 | ----- 15 | Implements modifications to GRAPPA as described in [1]_. The 16 | only change I can see is stacking the conjugate coils in the 17 | coil dimension. For best results, make sure there is a suitably 18 | chosen background phase variation as described in the paper. 19 | 20 | This function is a wrapper of pygrappa.cgrappa(). The existing 21 | coils are conjugated, added to the coil dimension, and passed 22 | through along with all other arguments. 23 | 24 | References 25 | ---------- 26 | .. [1] Blaimer, Martin, et al. "Virtual coil concept for improved 27 | parallel MRI employing conjugate symmetric signals." 28 | Magnetic Resonance in Medicine: An Official Journal of the 29 | International Society for Magnetic Resonance in Medicine 30 | 61.1 (2009): 93-102. 31 | """ 32 | 33 | # Move coil axis to end 34 | kspace = np.moveaxis(kspace, coil_axis, -1) 35 | calib = np.moveaxis(calib, coil_axis, -1) 36 | ax = (0, 1) 37 | 38 | # remember the type we started out with, np.fft will change 39 | # to complex128 regardless of what we started with 40 | tipe = kspace.dtype 41 | 42 | # In and out of kspace to get conjugate coils 43 | vc_kspace = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 44 | kspace, axes=ax), axes=ax), axes=ax) 45 | vc_kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 46 | np.conj(vc_kspace), axes=ax), axes=ax), axes=ax) 47 | 48 | # Same deal for calib... 49 | vc_calib = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift( 50 | calib, axes=ax), axes=ax), axes=ax) 51 | vc_calib = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift( 52 | np.conj(vc_calib), axes=ax), axes=ax), axes=ax) 53 | 54 | # Put all our ducks in a row... 55 | kspace = np.concatenate((kspace, vc_kspace), axis=-1) 56 | calib = np.concatenate((calib, vc_calib), axis=-1) 57 | 58 | # Pass through to GRAPPA 59 | return grappa( 60 | kspace, calib, coil_axis=-1, 61 | *args, **kwargs).astype(tipe) 62 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["meson", "ninja", "Cython", "numpy", "scipy", "meson-python"] 3 | build-backend = "mesonpy" 4 | 5 | [project] 6 | name = "pygrappa" 7 | version = "0.26.3" 8 | description = "GeneRalized Autocalibrating Partially Parallel Acquisitions." 9 | readme = "README.rst" 10 | requires-python = ">=3.9" 11 | keywords = ["mri", "grappa", "sense"] 12 | license = {text = "MIT"} 13 | classifiers = [ 14 | "Programming Language :: Python :: 3", 15 | ] 16 | 17 | [project.optional-dependencies] 18 | test = ["pytest", "flake8", "phantominator"] 19 | examples = ["matplotlib", "sckikit-image", "tqdm", "phantominator"] 20 | --------------------------------------------------------------------------------