├── .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