├── tests ├── __init__.py ├── test_main.py ├── test_utils.py ├── __main__.py ├── conftest.py └── test_regseg.py ├── examples ├── .gitignore └── demo.ipynb ├── .gitignore ├── spm12 ├── __main__.py ├── amypad_coreg_modify_affine.m ├── amypad_normw.m ├── __init__.py ├── cli.py ├── amypad_resample.m ├── amypad_coreg.m ├── amypad_seg.m ├── utils.py ├── setup_rt.py ├── standalone.py └── regseg.py ├── CONTRIBUTING.md ├── LICENCE.md ├── .pre-commit-config.yaml ├── README.rst ├── .github └── workflows │ ├── comment-bot.yml │ └── test.yml └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /.ipynb_checkpoints/ 2 | /*.nii* 3 | /affine-spm/ 4 | /*.png 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__/ 3 | /spm12/_dist_ver.py 4 | /*.egg*/ 5 | /build/ 6 | /dist/ 7 | /.coverage* 8 | /coverage.xml 9 | /.pytest_cache/ 10 | -------------------------------------------------------------------------------- /spm12/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from .cli import main 5 | 6 | if __name__ == "__main__": # pragma: no cover 7 | sys.exit(main(sys.argv[1:])) 8 | -------------------------------------------------------------------------------- /spm12/amypad_coreg_modify_affine.m: -------------------------------------------------------------------------------- 1 | function out = amypad_coreg_modify_affine(imflo, M) 2 | VF = strcat(imflo,',1'); 3 | MM = zeros(4,4); 4 | MM(:,:) = spm_get_space(VF); 5 | spm_get_space(VF, M\MM(:,:)); 6 | out = 0; 7 | end 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ```sh 4 | # clone & install dependencies (one-off) 5 | git clone https://github.com/AMYPAD/SPM12 6 | cd SPM12 7 | pip install -U .[dev] 8 | 9 | # run tests 10 | python -m tests 11 | ``` 12 | 13 | You will likely get an error message detailing where to download test data. 14 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from miutil.mlab import matlabroot 2 | from pytest import mark, skip 3 | 4 | pytestmark = mark.filterwarnings("ignore:numpy.ufunc size changed.*:RuntimeWarning") 5 | 6 | 7 | def test_cli(): 8 | try: 9 | if matlabroot("None") == "None": 10 | raise FileNotFoundError 11 | except FileNotFoundError: 12 | skip("MATLAB not installed") 13 | from spm12.cli import main 14 | 15 | assert 0 == main() 16 | -------------------------------------------------------------------------------- /spm12/amypad_normw.m: -------------------------------------------------------------------------------- 1 | function out = amypad_normw(def_file, flist4norm, voxsz, intrp, bbox) 2 | job.subj.def = {def_file}; 3 | job.subj.resample = flist4norm; 4 | %job.woptions.bb = [NaN, NaN, NaN; NaN, NaN, NaN]; 5 | job.woptions.bb = bbox; 6 | job.woptions.vox = voxsz; %[voxsz, voxsz, voxsz]; 7 | job.woptions.interp = intrp; 8 | job.woptions.prefix = 'w'; 9 | 10 | addpath(fullfile(spm('Dir'),'config')); 11 | spm_run_norm(job); 12 | out=0; 13 | end 14 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from pytest import importorskip 2 | 3 | 4 | def test_matlab(): 5 | engine = importorskip("matlab.engine") 6 | from spm12 import utils 7 | 8 | assert not engine.find_matlab() 9 | eng = utils.get_matlab() 10 | 11 | matrix = eng.eval("eye(3)") 12 | assert matrix.size == (3, 3) 13 | 14 | assert engine.find_matlab() 15 | eng2 = utils.get_matlab() 16 | assert eng == eng2 17 | 18 | utils.ensure_spm() 19 | assert utils.spm_dir_eng() == utils.spm_dir() 20 | -------------------------------------------------------------------------------- /spm12/__init__.py: -------------------------------------------------------------------------------- 1 | from .regseg import * # NOQA, yapf: disable 2 | from .setup_rt import * # NOQA, yapf: disable 3 | from .standalone import * # NOQA, yapf: disable 4 | from .utils import * # NOQA, yapf: disable 5 | 6 | # version detector. Precedence: installed dist, git, 'UNKNOWN' 7 | try: 8 | from ._dist_ver import __version__ 9 | except ImportError: 10 | try: 11 | from setuptools_scm import get_version 12 | 13 | __version__ = get_version(root="..", relative_to=__file__) 14 | except (ImportError, LookupError): 15 | __version__ = "UNKNOWN" 16 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from miutil.fdio import extractall 4 | from miutil.web import urlopen_cached 5 | 6 | from .conftest import HOME 7 | 8 | log = logging.getLogger(__name__) 9 | DATA_URL = "https://zenodo.org/record/3877529/files/amyloidPET_FBP_TP0_extra.zip?download=1" 10 | 11 | 12 | def main(): 13 | log.info(f"Downloading {DATA_URL}\nto ${{DATA_ROOT:-~}} ({HOME})") 14 | with urlopen_cached(DATA_URL, HOME) as fd: 15 | extractall(fd, HOME) 16 | 17 | 18 | if __name__ == "__main__": 19 | logging.basicConfig(level=logging.INFO) 20 | main() 21 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020-23 AMYPAD 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this project except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /spm12/cli.py: -------------------------------------------------------------------------------- 1 | """Usage: 2 | spm12 [options] 3 | 4 | Options: 5 | -c DIR, --cache DIR : directory to use for installation [default: ~/.spm12]. 6 | -s VER, --spm-version : version [default: 12]. 7 | """ 8 | import logging 9 | 10 | from argopt import argopt 11 | 12 | from .utils import ensure_spm 13 | 14 | 15 | def main(argv=None): 16 | logging.basicConfig(level=logging.DEBUG, format="%(levelname)s:%(funcName)s:%(message)s") 17 | args = argopt(__doc__).parse_args(argv) 18 | ensure_spm(cache=args.cache, version=args.spm_version) 19 | print(f"SPM{args.spm_version} is successfully installed") 20 | return 0 21 | -------------------------------------------------------------------------------- /spm12/amypad_resample.m: -------------------------------------------------------------------------------- 1 | function out = amypad_resample(imref, imflo, M, f_mask, f_mean, f_interp, f_which, f_prefix) 2 | %-Reslicing parameters 3 | rflags.mask = f_mask; 4 | rflags.mean = f_mean; 5 | rflags.interp = f_interp; 6 | rflags.which = f_which; 7 | rflags.wrap = [0 0 0]; 8 | rflags.prefix = f_prefix; 9 | 10 | VG = strcat(imref,',1'); 11 | VF = strcat(imflo,',1'); 12 | 13 | % disp('Matlab internal reference image:'); disp(imref); 14 | % disp('Matlab internal floating image:'); disp(imflo); 15 | % disp(rflags) 16 | 17 | MM = zeros(4,4); 18 | MM(:, :) = spm_get_space(VF); 19 | spm_get_space(VF, M\MM(:, :)); 20 | P = {VG; VF}; 21 | spm_reslice(P, rflags); 22 | out = 0; 23 | end 24 | -------------------------------------------------------------------------------- /spm12/amypad_coreg.m: -------------------------------------------------------------------------------- 1 | function [M, x] = amypad_coreg(imref, imflo, costfun, sep, tol, fwhm, params, graphics, visual) 2 | if visual>0 3 | Fgraph = spm_figure('GetWin','Graphics'); 4 | Finter = spm_figure('GetWin','Interactive'); 5 | end 6 | 7 | cflags.cost_fun = costfun; 8 | cflags.sep = sep; 9 | cflags.tol = tol; 10 | cflags.fwhm = fwhm; 11 | cflags.params = params; 12 | cflags.graphics = graphics; 13 | 14 | VG = strcat(imref,',1'); 15 | VF = strcat(imflo,',1'); 16 | 17 | %disp('Matlab internal reference image:'); disp(imref); 18 | %disp('Matlab internal floating image:'); disp(imflo); 19 | %disp(cflags); 20 | %disp(tol); 21 | 22 | spm_jobman('initcfg') 23 | x = spm_coreg(VG, VF, cflags); 24 | M = spm_matrix(x); 25 | 26 | %disp('translations and rotations:'); disp(x); 27 | %disp('affine matrix:'); disp(M); 28 | end 29 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-case-conflict 9 | - id: check-docstring-first 10 | - id: check-executables-have-shebangs 11 | - id: check-toml 12 | - id: check-merge-conflict 13 | - id: check-yaml 14 | - id: debug-statements 15 | - id: end-of-file-fixer 16 | - id: mixed-line-ending 17 | - id: sort-simple-yaml 18 | - id: trailing-whitespace 19 | - repo: local 20 | hooks: 21 | - id: todo 22 | name: Check TODO 23 | language: pygrep 24 | args: [-i] 25 | entry: TODO 26 | types: [text] 27 | exclude: ^(.pre-commit-config.yaml|.github/workflows/test.yml)$ 28 | - repo: https://github.com/PyCQA/flake8 29 | rev: 7.1.1 30 | hooks: 31 | - id: flake8 32 | args: [-j8] 33 | additional_dependencies: 34 | - flake8-broken-line 35 | - flake8-bugbear 36 | - flake8-comprehensions 37 | - flake8-debugger 38 | - flake8-isort 39 | - flake8-pyproject 40 | - flake8-string-format 41 | - repo: https://github.com/google/yapf 42 | rev: v0.43.0 43 | hooks: 44 | - id: yapf 45 | args: [-i] 46 | additional_dependencies: [toml] 47 | - repo: https://github.com/PyCQA/isort 48 | rev: 6.0.0 49 | hooks: 50 | - id: isort 51 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from textwrap import dedent 3 | 4 | from miutil.fdio import Path 5 | from pytest import fixture, skip 6 | 7 | HOME = Path(getenv("DATA_ROOT", "~")).expanduser() 8 | 9 | 10 | @fixture(scope="session") 11 | def folder_in(): 12 | Ab_PET_mMR_test = HOME / "Ab_PET_mMR_test" 13 | if not Ab_PET_mMR_test.is_dir(): 14 | skip( 15 | dedent("""\ 16 | Cannot find Ab_PET_mMR_test in ${DATA_ROOT:-~} (%s). 17 | Try running `python -m tests` to download it. 18 | """) % HOME) 19 | return Ab_PET_mMR_test 20 | 21 | 22 | @fixture(scope="session") 23 | def folder_ref(folder_in): 24 | Ab_PET_mMR_ref = folder_in / "testing_reference" / "Ab_PET_mMR_ref" 25 | if not Ab_PET_mMR_ref.is_dir(): 26 | skip( 27 | dedent("""\ 28 | Cannot find Ab_PET_mMR_ref in 29 | ${DATA_ROOT:-~}/testing_reference (%s/testing_reference). 30 | Try running `python -m tests` to download it. 31 | """) % HOME) 32 | return Ab_PET_mMR_ref 33 | 34 | 35 | @fixture 36 | def PET(folder_ref): 37 | res = folder_ref / "basic" / "17598013_t-3000-3600sec_itr-4_suvr.nii.gz" 38 | assert res.is_file() 39 | return res 40 | 41 | 42 | @fixture 43 | def MRI(folder_in): 44 | res = folder_in / "T1w_N4" / "t1_S00113_17598013_N4bias_cut.nii.gz" 45 | assert res.is_file() 46 | return res 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SPM12 2 | ===== 3 | 4 | |Version| |Py-Versions| |Tests| |Coverage| |DOI| |LICENCE| 5 | 6 | Thin Python wrappers around `Statistical Parametric Mapping `_. 7 | 8 | |PET-MR Coregistration| 9 | 10 | 11 | Install 12 | ------- 13 | 14 | Currently requires MATLAB. May change in future. 15 | 16 | .. code:: sh 17 | 18 | python -m pip install -U spm12 19 | python -m spm12 20 | 21 | 22 | Examples 23 | -------- 24 | 25 | See `examples/demo.ipynb `_. 26 | 27 | 28 | Contributing 29 | ------------ 30 | 31 | See `CONTRIBUTING.md `_. 32 | 33 | .. |PET-MR Coregistration| image:: https://raw.githubusercontent.com/AMYPAD/images/master/spm12/pet_mr_coreg.png 34 | .. |Tests| image:: https://img.shields.io/github/actions/workflow/status/AMYPAD/SPM12/test.yml?branch=master&logo=GitHub 35 | :target: https://github.com/AMYPAD/SPM12/actions 36 | .. |Coverage| image:: https://codecov.io/gh/AMYPAD/SPM12/branch/master/graph/badge.svg 37 | :target: https://codecov.io/gh/AMYPAD/SPM12 38 | .. |Version| image:: https://img.shields.io/pypi/v/spm12.svg?logo=python&logoColor=white 39 | :target: https://github.com/AMYPAD/SPM12/releases 40 | .. |Py-Versions| image:: https://img.shields.io/pypi/pyversions/spm12.svg?logo=python&logoColor=white 41 | :target: https://pypi.org/project/spm12 42 | .. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4272003.svg 43 | :target: https://doi.org/10.5281/zenodo.4272003 44 | .. |LICENCE| image:: https://img.shields.io/pypi/l/spm12.svg 45 | :target: https://raw.githubusercontent.com/AMYPAD/spm12/master/LICENCE.md 46 | -------------------------------------------------------------------------------- /tests/test_regseg.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import numpy as np 4 | from miutil.imio import nii 5 | from pytest import mark 6 | 7 | from spm12 import regseg 8 | 9 | MRI2PET = np.array([[0.99990508, 0.00800995, 0.01121016, -0.68164088], 10 | [-0.00806219, 0.99995682, 0.00462244, -1.16235105], 11 | [-0.01117265, -0.00471238, 0.99992648, -1.02167229], [0.0, 0.0, 0.0, 1.0]]) 12 | no_matlab_warn = mark.filterwarnings("ignore:.*collections.abc:DeprecationWarning") 13 | no_scipy_warn = mark.filterwarnings("ignore:numpy.ufunc size changed.*:RuntimeWarning") 14 | 15 | 16 | def assert_equal_arrays(x, y, nmse_tol=0, denan=True): 17 | if denan: 18 | x, y = map(np.nan_to_num, (x, y)) 19 | if nmse_tol: 20 | if ((x - y)**2).mean() / (y**2).mean() < nmse_tol: 21 | return 22 | elif (x == y).all(): 23 | return 24 | raise ValueError( 25 | dedent(f""" Unequal arrays:x != y. min/mean/max(std): 26 | x: {x.min():.3g}/{x.mean():.3g}/{x.max():.3g}({x.std():.3g}) 27 | y: {y.min():.3g}/{y.mean():.3g}/{y.max():.3g}({y.std():.3g}) 28 | """)) 29 | 30 | 31 | @no_scipy_warn 32 | @no_matlab_warn 33 | def test_resample(PET, MRI, tmp_path): 34 | res = regseg.resample_spm(PET, MRI, MRI2PET, outpath=tmp_path / "resample") 35 | res = nii.getnii(res) 36 | ref = nii.getnii(PET) 37 | assert res.shape == ref.shape 38 | assert not np.isnan(res).all() 39 | 40 | 41 | @no_scipy_warn 42 | @no_matlab_warn 43 | def test_coreg(PET, MRI, tmp_path): 44 | res = regseg.coreg_spm(PET, MRI, outpath=tmp_path / "coreg") 45 | assert_equal_arrays(res["affine"], MRI2PET, 5e-4) 46 | 47 | outpath = tmp_path / "resamp" 48 | res = regseg.resample_spm(PET, MRI, res["affine"], outpath=outpath) 49 | ref = regseg.resample_spm(PET, MRI, MRI2PET, outpath=outpath) 50 | assert_equal_arrays(nii.getnii(res), nii.getnii(ref), 1e-4) 51 | -------------------------------------------------------------------------------- /spm12/amypad_seg.m: -------------------------------------------------------------------------------- 1 | function [param,invdef,fordef] = amypad_seg(f_mri, spm_path, nat_gm, nat_wm, nat_csf, store_fwd, store_inv, visual) 2 | job.channel.vols = {strcat(f_mri,',1')}; 3 | job.channel.biasreg = 0.001; 4 | job.channel.biasfwhm = 60; 5 | job.channel.write = [0, 0]; 6 | job.tissue(1).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,1']}; 7 | job.tissue(1).ngaus = 1; 8 | job.tissue(1).native = [nat_gm, 0]; 9 | job.tissue(1).warped = [0, 0]; 10 | job.tissue(2).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,2']}; 11 | job.tissue(2).ngaus = 1; 12 | job.tissue(2).native = [nat_wm, 0]; 13 | job.tissue(2).warped = [0, 0]; 14 | job.tissue(3).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,3']}; 15 | job.tissue(3).ngaus = 2; 16 | job.tissue(3).native = [nat_csf, 0]; 17 | job.tissue(3).warped = [0, 0]; 18 | job.tissue(4).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,4']}; 19 | job.tissue(4).ngaus = 3; 20 | job.tissue(4).native = [0, 0]; 21 | job.tissue(4).warped = [0, 0]; 22 | job.tissue(5).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,5']}; 23 | job.tissue(5).ngaus = 4; 24 | job.tissue(5).native = [0, 0]; 25 | job.tissue(5).warped = [0, 0]; 26 | job.tissue(6).tpm = {[spm_path, filesep, 'tpm', filesep, 'TPM.nii,6']}; 27 | job.tissue(6).ngaus = 2; 28 | job.tissue(6).native = [0, 0]; 29 | job.tissue(6).warped = [0, 0]; 30 | job.warp.mrf = 1; 31 | job.warp.cleanup = 1; 32 | job.warp.reg = [0, 0.001, 0.5, 0.05, 0.2]; 33 | job.warp.affreg = 'mni'; 34 | job.warp.fwhm = 0; 35 | job.warp.samp = 3; 36 | job.warp.write = [store_fwd, store_inv]; 37 | if visual>0 38 | Finter = spm_figure('GetWin','Interactive'); 39 | end 40 | spm_jobman('initcfg'); 41 | segout = spm_preproc_run(job); 42 | param = segout.param{1}; 43 | invdef = segout.invdef{1}; 44 | fordef = segout.fordef{1}; 45 | %disp(segout); 46 | end 47 | -------------------------------------------------------------------------------- /.github/workflows/comment-bot.yml: -------------------------------------------------------------------------------- 1 | name: Comment Bot 2 | on: 3 | issue_comment: {types: [created]} 4 | pull_request_review_comment: {types: [created]} 5 | jobs: 6 | tag: # /tag 7 | if: startsWith(github.event.comment.body, '/tag ') 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | issues: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: React Seen 16 | uses: actions/github-script@v7 17 | with: 18 | script: | 19 | const perm = await github.rest.repos.getCollaboratorPermissionLevel({ 20 | owner: context.repo.owner, repo: context.repo.repo, 21 | username: context.payload.comment.user.login}) 22 | post = (context.eventName == "issue_comment" 23 | ? github.rest.reactions.createForIssueComment 24 | : github.rest.reactions.createForPullRequestReviewComment) 25 | if (!["admin", "write"].includes(perm.data.permission)){ 26 | post({ 27 | owner: context.repo.owner, repo: context.repo.repo, 28 | comment_id: context.payload.comment.id, content: "laugh"}) 29 | throw "Permission denied for user " + context.payload.comment.user.login 30 | } 31 | post({ 32 | owner: context.repo.owner, repo: context.repo.repo, 33 | comment_id: context.payload.comment.id, content: "eyes"}) 34 | github-token: ${{ secrets.GH_TOKEN || github.token }} 35 | - name: Tag Commit 36 | run: | 37 | git clone https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY} repo 38 | git -C repo tag $(echo "$BODY" | awk '{print $2" "$3}') 39 | git -C repo push --tags 40 | rm -rf repo 41 | env: 42 | BODY: ${{ github.event.comment.body }} 43 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN || github.token }} 44 | - name: React Success 45 | uses: actions/github-script@v7 46 | with: 47 | script: | 48 | post = (context.eventName == "issue_comment" 49 | ? github.rest.reactions.createForIssueComment 50 | : github.rest.reactions.createForPullRequestReviewComment) 51 | post({ 52 | owner: context.repo.owner, repo: context.repo.repo, 53 | comment_id: context.payload.comment.id, content: "rocket"}) 54 | github-token: ${{ secrets.GH_TOKEN || github.token }} 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | schedule: [{cron: '10 23 * * 6'}] # M H d m w (Sat at 23:10) 6 | jobs: 7 | test: 8 | if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) 9 | name: py${{ matrix.python }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python: [3.7, 3.11] 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: {fetch-depth: 0} 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python }} 20 | - run: pip install -U .[dev] 21 | - run: pytest --durations-min=1 22 | - uses: codecov/codecov-action@v5 23 | with: 24 | token: ${{ secrets.CODECOV_TOKEN }} 25 | matlab: 26 | if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) 27 | name: MATLAB py${{ matrix.python }} 28 | runs-on: [self-hosted, python, matlab] 29 | strategy: 30 | matrix: 31 | python: [3.7, 3.8] 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: {fetch-depth: 0} 35 | - name: Run setup-python 36 | run: setup-python -p${{ matrix.python }} 37 | - run: pip install -U .[dev] 'setuptools<66' # ignore matlab engine PEP440 non-compliance https://github.com/pypa/setuptools/issues/3772 38 | - run: pytest --durations-min=1 39 | - uses: codecov/codecov-action@v5 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | - name: Post Run setup-python 43 | run: setup-python -p${{ matrix.python }} -Dr 44 | if: ${{ always() }} 45 | deploy: 46 | needs: [test, matlab] 47 | name: PyPI Deploy 48 | environment: pypi 49 | permissions: {contents: write, id-token: write} 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | with: 54 | fetch-depth: 0 55 | - uses: actions/setup-python@v5 56 | with: {python-version: '3.x'} 57 | - id: dist 58 | uses: casperdcl/deploy-pypi@v2 59 | with: 60 | build: true 61 | upload: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} 62 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 63 | name: Release 64 | run: | 65 | changelog=$(git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD) 66 | tag="${GITHUB_REF#refs/tags/}" 67 | gh release create --title "spm12 $tag beta" --draft --notes "$changelog" "$tag" dist/${{ steps.dist.outputs.whl }} 68 | env: 69 | GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} 70 | -------------------------------------------------------------------------------- /spm12/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import lru_cache, wraps 3 | from os import fspath, path 4 | from textwrap import dedent 5 | 6 | from miutil.fdio import extractall 7 | from miutil.mlab import get_engine 8 | from miutil.web import urlopen_cached 9 | 10 | try: # py<3.9 11 | import importlib_resources as resources 12 | except ImportError: 13 | from importlib import resources 14 | 15 | __all__ = ["ensure_spm", "get_matlab", "spm_dir", "spm_dir_eng"] 16 | PATH_M = fspath(resources.files("spm12").resolve()) 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | def get_matlab(name=None): 21 | eng = get_engine(name=name) 22 | log.debug("adding wrappers (%s) to MATLAB path", PATH_M) 23 | eng.addpath(PATH_M, nargout=0) 24 | return eng 25 | 26 | 27 | def spm_dir(cache="~/.spm12", version=12): 28 | """Internal SPM12 directory""" 29 | cache = path.expanduser(cache) 30 | if str(version) != "12": 31 | raise NotImplementedError 32 | return path.join(cache, "spm12") 33 | 34 | 35 | def spm_dir_eng(name=None, cache="~/.spm12", version=12): 36 | """ 37 | Computed SPM12 directory. 38 | Uses matlab to find SPM12 directly, 39 | so may prefer user-installed version to the internal `spm_dir`. 40 | """ 41 | eng = ensure_spm(name=name, cache=cache, version=version) 42 | return path.dirname(eng.which("spm_jobman")) 43 | 44 | 45 | @lru_cache() 46 | @wraps(get_matlab) 47 | def ensure_spm(name=None, cache="~/.spm12", version=12): 48 | eng = get_matlab(name) 49 | cache = path.expanduser(cache) 50 | addpath = spm_dir(cache=cache, version=version) 51 | if path.exists(addpath): 52 | eng.addpath(addpath) 53 | if not eng.exist("spm_jobman"): 54 | log.warning("MATLAB could not find SPM.") 55 | try: 56 | log.info("Downloading to %s", cache) 57 | with urlopen_cached( 58 | "https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip", 59 | cache, 60 | ) as fd: 61 | extractall(fd, cache) 62 | eng.addpath(addpath) 63 | if not eng.exist("spm_jobman"): 64 | raise RuntimeError("MATLAB could not find SPM.") 65 | log.info("Installed") 66 | except: # NOQA: E722,B001 67 | raise ImportError( 68 | dedent("""\ 69 | MATLAB could not find SPM. 70 | Please follow installation instructions at 71 | https://en.wikibooks.org/wiki/SPM/Download 72 | Make sure to add SPM to MATLAB's path using `startup.m` 73 | """)) 74 | 75 | found = eng.which("spm_jobman") 76 | if path.realpath(path.dirname(found)) != path.realpath(addpath): 77 | log.warning(f"Internal ({addpath}) does not match detected ({found}) SPM12.\n" 78 | "This means `spm_dir()` is likely to fail - use `spm_dir_eng()` instead.") 79 | return eng 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | write_to = "spm12/_dist_ver.py" 7 | write_to_template = "__version__ = '{version}'\n" 8 | 9 | [tool.setuptools.packages.find] 10 | exclude = ["tests"] 11 | 12 | [tool.setuptools.package-data] 13 | "*" = ["*.md", "*.rst", "*.m"] 14 | 15 | [project.urls] 16 | documentation = "https://github.com/AMYPAD/SPM12/#SPM12" 17 | repository = "https://github.com/AMYPAD/SPM12" 18 | changelog = "https://github.com/AMYPAD/SPM12/releases" 19 | upstream-project = "https://www.fil.ion.ucl.ac.uk/spm" 20 | 21 | [project] 22 | name = "spm12" 23 | dynamic = ["version"] 24 | maintainers = [{name = "Casper da Costa-Luis", email = "casper.dcl@physics.org"}] 25 | description = "Statistical Parametric Mapping" 26 | readme = "README.rst" 27 | requires-python = ">=3.7" 28 | keywords = ["fMRI", "PET", "SPECT", "EEG", "MEG"] 29 | license = {text = "Apache-2.0"} 30 | classifiers = [ 31 | "Development Status :: 4 - Beta", 32 | "Intended Audience :: Developers", 33 | "Intended Audience :: Education", 34 | "Intended Audience :: Healthcare Industry", 35 | "Intended Audience :: Science/Research", 36 | "License :: OSI Approved :: Apache Software License", 37 | "Operating System :: Microsoft :: Windows", 38 | "Operating System :: POSIX :: Linux", 39 | "Programming Language :: Other Scripting Engines", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "Programming Language :: Python :: 3.10", 45 | "Programming Language :: Python :: 3.11", 46 | "Programming Language :: Python :: 3 :: Only", 47 | "Topic :: Scientific/Engineering :: Medical Science Apps.", 48 | "Topic :: Software Development :: Libraries", 49 | "Topic :: System :: Installation/Setup"] 50 | dependencies = ['importlib_resources; python_version < "3.9"', "argopt", "miutil[nii,web]>=0.12.0", "numpy", "scipy"] 51 | 52 | [project.optional-dependencies] 53 | dev = ["pytest>=6", "pytest-cov", "pytest-timeout", "pytest-xdist"] 54 | demo = ["miutil[plot]>=0.3.0", "matplotlib"] 55 | 56 | [project.scripts] 57 | spm12 = "spm12.cli:main" 58 | 59 | [tool.flake8] 60 | max_line_length = 99 61 | extend_ignore = ["E261"] 62 | exclude = [".git", "__pycache__", "build", "dist", ".eggs"] 63 | 64 | [tool.yapf] 65 | spaces_before_comment = [15, 20] 66 | arithmetic_precedence_indication = true 67 | allow_split_before_dict_value = false 68 | coalesce_brackets = true 69 | column_limit = 99 70 | each_dict_entry_on_separate_line = false 71 | space_between_ending_comma_and_closing_bracket = false 72 | split_before_named_assigns = false 73 | split_before_closing_bracket = false 74 | blank_line_before_nested_class_or_def = false 75 | 76 | [tool.isort] 77 | profile = "black" 78 | line_length = 99 79 | multi_line_output = 4 80 | known_first_party = ["spm12", "tests"] 81 | 82 | [tool.pytest.ini_options] 83 | minversion = "6.0" 84 | timeout = 300 85 | log_level = "INFO" 86 | python_files = ["tests/test_*.py"] 87 | testpaths = ["tests"] 88 | addopts = "-v --tb=short -rxs -W=error --log-level=debug -n=auto --durations=0 --durations-min=1 --cov=spm12 --cov-report=term-missing --cov-report=xml" 89 | filterwarnings = ["ignore:numpy.ufunc size changed.*:RuntimeWarning"] 90 | -------------------------------------------------------------------------------- /examples/demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Coregistration using SPM12\n", 8 | "\n", 9 | "Python package requirements: `pip install spm12[demo]`" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from __future__ import print_function\n", 19 | "%matplotlib notebook\n", 20 | "\n", 21 | "from os import path, getenv\n", 22 | "\n", 23 | "from miutil.imio import nii\n", 24 | "from miutil.plot import imscroll\n", 25 | "import matplotlib.pyplot as plt\n", 26 | "import numpy as np\n", 27 | "\n", 28 | "from spm12 import regseg\n", 29 | "\n", 30 | "HOME = getenv(\"DATA_ROOT\", path.expanduser(\"~\"))\n", 31 | "DATA = path.join(HOME, \"Ab_PET_mMR_test\")\n", 32 | "MRI = path.join(DATA, \"T1w_N4\", \"t1_S00113_17598013_N4bias_cut.nii.gz\")\n", 33 | "PET = path.join(\n", 34 | " DATA, \"testing_reference\", \"Ab_PET_mMR_ref\", \"basic\", \"17598013_t-3000-3600sec_itr-4_suvr.nii.gz\")\n", 35 | "if not path.exists(DATA):\n", 36 | " raise ValueError(\"\"\"\\\n", 37 | "Cannot find Ab_PET_mMR_test in ${DATA_ROOT:-~} (%s).\n", 38 | "Get it from https://zenodo.org/record/3877529\n", 39 | "\"\"\" % HOME)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "outpath = \".\"\n", 49 | "print(\"input PET shape:\", nii.getnii(PET).shape)\n", 50 | "print(\"input MRI shape:\", nii.getnii(MRI).shape)\n", 51 | "print(\"registering (~1min)\")\n", 52 | "reg = regseg.coreg_spm(PET, MRI, outpath=outpath)\n", 53 | "print(\"affine matrix:\")\n", 54 | "print(reg[\"affine\"])\n", 55 | "out = regseg.resample_spm(PET, MRI, reg[\"affine\"], outpath=outpath)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "def get_vol(fname):\n", 65 | " x = np.nan_to_num(nii.getnii(fname)[:, 100:-120, 120:-120, None])\n", 66 | " x -= x.min()\n", 67 | " x /= np.percentile(x, 99)\n", 68 | " x[x > 1] = 1\n", 69 | " return x\n", 70 | "\n", 71 | "pet, mri = map(get_vol, [PET, out])\n", 72 | "zer = np.zeros_like(pet)\n", 73 | "slicer = imscroll(\n", 74 | " {\n", 75 | " \"PET\": np.concatenate([pet, zer, zer], axis=-1),\n", 76 | " \"Registered MRI\": np.concatenate([zer, zer, mri], axis=-1),\n", 77 | " \"Overlay\": np.concatenate([pet * 0.6, zer, mri], axis=-1),\n", 78 | " },\n", 79 | " cmaps=[\"Reds_r\", \"Blues_r\", None],\n", 80 | " figsize=(9.5, 4),\n", 81 | " nrows=1, frameon=False)\n", 82 | "slicer(70)\n", 83 | "#plt.savefig(path.join(outpath, \"pet_mr_coreg.png\"))" 84 | ] 85 | } 86 | ], 87 | "metadata": { 88 | "kernelspec": { 89 | "display_name": "Python 3", 90 | "language": "python", 91 | "name": "python3" 92 | }, 93 | "language_info": { 94 | "codemirror_mode": { 95 | "name": "ipython" 96 | }, 97 | "file_extension": ".py", 98 | "mimetype": "text/x-python", 99 | "name": "python", 100 | "nbconvert_exporter": "python" 101 | } 102 | }, 103 | "nbformat": 4, 104 | "nbformat_minor": 2 105 | } 106 | -------------------------------------------------------------------------------- /spm12/setup_rt.py: -------------------------------------------------------------------------------- 1 | ''' 2 | All setup for standalone SPM12 using MATLAB Runtime 3 | ''' 4 | __author__ = "Pawel Markiewicz" 5 | __copyright__ = "Copyright 2023" 6 | 7 | import logging 8 | import os 9 | import platform 10 | import subprocess 11 | import zipfile 12 | from pathlib import Path 13 | from textwrap import dedent 14 | 15 | import requests 16 | from miutil import create_dir 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | # > MATLAB standalone: 21 | mat_core = 'https://ssd.mathworks.com/supportfiles/downloads/' 22 | mwin = mat_core + ('R2019b/Release/9/deployment_files/installer/complete/' 23 | 'win64/MATLAB_Runtime_R2019b_Update_9_win64.zip') 24 | mlnx = mat_core + ('R2022b/Release/7/deployment_files/installer/complete/' 25 | 'glnxa64/MATLAB_Runtime_R2022b_Update_7_glnxa64.zip') 26 | 27 | # > SPM12 stand-alone core address: 28 | spm_core = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/utopia/spm12/' 29 | swin = spm_core + 'spm12_r7771_Windows_R2019b.zip' 30 | slnx = spm_core + 'spm12_r7771_Linux_R2022b.zip' 31 | smac = spm_core + 'spm12_r7771_macOS_R2022b.zip' 32 | 33 | spmsa_fldr_name = '.spmruntime' 34 | 35 | 36 | def check_platform(): 37 | if platform.system() not in ['Windows']: # 'Darwin', 'Linux' 38 | log.error( 39 | dedent(f"""\ 40 | currently the operating system is not supported: {platform.system()} 41 | only Windows is supported (for now, Linux and macOS coming soon).""")) 42 | raise SystemError('unknown operating system (OS).') 43 | return {'Linux': 0, 'Windows': 1, 'Darwin': 2}.get(platform.system(), None) 44 | 45 | 46 | def get_user_folder(): 47 | return Path(os.path.expanduser("~")) 48 | 49 | 50 | def check_matlab_rt(): 51 | ''' check if MATLA Runtime exists and what is its position in the 52 | PATH variable relative to standard MATLAB installation (if 53 | exists) 54 | ''' 55 | 56 | # > get the environmental variables (PATH in particular) 57 | ospath = os.environ['PATH'] 58 | ospath = ospath.split(';') 59 | 60 | # > position of MATLAB Runtime and MATLAB (if any exists) 61 | matrt_pos = None 62 | mat_pos = None 63 | for ip, p in enumerate(ospath): 64 | if 'MATLAB' in p and 'runtime' in p.lower() and 'v97' in p: 65 | matrt_pos = ip 66 | if 'MATLAB' in p and 'runtime' not in p.lower(): 67 | mat_pos = ip 68 | 69 | if not (mat_pos is None or matrt_pos is None) and mat_pos < matrt_pos: 70 | raise ValueError( 71 | "MATLAB Runtime Path needs to be above standard MATLAB installation Path.\n" 72 | "Change the environment variable Path accordingly.") 73 | return matrt_pos is not None 74 | 75 | 76 | def standalone_path(): 77 | """get the path to standalone SPM""" 78 | # > user main folder 79 | usrpth = get_user_folder() 80 | # > core SPM 12 standalone/runtime path 81 | spmsa_fldr = usrpth / spmsa_fldr_name 82 | return spmsa_fldr / 'spm12' / 'spm12.exe' 83 | 84 | 85 | def check_standalone(): 86 | """Check if the standalone SPM12 is already installed with the correct MATLAB Runtime""" 87 | fspm = standalone_path() 88 | return fspm.is_file() and check_matlab_rt() 89 | 90 | 91 | def ensure_standalone(): 92 | """Ensure the standalone SPM12 is installed with the correct MATLAB Runtime""" 93 | if not check_standalone(): 94 | log.warning('MATLAB Runtime for SPM12 is not yet installed on your machine') 95 | response = input('Do you want to install MATLAB Runtime? [y/n]') 96 | if response in ['y', 'Y', 'yes']: 97 | install_standalone() 98 | 99 | 100 | def get_file(url, save_path): 101 | response = requests.get(url) 102 | 103 | print('Downloading setup file - this make take a while (it is Matlab) ...') 104 | 105 | # Check if the request was successful (status code 200) 106 | if response.status_code == 200: 107 | with open(save_path, 'wb') as file: 108 | file.write(response.content) 109 | print(f'Successfully downloaded: {save_path}') 110 | else: 111 | print(f'Failed to download file. Status code: {response.status_code}') 112 | 113 | 114 | def unzip_file(zip_path, extract_path): 115 | with zipfile.ZipFile(zip_path, 'r') as zip_ref: 116 | zip_ref.extractall(extract_path) 117 | 118 | 119 | # INSTALL MATLAB RUNTIME and STANDALONE SPM12 120 | 121 | 122 | def install_standalone(): 123 | """Install Matlab Runtime and the associated SPM12""" 124 | # > select the OS (0: Linux, 1: Windows, 2: MacOS) 125 | os_sel = check_platform() 126 | log.info('you are currently using OS platform: %s', platform.system()) 127 | 128 | if os_sel != 1: 129 | log.error('the operating system is yet supported') 130 | raise SystemError('OS not supported') 131 | 132 | # > user main folder 133 | usrpth = get_user_folder() 134 | # > core destination path 135 | spmsa_fldr = usrpth / spmsa_fldr_name 136 | create_dir(spmsa_fldr) 137 | # > downloads destination 138 | dpth = spmsa_fldr / 'downloads' 139 | create_dir(dpth) 140 | 141 | if os_sel == 1: 142 | if not check_matlab_rt(): 143 | # MATLAB Runtime Installation 144 | fmwin = dpth / os.path.basename(mwin) 145 | if not fmwin.is_file(): 146 | get_file(mwin, fmwin) 147 | 148 | # > unzip to MATLAB runtime setup folder 149 | matrun_setup = fmwin.parent / 'matlab_runtime' 150 | unzip_file(fmwin, matrun_setup) 151 | 152 | matrun_sexe = [str(f) for f in matrun_setup.iterdir() if f.name == 'setup.exe'] 153 | if len(matrun_sexe) != 1: 154 | raise FileExistsError( 155 | 'Matlab runtime setup executable does not exists or it is confusing') 156 | 157 | try: 158 | print("AmyPET:>> Running Matlab Runtime Installation - " 159 | "please approve by pressing yes for administrative privileges") 160 | subprocess.run(['powershell', 'Start-Process', matrun_sexe[0], '-Verb', 'Runas'], 161 | check=True) 162 | print("Setup started successfully.") 163 | except subprocess.CalledProcessError as e: 164 | print(f"Error: {e}") 165 | 166 | if not check_standalone(): 167 | log.warning('MATLAB Runtime not yet installed ...') 168 | 169 | else: 170 | log.info('MATLAB Runtime already installed') 171 | 172 | # SPM12 173 | # > check if SPM12 is already installed 174 | if not check_standalone(): 175 | fswin = dpth / os.path.basename(swin) 176 | if not fswin.is_file(): 177 | get_file(swin, fswin) 178 | 179 | unzip_file(fswin, spmsa_fldr) 180 | else: 181 | log.info('SPM12 standalone already installed') 182 | 183 | fspm = standalone_path() 184 | 185 | if not fspm.is_file(): 186 | raise FileExistsError('The SPM12 executable has not been installed or is missing') 187 | -------------------------------------------------------------------------------- /spm12/standalone.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Standalone SPM12 functionalities running on MATLAB Runtime 3 | ''' 4 | __author__ = "Pawel Markiewicz" 5 | __copyright__ = "Copyright 2023" 6 | 7 | import logging 8 | import os 9 | import subprocess 10 | from pathlib import Path, PurePath 11 | 12 | import numpy as np 13 | from miutil import create_dir 14 | 15 | import spm12 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | fmri = Path('D:/data/reg_test/04000177_MRI_T1_N4_N4bias_com-modified.nii') 20 | fpet = Path( 21 | 'D:/data/reg_test/UR-aligned_4-summed-frames_DY_MRAC_20MIN__PETBrain_static_com-modified.nii') 22 | 23 | 24 | def standalone_coreg(fref, fflo, foth=None, cost_fun='nmi', sep=None, tol=None, fwhm=None): 25 | """ 26 | Run SPM12 coregistration using SPM12 standalone on MATLAB Runtime 27 | Arguments: 28 | fref: file path to the reference image (uncompressed NIfTI) 29 | fflo: file path to the floating image (uncompressed NIfTI) 30 | sep: default [4, 2] 31 | tol: default [0.02, 0.02, 0.02, 0.001, 0.001, 0.001, 0.01, 0.01, 0.01, 0.001, 0.001, 0.001] 32 | fwhm: default [7, 7] 33 | """ 34 | if sep is None: 35 | sep = [4, 2] 36 | if tol is None: 37 | tol = [0.02, 0.02, 0.02, 0.001, 0.001, 0.001, 0.01, 0.01, 0.01, 0.001, 0.001, 0.001] 38 | if fwhm is None: 39 | fwhm = [7, 7] 40 | 41 | spm12.ensure_standalone() 42 | fspm = spm12.standalone_path() 43 | 44 | # > reference and floating images 45 | fref = str(fref) 46 | fflo = str(fflo) 47 | 48 | # > other image 49 | if foth is None: 50 | foth = '' 51 | else: 52 | foth = str(foth) 53 | 54 | # > change the parameters to strings for MATLAB scripting 55 | sep_str = ' '.join([str(s) for s in sep]) 56 | tol_str = ' '.join([str(s) for s in tol]) 57 | fwhm_str = ' '.join([str(s) for s in fwhm]) 58 | 59 | # > form full set of commands for SPM12 coregistration 60 | coreg_batch_txt = "spm('defaults', 'PET');\nspm_jobman('initcfg');\n\n" 61 | coreg_batch_txt += f"matlabbatch{{1}}.spm.spatial.coreg.estimate.ref = {{'{fref},1'}};\n" 62 | coreg_batch_txt += f"matlabbatch{{1}}.spm.spatial.coreg.estimate.source = {{'{fflo},1'}};\n" 63 | coreg_batch_txt += f"matlabbatch{{1}}.spm.spatial.coreg.estimate.other = {{'{foth},1'}};\n" 64 | coreg_batch_txt += ( 65 | f"matlabbatch{{1}}.spm.spatial.coreg.estimate.eoptions.cost_fun = '{cost_fun}';\n") 66 | coreg_batch_txt += f"matlabbatch{{1}}.spm.spatial.coreg.estimate.eoptions.sep = [{sep_str}];\n" 67 | coreg_batch_txt += f"matlabbatch{{1}}.spm.spatial.coreg.estimate.eoptions.tol = [{tol_str}];\n" 68 | coreg_batch_txt += ( 69 | f"matlabbatch{{1}}.spm.spatial.coreg.estimate.eoptions.fwhm = [{fwhm_str}];\n\n") 70 | coreg_batch_txt += "spm_jobman('run', matlabbatch);" 71 | 72 | fcoreg = fspm.parent.parent / 'spm_coreg_runtime.m' 73 | 74 | with open(fcoreg, 'w') as f: 75 | f.write(coreg_batch_txt) 76 | 77 | try: 78 | print('Running MATLAB Runtime SPM12 Coregistration...') 79 | subprocess.run([fspm, 'batch', fcoreg], check=True) 80 | print("SPM12 coregistration started successfully.") 81 | except subprocess.CalledProcessError as e: 82 | print(f"Coregistration error: {e}") 83 | 84 | return fcoreg 85 | 86 | 87 | # > Segmentation 88 | 89 | 90 | def standalone_seg(fmri, outpath=None, nat_gm=True, nat_wm=True, nat_csf=True, nat_bn=False, 91 | biasreg=0.001, biasfwhm=60, mrf_cleanup=1, cleanup=1, regulariser=None, 92 | affinereg='mni', fwhm=0, sampling=3, store_fwd=True, store_inv=True): 93 | ''' Segment MRI NIfTI image using standalone SPM12 with normalisation. 94 | Arguments: 95 | fmri: input T1w MRI image. 96 | nat_{gm,wm,csf,bn}: output native space grey matter, white matter 97 | CSF or bone segmentations. 98 | regulariser: default [0, 0.001, 0.5, 0.05, 0.2] 99 | store_{fwd,inv}: store forward and/or inverse deformation 100 | fields definitions. 101 | ''' 102 | if regulariser is None: 103 | regulariser = [0, 0.001, 0.5, 0.05, 0.2] 104 | if not spm12.check_standalone(): 105 | log.error("MATLAB Runtime or standalone SPM12 has not been correctly installed.") 106 | log.error("Attempting installation...") 107 | response = input('Do you want to install MATLAB Runtime? [y/n]') 108 | if response in ['y', 'Y', 'yes']: 109 | spm12.install_standalone() 110 | else: 111 | fspm = spm12.standalone_path() 112 | 113 | # > the path to the input T1w MRI image 114 | fmri = Path(fmri) 115 | f_mri = str(fmri) 116 | 117 | # > path to the TPM.nii internal file 118 | tpm_pth = str(fspm.parent / 'spm12_mcr' / 'spm12' / 'spm12' / 'tpm' / 'TPM.nii') 119 | 120 | # > regulariser string 121 | rglrsr_str = ' '.join([str(s) for s in regulariser]) 122 | 123 | # > writing deformations (forward and inverse) 124 | wrt_dfrm = [int(store_fwd), int(store_inv)] 125 | wrtdfrm_str = ' '.join([str(s) for s in wrt_dfrm]) 126 | 127 | nat1 = f'[{nat_gm:d} 0]' 128 | nat2 = f'[{nat_wm:d} 0]' 129 | nat3 = f'[{nat_csf:d} 0]' 130 | nat4 = f'[{nat_bn:d} 0]' 131 | 132 | seg_batch_txt = "spm('defaults', 'PET');\nspm_jobman('initcfg');\n\n" 133 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.channel.vols = {{'{f_mri},1'}};\n" 134 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.channel.biasreg = {biasreg};\n" 135 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.channel.biasfwhm = {biasfwhm};\n" 136 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.channel.write = [0 0];\n" 137 | 138 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(1).tpm = {{'{tpm_pth},1'}};\n" 139 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(1).ngaus = 1;\n" 140 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(1).native = {nat1};\n" 141 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(1).warped = [0 0];\n" 142 | 143 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(2).tpm = {{'{tpm_pth},2'}};\n" 144 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(2).ngaus = 1;\n" 145 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(2).native = {nat2};\n" 146 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(2).warped = [0 0];\n" 147 | 148 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(3).tpm = {{'{tpm_pth},3'}};\n" 149 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(3).ngaus = 2;\n" 150 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(3).native = {nat3};\n" 151 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(3).warped = [0 0];\n" 152 | 153 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(4).tpm = {{'{tpm_pth},4'}};\n" 154 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(4).ngaus = 3;\n" 155 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(4).native = {nat4};\n" 156 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(4).warped = [0 0];\n" 157 | 158 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(5).tpm = {{'{tpm_pth},5'}};\n" 159 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(5).ngaus = 4;\n" 160 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(5).native = [0 0];\n" 161 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(5).warped = [0 0];\n" 162 | 163 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.tissue(6).tpm = {{'{tpm_pth},6'}};\n" 164 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(6).ngaus = 2;\n" 165 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(6).native = [0 0];\n" 166 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.tissue(6).warped = [0 0];\n" 167 | 168 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.mrf = {mrf_cleanup};\n" 169 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.cleanup = {cleanup};\n" 170 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.reg = [{rglrsr_str}];\n" 171 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.affreg = '{affinereg}';\n" 172 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.fwhm = {fwhm};\n" 173 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.samp = {sampling};\n" 174 | seg_batch_txt += f"matlabbatch{{1}}.spm.spatial.preproc.warp.write = [{wrtdfrm_str}];\n" 175 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.warp.vox = NaN;\n" 176 | seg_batch_txt += "matlabbatch{1}.spm.spatial.preproc.warp.bb = [NaN NaN NaN;NaN NaN NaN];\n" 177 | 178 | seg_batch_txt += "spm_jobman('run', matlabbatch);" 179 | 180 | fseg = fspm.parent.parent / 'spm_seg_runtime.m' 181 | 182 | with open(fseg, 'w') as f: 183 | f.write(seg_batch_txt) 184 | 185 | # > attempt SPM12 segmentation 186 | try: 187 | print('Running MATLAB Runtime SPM12 segmentation...') 188 | subprocess.run([fspm, 'batch', fseg], check=True) 189 | print("SPM12 segmentation started successfully.") 190 | except subprocess.CalledProcessError as e: 191 | print(f"Segmentation error: {e}") 192 | 193 | opth = Path(outpath) if outpath is not None else fmri.parent 194 | create_dir(opth) 195 | 196 | fmri_fldr = fmri.parent 197 | 198 | outdct = {} 199 | for f in fmri_fldr.iterdir(): 200 | if f.name[:2] == 'c1': 201 | outdct['c1'] = opth / f.name 202 | elif f.name[:2] == 'c2': 203 | outdct['c2'] = opth / f.name 204 | elif f.name[:2] == 'c3': 205 | outdct['c3'] = opth / f.name 206 | elif f.name[:2] == 'c4': 207 | outdct['c4'] = opth / f.name 208 | elif f.name[-8:] == 'seg8.mat': 209 | outdct['param'] = opth / f.name 210 | elif f.name[:2] == 'y_': 211 | outdct['fordef'] = opth / f.name 212 | elif f.name[:3] == 'iy_': 213 | outdct['invdef'] = opth / f.name 214 | else: 215 | continue 216 | 217 | os.replace(f, opth / f.name) 218 | 219 | outdct['fbatch'] = fseg 220 | 221 | return outdct 222 | 223 | 224 | def standalone_normw(fdef, list4norm, bbox=None, voxsz=None, intrp=4, prfx='w', outpath=None): 225 | """ 226 | Write out normalised NIfTI images using definitions `fdef`. 227 | Arguments: 228 | voxsz: default [2, 2, 2]. 229 | """ 230 | if voxsz is None: 231 | voxsz = [2, 2, 2] 232 | if not spm12.check_standalone(): 233 | log.error("MATLAB Runtime or standalone SPM12 has not been correctly installed.") 234 | log.error("Attempting installation...") 235 | response = input('Do you want to install MATLAB Runtime? [y/n]') 236 | if response in ['y', 'Y', 'yes']: 237 | spm12.install_standalone() 238 | else: 239 | fspm = spm12.standalone_path() 240 | 241 | # > the path to the input T1w MRI image 242 | fdef = str(fdef) 243 | 244 | if isinstance(list4norm, (PurePath, str)): 245 | list4norm = [str(list4norm)] 246 | elif isinstance(list4norm, list): 247 | # > ensure paths are in strings 248 | list4norm = [str(s) for s in list4norm] 249 | 250 | # > form list of flies to be normalised 251 | lst2nrm = '\n'.join([f"'{s},1'" for s in list4norm]) 252 | 253 | if isinstance(voxsz, (int, float)): 254 | voxsz = [voxsz, voxsz, voxsz] 255 | voxstr = ' '.join([str(v) for v in voxsz]) 256 | 257 | if bbox is None: 258 | bbxstr = "NaN NaN NaN; NaN NaN NaN" 259 | elif isinstance(bbox, np.ndarray) and bbox.shape == (2, 3): 260 | bbxstr = '' 261 | for i in range(len(bbox)): 262 | bbxstr += ' '.join([str(e) for e in bbox[i]]) 263 | bbxstr += '; ' 264 | 265 | elif isinstance(bbox, list) and len(bbox) == 2: 266 | bbxstr = '' 267 | for i in range(len(bbox)): 268 | bbxstr += ' '.join([str(e) for e in bbox[i]]) 269 | bbxstr += '; ' 270 | else: 271 | raise ValueError("unrecognised format for bounding box") 272 | 273 | wnrm_batch_txt = "spm('defaults', 'PET');\nspm_jobman('initcfg');\n\n" 274 | wnrm_batch_txt += f"matlabbatch{{1}}.spm.spatial.normalise.write.subj.def = {{'{fdef}'}};\n" 275 | wnrm_batch_txt += ( 276 | f"matlabbatch{{1}}.spm.spatial.normalise.write.subj.resample = {{{lst2nrm}}};\n") 277 | wnrm_batch_txt += f"matlabbatch{{1}}.spm.spatial.normalise.write.woptions.bb = [{bbxstr}];\n" 278 | wnrm_batch_txt += f"matlabbatch{{1}}.spm.spatial.normalise.write.woptions.vox = [{voxstr}];\n" 279 | wnrm_batch_txt += f"matlabbatch{{1}}.spm.spatial.normalise.write.woptions.interp = {intrp};\n" 280 | wnrm_batch_txt += f"matlabbatch{{1}}.spm.spatial.normalise.write.woptions.prefix = '{prfx}';\n" 281 | 282 | wnrm_batch_txt += "spm_jobman('run', matlabbatch);" 283 | 284 | fwnrm = fspm.parent.parent / 'spm_writenorm_runtime.m' 285 | 286 | with open(fwnrm, 'w') as f: 287 | f.write(wnrm_batch_txt) 288 | 289 | # > attempt spm12 segmentation 290 | try: 291 | print('running MATLAB runtime SPM12 normalisation writing...') 292 | subprocess.run([fspm, 'batch', fwnrm], check=True) 293 | print("SPM12 write norm started successfully.") 294 | except subprocess.CalledProcessError as e: 295 | print(f"write normalisation error: {e}") 296 | 297 | opth = Path(outpath) if outpath is not None else Path(list4norm[0]).parent 298 | create_dir(opth) 299 | 300 | fwnrm_out = [] 301 | for f in list4norm: 302 | f = Path(f) 303 | fout = opth / (prfx + f.name) 304 | os.replace(f.parent / (prfx + f.name), fout) 305 | fwnrm_out.append(fout) 306 | 307 | return fwnrm_out 308 | -------------------------------------------------------------------------------- /spm12/regseg.py: -------------------------------------------------------------------------------- 1 | __author__ = ("Pawel J. Markiewicz", "Casper O. da Costa-Luis") 2 | 3 | import errno 4 | import logging 5 | import os 6 | import re 7 | import shutil 8 | from numbers import Number 9 | from pathlib import PurePath 10 | from textwrap import dedent 11 | 12 | import numpy as np 13 | import scipy.ndimage as ndi 14 | from miutil import create_dir, hasext 15 | from miutil.imio import nii 16 | 17 | from .setup_rt import ensure_standalone 18 | from .standalone import standalone_coreg, standalone_normw, standalone_seg 19 | from .utils import ensure_spm, spm_dir 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | 24 | def move_files(fin, opth): 25 | """ 26 | Move input file path fin to the output folder opth. 27 | """ 28 | fdst = os.path.join(opth, os.path.basename(fin)) 29 | shutil.move(fin, fdst) 30 | return fdst 31 | 32 | 33 | def glob_match(pttrn, pth): 34 | """ 35 | glob with regular expressions 36 | """ 37 | return (os.path.join(pth, f) for f in os.listdir(pth) if re.match(pttrn, f)) 38 | 39 | 40 | def fwhm2sig(fwhm, voxsize=2.0): 41 | return fwhm / (voxsize * (8 * np.log(2))**0.5) 42 | 43 | 44 | def smoothim(fim, fwhm=4, fout=""): 45 | """ 46 | Smooth image using Gaussian filter with FWHM given as an option. 47 | """ 48 | imd = nii.getnii(fim, output="all") 49 | imsmo = ndi.filters.gaussian_filter(imd["im"], fwhm2sig(fwhm, voxsize=imd["voxsize"]), 50 | mode="constant") 51 | if not fout: 52 | f = nii.file_parts(fim) 53 | fout = os.path.join(f[0], f"{f[1]}_smo{str(fwhm).replace('.', '-')}{f[2]}") 54 | nii.array2nii( 55 | imsmo, 56 | imd["affine"], 57 | fout, 58 | trnsp=( 59 | imd["transpose"].index(0), 60 | imd["transpose"].index(1), 61 | imd["transpose"].index(2), 62 | ), 63 | flip=imd["flip"], 64 | ) 65 | return {"im": imsmo, "fim": fout, "fwhm": fwhm, "affine": imd["affine"]} 66 | 67 | 68 | def get_bbox(fnii): 69 | """get the SPM equivalent of the bounding box for 70 | NIfTI image `fnii` which can be a dictionary or file. 71 | """ 72 | 73 | if isinstance(fnii, (str, PurePath)): 74 | niidct = nii.getnii(fnii, output="all") 75 | elif isinstance(fnii, dict) and "hdr" in fnii: 76 | niidct = fnii 77 | else: 78 | raise ValueError("incorrect input NIfTI file/dictionary") 79 | 80 | dim = niidct["hdr"]["dim"] 81 | corners = np.array([[1, 1, 1, 1], [1, 1, dim[3], 1], [1, dim[2], 1, 1], [1, dim[2], dim[3], 1], 82 | [dim[1], 1, 1, 1], [dim[1], 1, dim[3], 1], [dim[1], dim[2], 1, 1], 83 | [dim[1], dim[2], dim[3], 1]]) 84 | 85 | XYZ = np.dot(niidct["affine"][:3, :], corners.T) 86 | 87 | # FIXME: weird correction for SPM bounding box (??) 88 | crr = np.dot(niidct["affine"][:3, :3], [1, 1, 1]) 89 | 90 | # bounding box as matrix 91 | bbox = np.concatenate((np.min(XYZ, axis=1) - crr, np.max(XYZ, axis=1) - crr)) 92 | bbox.shape = (2, 3) 93 | 94 | return bbox 95 | 96 | 97 | def mat2array(matlab_mat): 98 | if hasattr(matlab_mat, '_data'): # matlab 0: 170 | smodct = smoothim(imrefu, fwhm_ref) 171 | # delete the previous version (non-smoothed) 172 | os.remove(imrefu) 173 | imrefu = smodct["fim"] 174 | log.info("smoothed the reference image with FWHM=%r and saved to\n%r", fwhm_ref, imrefu) 175 | 176 | # floating 177 | if hasext(imflo, "gz"): 178 | imflou = nii.nii_ugzip(imflo, outpath=opth) 179 | else: 180 | fnm = nii.file_parts(imflo)[1] + "_copy.nii" 181 | imflou = os.path.join(opth, fnm) 182 | shutil.copyfile(imflo, imflou) 183 | 184 | if fwhm_flo > 0: 185 | smodct = smoothim(imflou, fwhm_flo) 186 | # delete the previous version (non-smoothed) 187 | if not modify_nii: 188 | os.remove(imflou) 189 | else: 190 | # save the uncompressed and unsmoothed version 191 | imflou_ = imflou 192 | 193 | imflou = smodct["fim"] 194 | 195 | log.info("smoothed the floating image with FWHM=%r and saved to\n%r", fwhm_flo, imflou) 196 | 197 | if not standalone: 198 | # > ensure MATLAB and SPM 199 | eng = ensure_spm(matlab_eng_name) # get_matlab 200 | import matlab as ml 201 | 202 | # > run registration using standard MATLAB 203 | Mm, xm = eng.amypad_coreg( 204 | imrefu, 205 | imflou, 206 | costfun, 207 | ml.double(sep), 208 | ml.double(tol), 209 | ml.double(fwhm), 210 | ml.double(params), 211 | graphics, 212 | visual, 213 | nargout=2, 214 | ) 215 | 216 | # modify the affine of the floating image (as usually done in SPM) 217 | if modify_nii: 218 | eng.amypad_coreg_modify_affine(imflou_, Mm) 219 | out["freg"] = imflou_ 220 | 221 | # get the affine matrix 222 | M = mat2array(Mm) 223 | 224 | # get the translation and rotation parameters in a vector 225 | x = mat2array(xm) 226 | 227 | # > save affine 228 | create_dir(os.path.join(opth, "affine-spm")) 229 | 230 | if fname_aff == "": 231 | if pickname == "ref": 232 | faff = os.path.join( 233 | opth, 234 | "affine-spm", 235 | "affine-ref-" + nii.file_parts(imref)[1] + fcomment + ".npy", 236 | ) 237 | else: 238 | faff = os.path.join( 239 | opth, 240 | "affine-spm", 241 | "affine-flo-" + nii.file_parts(imflo)[1] + fcomment + ".npy", 242 | ) 243 | else: 244 | # add '.npy' extension if not in the affine output file name 245 | if not fname_aff.endswith(".npy"): 246 | fname_aff += ".npy" 247 | faff = os.path.join(opth, "affine-spm", fname_aff) 248 | 249 | # > save the affine transformation 250 | if save_arr: 251 | np.save(faff, M) 252 | if save_txt: 253 | faff = os.path.splitext(faff)[0] + ".txt" 254 | np.savetxt(faff, M) 255 | 256 | out["affine"] = M 257 | out["faff"] = faff 258 | out["rotations"] = x[3:] 259 | out["translations"] = x[:3] 260 | if output_eng: 261 | out["matlab_eng"] = eng 262 | 263 | else: 264 | # > Standalone SPM12 265 | if modify_nii: 266 | foth = imflou_ 267 | else: 268 | foth = None 269 | 270 | out['fbatch'] = standalone_coreg(imrefu, imflou, foth, cost_fun=costfun, sep=sep, tol=tol, 271 | fwhm=fwhm) 272 | 273 | out['freg'] = imflou_ 274 | 275 | # > delete the uncompressed files 276 | if del_uncmpr: 277 | os.remove(imrefu) 278 | os.remove(imflou) 279 | 280 | return out 281 | 282 | 283 | def resample_spm(imref, imflo, M, matlab_eng_name="", fwhm=0, intrp=1, which=1, mask=0, mean=0, 284 | outpath="", fimout="", fcomment="", prefix="r_", pickname="ref", 285 | del_ref_uncmpr=False, del_flo_uncmpr=False, del_out_uncmpr=False, 286 | standalone=False): 287 | log.debug( 288 | dedent("""\ 289 | ====================================================================== 290 | S P M inputs: 291 | > ref:' %r 292 | > flo:' %r 293 | ======================================================================"""), 294 | imref, 295 | imflo, 296 | ) 297 | eng = ensure_spm(matlab_eng_name) # get_matlab 298 | 299 | if not outpath and fimout: 300 | opth = os.path.dirname(fimout) or os.path.dirname(imflo) 301 | else: 302 | opth = outpath or os.path.dirname(imflo) 303 | log.debug("output path:%s", opth) 304 | create_dir(opth) 305 | 306 | # decompress if necessary 307 | if hasext(imref, "gz"): 308 | imrefu = nii.nii_ugzip(imref, outpath=opth) 309 | else: 310 | fnm = nii.file_parts(imref)[1] + "_copy.nii" 311 | imrefu = os.path.join(opth, fnm) 312 | shutil.copyfile(imref, imrefu) 313 | 314 | # floating 315 | if hasext(imflo, "gz"): 316 | imflou = nii.nii_ugzip(imflo, outpath=opth) 317 | else: 318 | fnm = nii.file_parts(imflo)[1] + "_copy.nii" 319 | imflou = os.path.join(opth, fnm) 320 | shutil.copyfile(imflo, imflou) 321 | 322 | if isinstance(M, str): 323 | if hasext(M, ".txt"): 324 | M = np.loadtxt(M) 325 | log.info("matrix M given in the form of text file") 326 | elif hasext(M, ".npy"): 327 | M = np.load(M) 328 | log.info("matrix M given in the form of NumPy file") 329 | else: 330 | raise IOError(errno.ENOENT, M, "Unrecognised file extension for the affine.") 331 | elif isinstance(M, (np.ndarray, np.generic)): 332 | log.info("matrix M given in the form of Numpy array") 333 | else: 334 | raise ValueError("unrecognised affine matrix format") 335 | 336 | # run the Matlab SPM resampling 337 | import matlab as ml 338 | 339 | eng.amypad_resample( 340 | imrefu, 341 | imflou, 342 | ml.double(M.tolist()), 343 | mask, 344 | mean, 345 | float(intrp), 346 | which, 347 | prefix, 348 | ) 349 | 350 | # -compress the output 351 | split = os.path.split(imflou) 352 | fim = os.path.join(split[0], prefix + split[1]) 353 | nii.nii_gzip(fim, outpath=opth) 354 | 355 | # delete the uncompressed 356 | if del_ref_uncmpr: 357 | os.remove(imrefu) 358 | if del_flo_uncmpr and os.path.isfile(imflou): 359 | os.remove(imflou) 360 | if del_out_uncmpr: 361 | os.remove(fim) 362 | 363 | # the compressed output naming 364 | if fimout: 365 | fout = os.path.join(opth, fimout) 366 | elif pickname == "ref": 367 | fout = os.path.join( 368 | opth, 369 | "affine_ref-" + nii.file_parts(imrefu)[1] + fcomment + ".nii.gz", 370 | ) 371 | elif pickname == "flo": 372 | fout = os.path.join( 373 | opth, 374 | "affine_flo-" + nii.file_parts(imflo)[1] + fcomment + ".nii.gz", 375 | ) 376 | # change the file name 377 | os.rename(fim + ".gz", fout) 378 | 379 | if fwhm > 0: 380 | smodct = smoothim(fout, fwhm) 381 | log.info("smoothed the resampled image with FWHM=%r and saved to\n%r", fwhm, smodct["fim"]) 382 | 383 | return fout 384 | 385 | 386 | def seg_spm( 387 | f_mri, 388 | spm_path=None, 389 | matlab_eng_name="", 390 | outpath=None, 391 | store_nat_gm=False, 392 | store_nat_wm=False, 393 | store_nat_csf=False, 394 | store_nat_bon=False, 395 | store_fwd=False, 396 | store_inv=False, 397 | visual=False, 398 | standalone=False, 399 | ): 400 | """ 401 | Normalisation/segmentation using SPM12. 402 | Args: 403 | f_mri: file path to the T1w MRI file 404 | spm_path(str): SPM path 405 | matlab_eng_name: name of the Python engine for Matlab. 406 | outpath: output folder path for the normalisation file output 407 | store_nat_*: stores native space segmentation output for either 408 | grey matter, white matter or CSF 409 | sotre_fwd/inv: stores forward/inverse normalisation definitions 410 | visual: shows the Matlab window progress 411 | """ 412 | 413 | # > output dictionary 414 | out = {} 415 | 416 | if not standalone: 417 | 418 | # > run SPM normalisation/segmentation using standard MATLAB 419 | # > get Matlab engine or use the provided one 420 | eng = ensure_spm(matlab_eng_name) 421 | if not spm_path: 422 | spm_path = spm_dir() 423 | 424 | param, invdef, fordef = eng.amypad_seg( 425 | f_mri, 426 | str(spm_path), 427 | float(store_nat_gm), 428 | float(store_nat_wm), 429 | float(store_nat_csf), 430 | float(store_fwd), 431 | float(store_inv), 432 | float(visual), 433 | nargout=3, 434 | ) 435 | 436 | if outpath is not None: 437 | create_dir(outpath) 438 | out["param"] = move_files(param, outpath) 439 | out["invdef"] = move_files(invdef, outpath) 440 | out["fordef"] = move_files(fordef, outpath) 441 | 442 | # move each tissue type to the output folder 443 | for c in glob_match(r"c\d*", os.path.dirname(param)): 444 | nm = os.path.basename(c)[:2] 445 | out[nm] = move_files(c, outpath) 446 | else: 447 | out["param"] = param 448 | out["invdef"] = invdef 449 | out["fordef"] = fordef 450 | 451 | for c in glob_match(r"c\d*", os.path.dirname(param)): 452 | nm = os.path.basename(c)[:2] 453 | out[nm] = c 454 | 455 | else: 456 | # > run standalone SPM12 using MATLAB Runtime (no license needed) 457 | out = standalone_seg(f_mri, outpath=outpath, nat_gm=store_nat_gm, nat_wm=store_nat_wm, 458 | nat_csf=store_nat_csf, nat_bn=store_nat_bon, biasreg=0.001, 459 | biasfwhm=60, mrf_cleanup=1, cleanup=1, 460 | regulariser=[0, 0.001, 0.5, 0.05, 0.2], affinereg='mni', fwhm=0, 461 | sampling=3, store_fwd=store_fwd, store_inv=store_inv) 462 | 463 | return out 464 | 465 | 466 | def normw_spm(f_def, files4norm, outpath=None, voxsz=2, intrp=4, bbox=None, matlab_eng_name="", 467 | standalone=False): 468 | """ 469 | Write normalisation output to NIfTI files using SPM12. 470 | Args: 471 | f_def: NIfTI file of definitions for non-rigid normalisation 472 | files4norm: list or single Path/string of input NIfTI file 473 | path(s) 474 | voxsz: voxel size of the output (normalised) images 475 | intrp: interpolation level used for the normalised images 476 | (4: B-spline, default) 477 | matlab_eng_name: name of the Python engine for Matlab. 478 | outpath: output folder path for the normalisation files 479 | """ 480 | 481 | if isinstance(files4norm, (str, PurePath)): 482 | files4norm = [str(files4norm)] 483 | elif isinstance(files4norm, list): 484 | files4norm = [str(f) for f in files4norm] 485 | else: 486 | raise ValueError("unknown input type for `files4norm`" 487 | " (only strings, Paths or list of Paths/strings is accepted)") 488 | 489 | if not standalone: 490 | import matlab as ml 491 | 492 | list4norm = [f + ',1' for f in files4norm] 493 | 494 | if bbox is None: 495 | bb = ml.double([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]]) 496 | elif isinstance(bbox, np.ndarray) and bbox.shape == (2, 3): 497 | bb = ml.double(bbox.tolist()) 498 | elif isinstance(bbox, list) and len(bbox) == 2: 499 | bb = ml.double(bbox) 500 | else: 501 | raise ValueError("unrecognised format for bounding box") 502 | 503 | if isinstance(voxsz, Number): 504 | voxsz = ml.double([voxsz] * 3) 505 | elif isinstance(voxsz, (np.ndarray, list)): 506 | if len(voxsz) != 3: 507 | raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") 508 | voxsz = ml.double(np.float64(voxsz)) 509 | else: 510 | raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") 511 | 512 | eng = ensure_spm(matlab_eng_name) # get_matlab 513 | eng.amypad_normw(f_def, list4norm, voxsz, float(intrp), bb) 514 | out = [] # output list 515 | 516 | if outpath is not None: 517 | create_dir(outpath) 518 | for f in files4norm: 519 | fpth = f 520 | # fpth = f.split(",")[0] 521 | out.append( 522 | move_files( 523 | os.path.join(os.path.dirname(fpth), "w" + os.path.basename(fpth)), 524 | outpath, 525 | )) 526 | else: 527 | out.append("w" + os.path.basename(f.split(",")[0])) 528 | 529 | # > Standalone SPM12 530 | else: 531 | out = standalone_normw(f_def, files4norm, bbox=bbox, voxsz=voxsz, intrp=intrp, prfx='w', 532 | outpath=outpath) 533 | 534 | return out 535 | --------------------------------------------------------------------------------