├── .github └── workflows │ ├── build-test.yml │ └── style.yml ├── .gitignore ├── .maint ├── contributors.json ├── local_gitignore └── make_gitignore ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── ORIGINAL_LICENSE ├── README.md ├── nipreps └── synthstrip │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── model.py │ └── wrappers │ ├── __init__.py │ ├── nipype.py │ └── pydra.py └── pyproject.toml /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '0 0 * * 1' 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | concurrency: 20 | group: python-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | job_metadata: 28 | if: github.repository == 'nipreps/synthstrip' 29 | runs-on: ubuntu-latest 30 | outputs: 31 | commit_message: ${{ steps.get_commit_message.outputs.commit_message }} 32 | version: ${{ steps.show_version.outputs.version }} 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | - name: Print head git commit message 39 | id: get_commit_message 40 | run: | 41 | if [[ -z "$COMMIT_MSG" ]]; then 42 | COMMIT_MSG=$(git show -s --format=%s $REF) 43 | fi 44 | echo commit_message=$COMMIT_MSG | tee -a $GITHUB_OUTPUT 45 | env: 46 | COMMIT_MSG: ${{ github.event.head_commit.message }} 47 | REF: ${{ github.event.pull_request.head.sha }} 48 | - name: Detect version 49 | id: show_version 50 | run: | 51 | if [[ "$GITHUB_REF" == refs/tags/* ]]; then 52 | VERSION=${GITHUB_REF##*/} 53 | else 54 | pipx run hatch version # Once to avoid output of initial setup 55 | VERSION=$( pipx run hatch version ) 56 | fi 57 | echo version=$VERSION | tee -a $GITHUB_OUTPUT 58 | 59 | build: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | fetch-depth: 0 65 | - name: Setup Python 66 | uses: actions/setup-python@v5 67 | with: 68 | python-version: 3 69 | - name: Display Python information 70 | run: python -c "import sys; print(sys.version)" 71 | - name: Build sdist and wheel 72 | run: pipx run build 73 | - name: Check release tools 74 | run: pipx run twine check dist/* 75 | - name: Save build output 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: dist 79 | path: dist/ 80 | 81 | test: 82 | needs: [job_metadata, build] 83 | runs-on: ubuntu-latest 84 | strategy: 85 | matrix: 86 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 87 | install: [repo] 88 | include: 89 | - python-version: "3.12" 90 | install: sdist 91 | - python-version: "3.12" 92 | install: wheel 93 | - python-version: "3.12" 94 | install: editable 95 | env: 96 | INSTALL_TYPE: ${{ matrix.install }} 97 | steps: 98 | - uses: actions/checkout@v4 99 | if: matrix.install == 'repo' || matrix.install == 'editable' 100 | with: 101 | fetch-depth: 0 102 | - name: Set up Python ${{ matrix.python-version }} 103 | uses: actions/setup-python@v5 104 | with: 105 | python-version: ${{ matrix.python-version }} 106 | - name: Fetch packages 107 | if: matrix.install == 'sdist' || matrix.install == 'wheel' 108 | uses: actions/download-artifact@v4 109 | with: 110 | name: dist 111 | path: dist/ 112 | - name: Select archive 113 | run: | 114 | if [ "$INSTALL_TYPE" = "sdist" ]; then 115 | ARCHIVE=$( ls dist/*.tar.gz ) 116 | elif [ "$INSTALL_TYPE" = "wheel" ]; then 117 | ARCHIVE=$( ls dist/*.whl ) 118 | elif [ "$INSTALL_TYPE" = "repo" ]; then 119 | ARCHIVE="." 120 | elif [ "$INSTALL_TYPE" = "editable" ]; then 121 | ARCHIVE="-e ." 122 | fi 123 | echo "ARCHIVE=$ARCHIVE" | tee -a $GITHUB_ENV 124 | - name: Install package 125 | run: python -m pip install $ARCHIVE 126 | - name: Check version 127 | run: | 128 | INSTALLED_VERSION=$(python -c 'from nipreps.synthstrip import __version__; print(__version__, end="")') 129 | echo "INSTALLED: \"${INSTALLED_VERSION}\"" 130 | test "${INSTALLED_VERSION}" = "${VERSION}" 131 | env: 132 | VERSION: ${{ needs.job_metadata.outputs.version }} 133 | - name: Ensure CLI tool is available 134 | run: nipreps-synthstrip -h 135 | 136 | publish: 137 | runs-on: ubuntu-latest 138 | needs: test 139 | if: github.repository == 'nipreps/synthstrip' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 140 | steps: 141 | - uses: actions/download-artifact@v4 142 | with: 143 | name: dist 144 | path: dist/ 145 | - uses: pypa/gh-action-pypi-publish@release/v1 146 | with: 147 | user: __token__ 148 | password: ${{ secrets.PYPI_API_TOKEN }} 149 | -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: Contribution checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | style: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - run: pipx run ruff check . 28 | - run: pipx run ruff format --diff . 29 | 30 | codespell: 31 | name: Check for spelling errors 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | - name: Codespell 38 | uses: codespell-project/actions-codespell@v2 39 | with: 40 | exclude_file: ORIGINAL_LICENSE 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Auto-generated by .maint/make_gitignore on Tue Apr 18 11:16:44 AM EDT 2023 2 | # setuptools_scm 3 | _version.py 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | # -*- mode: gitignore; -*- 167 | *~ 168 | \#*\# 169 | /.emacs.desktop 170 | /.emacs.desktop.lock 171 | *.elc 172 | auto-save-list 173 | tramp 174 | .\#* 175 | 176 | # Org-mode 177 | .org-id-locations 178 | *_archive 179 | 180 | # flymake-mode 181 | *_flymake.* 182 | 183 | # eshell files 184 | /eshell/history 185 | /eshell/lastdir 186 | 187 | # elpa packages 188 | /elpa/ 189 | 190 | # reftex files 191 | *.rel 192 | 193 | # AUCTeX auto folder 194 | /auto/ 195 | 196 | # cask packages 197 | .cask/ 198 | dist/ 199 | 200 | # Flycheck 201 | flycheck_*.el 202 | 203 | # server auth directory 204 | /server/ 205 | 206 | # projectiles files 207 | .projectile 208 | 209 | # directory configuration 210 | .dir-locals.el 211 | 212 | # network security 213 | /network-security.data 214 | 215 | 216 | *~ 217 | 218 | # temporary files which can be created if a process still has a handle open of a deleted file 219 | .fuse_hidden* 220 | 221 | # KDE directory preferences 222 | .directory 223 | 224 | # Linux trash folder which might appear on any partition or disk 225 | .Trash-* 226 | 227 | # .nfs files are created when an open file is removed but is still being accessed 228 | .nfs* 229 | 230 | # Swap 231 | [._]*.s[a-v][a-z] 232 | !*.svg # comment out if you don't need vector files 233 | [._]*.sw[a-p] 234 | [._]s[a-rt-v][a-z] 235 | [._]ss[a-gi-z] 236 | [._]sw[a-p] 237 | 238 | # Session 239 | Session.vim 240 | Sessionx.vim 241 | 242 | # Temporary 243 | .netrwhist 244 | *~ 245 | # Auto-generated tag files 246 | tags 247 | # Persistent undo 248 | [._]*.un~ 249 | 250 | .vscode/* 251 | !.vscode/settings.json 252 | !.vscode/tasks.json 253 | !.vscode/launch.json 254 | !.vscode/extensions.json 255 | !.vscode/*.code-snippets 256 | 257 | # Local History for Visual Studio Code 258 | .history/ 259 | 260 | # Built Visual Studio Code Extensions 261 | *.vsix 262 | 263 | # Windows thumbnail cache files 264 | Thumbs.db 265 | Thumbs.db:encryptable 266 | ehthumbs.db 267 | ehthumbs_vista.db 268 | 269 | # Dump file 270 | *.stackdump 271 | 272 | # Folder config file 273 | [Dd]esktop.ini 274 | 275 | # Recycle Bin used on file shares 276 | $RECYCLE.BIN/ 277 | 278 | # Windows Installer files 279 | *.cab 280 | *.msi 281 | *.msix 282 | *.msm 283 | *.msp 284 | 285 | # Windows shortcuts 286 | *.lnk 287 | 288 | # General 289 | .DS_Store 290 | .AppleDouble 291 | .LSOverride 292 | 293 | # Icon must end with two \r 294 | Icon 295 | 296 | # Thumbnails 297 | ._* 298 | 299 | # Files that might appear in the root of a volume 300 | .DocumentRevisions-V100 301 | .fseventsd 302 | .Spotlight-V100 303 | .TemporaryItems 304 | .Trashes 305 | .VolumeIcon.icns 306 | .com.apple.timemachine.donotpresent 307 | 308 | # Directories potentially created on remote AFP share 309 | .AppleDB 310 | .AppleDesktop 311 | Network Trash Folder 312 | Temporary Items 313 | .apdisk 314 | -------------------------------------------------------------------------------- /.maint/contributors.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipreps/synthstrip/dd29a6c751f6c9cc8a31983d8bfbc82d1d97c4a6/.maint/contributors.json -------------------------------------------------------------------------------- /.maint/local_gitignore: -------------------------------------------------------------------------------- 1 | # setuptools_scm 2 | _version.py 3 | -------------------------------------------------------------------------------- /.maint/make_gitignore: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Generate .gitignore from GitHub gitignore templates 4 | 5 | BASE_URL="https://raw.githubusercontent.com/github/gitignore/main" 6 | ROOT=$( dirname $( dirname $( realpath $0 ) ) ) 7 | 8 | SOURCES=( 9 | Python 10 | Global/Emacs 11 | Global/Linux 12 | Global/Vim 13 | Global/VisualStudioCode 14 | Global/Windows 15 | Global/macOS 16 | ) 17 | 18 | cat >$ROOT/.gitignore <> $ROOT/.gitignore 23 | 24 | for SRC in ${SOURCES[@]}; do 25 | echo >> $ROOT/.gitignore 26 | curl -sSL ${BASE_URL}/${SRC}.gitignore >> $ROOT/.gitignore 27 | done 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipreps/synthstrip/dd29a6c751f6c9cc8a31983d8bfbc82d1d97c4a6/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 The NiPreps Developers 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Synthstrip-wrapping for NiPreps 2 | Copyright 2023 The NiPreps Developers. 3 | 4 | This product includes software developed by 5 | the NiPreps Community (https://nipreps.org/). 6 | 7 | This software contains code derived from FreeSurfer, and makes use 8 | of a large data file storing the trained model. 9 | Please note that this data file is not distributed with this project 10 | and must be obtained from FreeSurfer directly. 11 | A copy of the FreeSurfer software license is available within the 12 | file ORIGINAL_LICENSE in this project. 13 | Files derived from FreeSurfer include an STATEMENT OF CHANGES in 14 | the attribution notice at the head of the file. 15 | -------------------------------------------------------------------------------- /ORIGINAL_LICENSE: -------------------------------------------------------------------------------- 1 | FreeSurfer Software License Agreement ("Agreement") 2 | Version 1.0 (February 2011) 3 | 4 | This Agreement covers contributions to and downloads from the 5 | FreeSurfer project ("FreeSurfer") maintained by The General Hospital 6 | Corporation, Boston MA, USA ("MGH"). Part A of this Agreement applies to 7 | contributions of software and/or data to FreeSurfer (including making 8 | revisions of or additions to code and/or data already in FreeSurfer). Part 9 | B of this Agreement applies to downloads of software and/or data from 10 | FreeSurfer. Part C of this Agreement applies to all transactions with 11 | FreeSurfer. If you distribute Software (as defined below) downloaded from 12 | FreeSurfer, all of the paragraphs of Part B of this Agreement must be 13 | included with and apply to such Software. 14 | 15 | Your contribution of software and/or data to FreeSurfer (including prior 16 | to the date of the first publication of this Agreement, each a 17 | "Contribution") and/or downloading, copying, modifying, displaying, 18 | distributing or use of any software and/or data from FreeSurfer 19 | (collectively, the "Software") constitutes acceptance of all of the 20 | terms and conditions of this Agreement. If you do not agree to such 21 | terms and conditions, you have no right to contribute your 22 | Contribution, or to download, copy, modify, display, distribute or use 23 | the Software. 24 | 25 | PART A. CONTRIBUTION AGREEMENT - License to MGH with Right to Sublicense 26 | ("Contribution Agreement"). 27 | 28 | 1. As used in this Contribution Agreement, "you" means the individual 29 | contributing the Contribution to FreeSurfer and the institution or 30 | entity which employs or is otherwise affiliated with such 31 | individual in connection with such Contribution. 32 | 33 | 2. This Contribution Agreement applies to all Contributions made to 34 | FreeSurfer, including without limitation Contributions made prior to 35 | the date of first publication of this Agreement. If at any time you 36 | make a Contribution to FreeSurfer, you represent that (i) you are 37 | legally authorized and entitled to make such Contribution and to 38 | grant all licenses granted in this Contribution Agreement with 39 | respect to such Contribution; (ii) if your Contribution includes 40 | any patient data, all such data is de-identified in accordance with 41 | U.S. confidentiality and security laws and requirements, including 42 | but not limited to the Health Insurance Portability and 43 | Accountability Act (HIPAA) and its regulations, and your disclosure 44 | of such data for the purposes contemplated by this Agreement is 45 | properly authorized and in compliance with all applicable laws and 46 | regulations; and (iii) you have preserved in the Contribution all 47 | applicable attributions, copyright notices and licenses for any 48 | third party software or data included in the Contribution. 49 | 50 | 3. Except for the licenses granted in this Agreement, you reserve all 51 | right, title and interest in your Contribution. 52 | 53 | 4. You hereby grant to MGH, with the right to sublicense, a 54 | perpetual, worldwide, non-exclusive, no charge, royalty-free, 55 | irrevocable license to use, reproduce, make derivative works of, 56 | display and distribute the Contribution. If your Contribution is 57 | protected by patent, you hereby grant to MGH, with the right to 58 | sublicense, a perpetual, worldwide, non-exclusive, no-charge, 59 | royalty-free, irrevocable license under your interest in patent 60 | rights covering the Contribution, to make, have made, use, sell and 61 | otherwise transfer your Contribution, alone or in combination with 62 | any other code. 63 | 64 | 5. You acknowledge and agree that MGH may incorporate your 65 | Contribution into FreeSurfer and may make FreeSurfer available to members 66 | of the public on an open source basis under terms substantially in 67 | accordance with the Software License set forth in Part B of this 68 | Agreement. You further acknowledge and agree that MGH shall 69 | have no liability arising in connection with claims resulting from 70 | your breach of any of the terms of this Agreement. 71 | 72 | 6. YOU WARRANT THAT TO THE BEST OF YOUR KNOWLEDGE YOUR CONTRIBUTION 73 | DOES NOT CONTAIN ANY CODE THAT REQURES OR PRESCRIBES AN "OPEN 74 | SOURCE LICENSE" FOR DERIVATIVE WORKS (by way of non-limiting 75 | example, the GNU General Public License or other so-called 76 | "reciprocal" license that requires any derived work to be licensed 77 | under the GNU General Public License or other "open source 78 | license"). 79 | 80 | PART B. DOWNLOADING AGREEMENT - License from MGH with Right to Sublicense 81 | ("Software License"). 82 | 83 | 1. As used in this Software License, "you" means the individual 84 | downloading and/or using, reproducing, modifying, displaying and/or 85 | distributing the Software and the institution or entity which 86 | employs or is otherwise affiliated with such individual in 87 | connection therewith. The General Hospital Corporation ("MGH") 88 | hereby grants you, with right to sublicense, with 89 | respect to MGH's rights in the software, and data, if any, 90 | which is the subject of this Software License (collectively, the 91 | "Software"), a royalty-free, non-exclusive license to use, 92 | reproduce, make derivative works of, display and distribute the 93 | Software, provided that: 94 | 95 | (a) you accept and adhere to all of the terms and conditions of this 96 | Software License; 97 | 98 | (b) in connection with any copy of or sublicense of all or any portion 99 | of the Software, all of the terms and conditions in this Software 100 | License shall appear in and shall apply to such copy and such 101 | sublicense, including without limitation all source and executable 102 | forms and on any user documentation, prefaced with the following 103 | words: "All or portions of this licensed product (such portions are 104 | the "Software") have been obtained under license from The General Hospital 105 | Corporation "MGH" and are subject to the following terms and 106 | conditions:" 107 | 108 | (c) you preserve and maintain all applicable attributions, copyright 109 | notices and licenses included in or applicable to the Software; 110 | 111 | (d) modified versions of the Software must be clearly identified and 112 | marked as such, and must not be misrepresented as being the original 113 | Software; and 114 | 115 | (e) you consider making, but are under no obligation to make, the 116 | source code of any of your modifications to the Software freely 117 | available to others on an open source basis. 118 | 119 | 2. The license granted in this Software License includes without 120 | limitation the right to (i) incorporate the Software into 121 | proprietary programs (subject to any restrictions applicable to 122 | such programs), (ii) add your own copyright statement to your 123 | modifications of the Software, and (iii) provide additional or 124 | different license terms and conditions in your sublicenses of 125 | modifications of the Software; provided that in each case your use, 126 | reproduction or distribution of such modifications otherwise 127 | complies with the conditions stated in this Software License. 128 | 129 | 3. This Software License does not grant any rights with respect to 130 | third party software, except those rights that MGH has been 131 | authorized by a third party to grant to you, and accordingly you 132 | are solely responsible for (i) obtaining any permissions from third 133 | parties that you need to use, reproduce, make derivative works of, 134 | display and distribute the Software, and (ii) informing your 135 | sublicensees, including without limitation your end-users, of their 136 | obligations to secure any such required permissions. 137 | 138 | 4. The Software has been designed for research purposes only and has 139 | not been reviewed or approved by the Food and Drug Administration 140 | or by any other agency. YOU ACKNOWLEDGE AND AGREE THAT CLINICAL 141 | APPLICATIONS ARE NEITHER RECOMMENDED NOR ADVISED. Any 142 | commercialization of the Software is at the sole risk of the party 143 | or parties engaged in such commercialization. You further agree to 144 | use, reproduce, make derivative works of, display and distribute 145 | the Software in compliance with all applicable governmental laws, 146 | regulations and orders, including without limitation those relating 147 | to export and import control. 148 | 149 | 5. The Software is provided "AS IS" and neither MGH nor any 150 | contributor to the software (each a "Contributor") shall have any 151 | obligation to provide maintenance, support, updates, enhancements 152 | or modifications thereto. MGH AND ALL CONTRIBUTORS SPECIFICALLY 153 | DISCLAIM ALL EXPRESS AND IMPLIED WARRANTIES OF ANY KIND INCLUDING, 154 | BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR 155 | A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL 156 | MGH OR ANY CONTRIBUTOR BE LIABLE TO ANY PARTY FOR DIRECT, 157 | INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES 158 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY ARISING IN ANY WAY 159 | RELATED TO THE SOFTWARE, EVEN IF MGH OR ANY CONTRIBUTOR HAS 160 | BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. TO THE MAXIMUM 161 | EXTENT NOT PROHIBITED BY LAW OR REGULATION, YOU FURTHER ASSUME ALL 162 | LIABILITY FOR YOUR USE, REPRODUCTION, MAKING OF DERIVATIVE WORKS, 163 | DISPLAY, LICENSE OR DISTRIBUTION OF THE SOFTWARE AND AGREE TO 164 | INDEMNIFY AND HOLD HARMLESS MGH AND ALL CONTRIBUTORS FROM AND 165 | AGAINST ANY AND ALL CLAIMS, SUITS, ACTIONS, DEMANDS AND JUDGMENTS 166 | ARISING THEREFROM. 167 | 168 | 6. None of the names, logos or trademarks of MGH or any of 169 | MGH's affiliates or any of the Contributors, or any funding 170 | agency, may be used to endorse or promote products produced in 171 | whole or in part by operation of the Software or derived from or 172 | based on the Software without specific prior written permission 173 | from the applicable party. 174 | 175 | 7. Any use, reproduction or distribution of the Software which is not 176 | in accordance with this Software License shall automatically revoke 177 | all rights granted to you under this Software License and render 178 | Paragraphs 1 and 2 of this Software License null and void. 179 | 180 | 8. This Software License does not grant any rights in or to any 181 | intellectual property owned by MGH or any Contributor except 182 | those rights expressly granted hereunder. 183 | 184 | PART C. MISCELLANEOUS 185 | 186 | This Agreement shall be governed by and construed in accordance with 187 | the laws of The Commonwealth of Massachusetts without regard to 188 | principles of conflicts of law. This Agreement shall supercede and 189 | replace any license terms that you may have agreed to previously with 190 | respect to FreeSurfer. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SynthStrip 2 | 3 | This is a NiPreps implementation of the SynthStrip skull-stripping tool. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | # To install the base package 9 | pip install nipreps-synthstrip 10 | 11 | # For the nipype interface 12 | pip install nipreps-synthstrip[nipype] 13 | 14 | # For the pydra interface 15 | pip install nipreps-synthstrip[pydra] 16 | ``` 17 | 18 | ### Command Line Tool 19 | 20 | ```bash 21 | $ nipreps-synthstrip 22 | ``` 23 | 24 | ### Nipype Interface 25 | 26 | ```python 27 | from nipreps.synthstrip.wrappers.nipype import SynthStrip 28 | ``` 29 | 30 | ### Pydra Interface 31 | 32 | ```python 33 | from nipreps.synthstrip.wrappers.pydra import SynthStrip 34 | ``` 35 | 36 | ## Citation 37 | 38 | > A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann. 39 | > SynthStrip: Skull-Stripping for Any Brain Image. 40 | > https://arxiv.org/abs/2203.09974 41 | -------------------------------------------------------------------------------- /nipreps/synthstrip/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | __version__ = 'unknown' 5 | -------------------------------------------------------------------------------- /nipreps/synthstrip/__main__.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # vi: set ft=python sts=4 ts=4 sw=4 et: 3 | # 4 | # Copyright 2022 The NiPreps Developers 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # We support and encourage derived works from this project, please read 19 | # about our expectations at 20 | # 21 | # https://www.nipreps.org/community/licensing/ 22 | # 23 | 24 | if __name__ == '__main__': 25 | import sys 26 | 27 | from nipreps.synthstrip.cli import main 28 | 29 | from . import __name__ as module 30 | 31 | # `python -m ` typically displays the command as __main__.py 32 | if '__main__.py' in sys.argv[0]: 33 | sys.argv[0] = f'{sys.executable} -m {module}' 34 | main() 35 | -------------------------------------------------------------------------------- /nipreps/synthstrip/cli.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # vi: set ft=python sts=4 ts=4 sw=4 et: 3 | # 4 | # Copyright 2022 The NiPreps Developers 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # We support and encourage derived works from this project, please read 19 | # about our expectations at 20 | # 21 | # https://www.nipreps.org/community/licensing/ 22 | # 23 | # STATEMENT OF CHANGES: This file is derived from sources licensed under the FreeSurfer 1.0 license 24 | # terms, and this file has been changed. 25 | # The full licensing terms of the original work are found at: 26 | # https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/LICENSE.txt 27 | # A copy of the license has been archived in the ORIGINAL_LICENSE file 28 | # found within this redistribution. 29 | # 30 | # The original file this work derives from is found at: 31 | # https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/mri_synthstrip/mri_synthstrip 32 | # 33 | # [April 2022] CHANGES: 34 | # * MAINT: Split the monolithic file into model and CLI submodules 35 | # * ENH: Replace freesurfer Python bundle with in-house code. 36 | # 37 | """ 38 | Robust, universal skull-stripping for brain images of any type. 39 | If you use SynthStrip in your analysis, please cite: 40 | 41 | A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann. 42 | SynthStrip: Skull-Stripping for Any Brain Image. 43 | https://arxiv.org/abs/2203.09974 44 | 45 | """ 46 | 47 | 48 | def main(): 49 | """Entry point to SynthStrip.""" 50 | import os 51 | from argparse import ArgumentParser 52 | 53 | import nibabel as nb 54 | import numpy as np 55 | import scipy 56 | import torch 57 | 58 | from .model import StripModel 59 | 60 | # parse command line 61 | parser = ArgumentParser(description=__doc__) 62 | parser.add_argument( 63 | '-i', 64 | '--image', 65 | metavar='file', 66 | required=True, 67 | help='Input image to skullstrip.', 68 | ) 69 | parser.add_argument('-o', '--out', metavar='file', help='Save stripped image to path.') 70 | parser.add_argument('-m', '--mask', metavar='file', help='Save binary brain mask to path.') 71 | parser.add_argument('-g', '--gpu', action='store_true', help='Use the GPU.') 72 | parser.add_argument('-n', '--num-threads', action='store', type=int, help='number of threads') 73 | parser.add_argument( 74 | '-b', 75 | '--border', 76 | default=1, 77 | type=int, 78 | help='Mask border threshold in mm. Default is 1.', 79 | ) 80 | parser.add_argument('--model', metavar='file', help='Alternative model weights.') 81 | args = parser.parse_args() 82 | 83 | # sanity check on the inputs 84 | if not args.out and not args.mask: 85 | parser.fatal('Must provide at least --out or --mask output flags.') 86 | 87 | # necessary for speed gains (I think) 88 | torch.backends.cudnn.benchmark = True 89 | torch.backends.cudnn.deterministic = True 90 | 91 | # configure GPU device 92 | if args.gpu: 93 | os.environ['CUDA_VISIBLE_DEVICES'] = '0' 94 | device = torch.device('cuda') 95 | device_name = 'GPU' 96 | else: 97 | os.environ['CUDA_VISIBLE_DEVICES'] = '-1' 98 | device = torch.device('cpu') 99 | device_name = 'CPU' 100 | 101 | if args.num_threads and args.num_threads > 0: 102 | torch.set_num_threads(args.num_threads) 103 | 104 | # configure model 105 | print(f'Configuring model on the {device_name}') 106 | 107 | with torch.no_grad(): 108 | model = StripModel() 109 | model.to(device) 110 | model.eval() 111 | 112 | # load model weights 113 | if args.model is not None: 114 | modelfile = args.model 115 | print('Using custom model weights') 116 | else: 117 | raise RuntimeError('A model must be provided.') 118 | 119 | checkpoint = torch.load(modelfile, map_location=device) 120 | model.load_state_dict(checkpoint['model_state_dict']) 121 | 122 | # load input volume 123 | print(f'Input image read from: {args.image}') 124 | 125 | # normalize intensities 126 | image = nb.load(args.image) 127 | conformed = conform(image) 128 | in_data = conformed.get_fdata(dtype='float32') 129 | in_data -= in_data.min() 130 | in_data = np.clip(in_data / np.percentile(in_data, 99), 0, 1) 131 | in_data = in_data[np.newaxis, np.newaxis] 132 | 133 | # predict the surface distance transform 134 | input_tensor = torch.from_numpy(in_data).to(device) 135 | with torch.no_grad(): 136 | sdt = model(input_tensor).cpu().numpy().squeeze() 137 | 138 | # unconform the sdt and extract mask 139 | sdt_target = resample_like( 140 | nb.Nifti1Image(sdt, conformed.affine, None), 141 | image, 142 | output_dtype='int16', 143 | cval=100, 144 | ) 145 | sdt_data = np.asanyarray(sdt_target.dataobj).astype('int16') 146 | 147 | # find largest CC (just do this to be safe for now) 148 | components = scipy.ndimage.label(sdt_data.squeeze() < args.border)[0] 149 | bincount = np.bincount(components.flatten())[1:] 150 | mask = components == (np.argmax(bincount) + 1) 151 | mask = scipy.ndimage.morphology.binary_fill_holes(mask) 152 | 153 | # write the masked output 154 | if args.out: 155 | img_data = image.get_fdata() 156 | bg = np.min([0, img_data.min()]) 157 | img_data[mask == 0] = bg 158 | nb.Nifti1Image(img_data, image.affine, image.header).to_filename( 159 | args.out, 160 | ) 161 | print(f'Masked image saved to: {args.out}') 162 | 163 | # write the brain mask 164 | if args.mask: 165 | hdr = image.header.copy() 166 | hdr.set_data_dtype('uint8') 167 | nb.Nifti1Image(mask, image.affine, hdr).to_filename(args.mask) 168 | print(f'Binary brain mask saved to: {args.mask}') 169 | 170 | print('If you use SynthStrip in your analysis, please cite:') 171 | print('----------------------------------------------------') 172 | print('SynthStrip: Skull-Stripping for Any Brain Image.') 173 | print('A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann.') 174 | 175 | 176 | def conform(input_nii): 177 | """Resample image as SynthStrip likes it.""" 178 | import nibabel as nb 179 | import numpy as np 180 | from nitransforms.linear import Affine 181 | 182 | shape = np.array(input_nii.shape[:3]) 183 | affine = input_nii.affine 184 | 185 | # Get corner voxel centers in index coords 186 | corner_centers_ijk = ( 187 | np.array( 188 | [ 189 | (i, j, k) 190 | for k in (0, shape[2] - 1) 191 | for j in (0, shape[1] - 1) 192 | for i in (0, shape[0] - 1) 193 | ] 194 | ) 195 | + 0.5 196 | ) 197 | 198 | # Get corner voxel centers in mm 199 | corners_xyz = affine @ np.hstack((corner_centers_ijk, np.ones((len(corner_centers_ijk), 1)))).T 200 | 201 | # Target affine is 1mm voxels in LIA orientation 202 | target_affine = np.diag([-1.0, 1.0, -1.0, 1.0])[:, (0, 2, 1, 3)] 203 | 204 | # Target shape 205 | extent = corners_xyz.min(1)[:3], corners_xyz.max(1)[:3] 206 | target_shape = ((extent[1] - extent[0]) / 1.0 + 0.999).astype(int) 207 | 208 | # SynthStrip likes dimensions be multiple of 64 (192, 256, or 320) 209 | target_shape = np.clip(np.ceil(np.array(target_shape) / 64).astype(int) * 64, 192, 320) 210 | 211 | # Ensure shape ordering is LIA too 212 | target_shape[2], target_shape[1] = target_shape[1:3] 213 | 214 | # Coordinates of center voxel do not change 215 | input_c = affine @ np.hstack((0.5 * (shape - 1), 1.0)) 216 | target_c = target_affine @ np.hstack((0.5 * (target_shape - 1), 1.0)) 217 | 218 | # Rebase the origin of the new, plumb affine 219 | target_affine[:3, 3] -= target_c[:3] - input_c[:3] 220 | 221 | nii = Affine( 222 | reference=nb.Nifti1Image(np.zeros(target_shape), target_affine, None), 223 | ).apply(input_nii) 224 | return nii 225 | 226 | 227 | def resample_like(image, target, output_dtype=None, cval=0): 228 | """Resample the input image to be in the target's grid via identity transform.""" 229 | from nitransforms.linear import Affine 230 | 231 | return Affine(reference=target).apply(image, output_dtype=output_dtype, cval=cval) 232 | 233 | 234 | if __name__ == '__main__': 235 | main() 236 | -------------------------------------------------------------------------------- /nipreps/synthstrip/model.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # vi: set ft=python sts=4 ts=4 sw=4 et: 3 | # 4 | # Copyright 2022 The NiPreps Developers 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # We support and encourage derived works from this project, please read 19 | # about our expectations at 20 | # 21 | # https://www.nipreps.org/community/licensing/ 22 | # 23 | # STATEMENT OF CHANGES: This file is derived from sources licensed under the FreeSurfer 1.0 license 24 | # terms, and this file has been changed. 25 | # The full licensing terms of the original work are found at: 26 | # https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/LICENSE.txt 27 | # A copy of the license has been archived in the ORIGINAL_LICENSE file 28 | # found within this redistribution. 29 | # 30 | # The original file this work derives from is found at: 31 | # https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/mri_synthstrip/mri_synthstrip 32 | # 33 | # [April 2022] CHANGES: 34 | # * MAINT: Split the monolithic file into model and CLI submodules 35 | # * ENH: Replace freesurfer Python bundle with in-house code. 36 | # 37 | """ 38 | Robust, universal skull-stripping for brain images of any type. 39 | If you use SynthStrip in your analysis, please cite: 40 | 41 | A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann. 42 | SynthStrip: Skull-Stripping for Any Brain Image. 43 | https://arxiv.org/abs/2203.09974 44 | 45 | """ 46 | 47 | import numpy as np 48 | import torch 49 | import torch.nn as nn 50 | 51 | 52 | class StripModel(nn.Module): 53 | def __init__( 54 | self, 55 | nb_features=16, 56 | nb_levels=7, 57 | feat_mult=2, 58 | max_features=64, 59 | nb_conv_per_level=2, 60 | max_pool=2, 61 | return_mask=False, 62 | ): 63 | super().__init__() 64 | 65 | # dimensionality 66 | ndims = 3 67 | 68 | # build feature list automatically 69 | if isinstance(nb_features, int): 70 | if nb_levels is None: 71 | raise ValueError('must provide unet nb_levels if nb_features is an integer') 72 | feats = np.round(nb_features * feat_mult ** np.arange(nb_levels)).astype(int) 73 | feats = np.clip(feats, 1, max_features) 74 | nb_features = [ 75 | np.repeat(feats[:-1], nb_conv_per_level), 76 | np.repeat(np.flip(feats), nb_conv_per_level), 77 | ] 78 | elif nb_levels is not None: 79 | raise ValueError('cannot use nb_levels if nb_features is not an integer') 80 | 81 | # extract any surplus (full resolution) decoder convolutions 82 | enc_nf, dec_nf = nb_features 83 | nb_dec_convs = len(enc_nf) 84 | final_convs = dec_nf[nb_dec_convs:] 85 | dec_nf = dec_nf[:nb_dec_convs] 86 | self.nb_levels = int(nb_dec_convs / nb_conv_per_level) + 1 87 | 88 | if isinstance(max_pool, int): 89 | max_pool = [max_pool] * self.nb_levels 90 | 91 | # cache downsampling / upsampling operations 92 | MaxPooling = getattr(nn, 'MaxPool%dd' % ndims) 93 | self.pooling = [MaxPooling(s) for s in max_pool] 94 | self.upsampling = [nn.Upsample(scale_factor=s, mode='nearest') for s in max_pool] 95 | 96 | # configure encoder (down-sampling path) 97 | prev_nf = 1 98 | encoder_nfs = [prev_nf] 99 | self.encoder = nn.ModuleList() 100 | for level in range(self.nb_levels - 1): 101 | convs = nn.ModuleList() 102 | for conv in range(nb_conv_per_level): 103 | nf = enc_nf[level * nb_conv_per_level + conv] 104 | convs.append(ConvBlock(ndims, prev_nf, nf)) 105 | prev_nf = nf 106 | self.encoder.append(convs) 107 | encoder_nfs.append(prev_nf) 108 | 109 | # configure decoder (up-sampling path) 110 | encoder_nfs = np.flip(encoder_nfs) 111 | self.decoder = nn.ModuleList() 112 | for level in range(self.nb_levels - 1): 113 | convs = nn.ModuleList() 114 | for conv in range(nb_conv_per_level): 115 | nf = dec_nf[level * nb_conv_per_level + conv] 116 | convs.append(ConvBlock(ndims, prev_nf, nf)) 117 | prev_nf = nf 118 | self.decoder.append(convs) 119 | if level < (self.nb_levels - 1): 120 | prev_nf += encoder_nfs[level] 121 | 122 | # now we take care of any remaining convolutions 123 | self.remaining = nn.ModuleList() 124 | for nf in final_convs: 125 | self.remaining.append(ConvBlock(ndims, prev_nf, nf)) 126 | prev_nf = nf 127 | 128 | # final convolutions 129 | if return_mask: 130 | self.remaining.append(ConvBlock(ndims, prev_nf, 2, activation=None)) 131 | self.remaining.append(nn.Softmax(dim=1)) 132 | else: 133 | self.remaining.append(ConvBlock(ndims, prev_nf, 1, activation=None)) 134 | 135 | def forward(self, x): 136 | # encoder forward pass 137 | x_history = [x] 138 | for level, convs in enumerate(self.encoder): 139 | for conv in convs: 140 | x = conv(x) 141 | x_history.append(x) 142 | x = self.pooling[level](x) 143 | 144 | # decoder forward pass with upsampling and concatenation 145 | for level, convs in enumerate(self.decoder): 146 | for conv in convs: 147 | x = conv(x) 148 | if level < (self.nb_levels - 1): 149 | x = self.upsampling[level](x) 150 | x = torch.cat([x, x_history.pop()], dim=1) 151 | 152 | # remaining convs at full resolution 153 | for conv in self.remaining: 154 | x = conv(x) 155 | 156 | return x 157 | 158 | 159 | class ConvBlock(nn.Module): 160 | """ 161 | Specific convolutional block followed by leakyrelu for unet. 162 | """ 163 | 164 | def __init__(self, ndims, in_channels, out_channels, stride=1, activation='leaky'): 165 | super().__init__() 166 | 167 | Conv = getattr(nn, 'Conv%dd' % ndims) 168 | self.conv = Conv(in_channels, out_channels, 3, stride, 1) 169 | if activation == 'leaky': 170 | self.activation = nn.LeakyReLU(0.2) 171 | elif activation is None: 172 | self.activation = None 173 | else: 174 | raise ValueError(f'Unknown activation: {activation}') 175 | 176 | def forward(self, x): 177 | out = self.conv(x) 178 | if self.activation is not None: 179 | out = self.activation(out) 180 | return out 181 | -------------------------------------------------------------------------------- /nipreps/synthstrip/wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipreps/synthstrip/dd29a6c751f6c9cc8a31983d8bfbc82d1d97c4a6/nipreps/synthstrip/wrappers/__init__.py -------------------------------------------------------------------------------- /nipreps/synthstrip/wrappers/nipype.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # vi: set ft=python sts=4 ts=4 sw=4 et: 3 | # 4 | # Copyright 2022 The NiPreps Developers 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # We support and encourage derived works from this project, please read 19 | # about our expectations at 20 | # 21 | # https://www.nipreps.org/community/licensing/ 22 | # 23 | """SynthStrip interface.""" 24 | 25 | import os 26 | from pathlib import Path 27 | 28 | from nipype.interfaces.base import ( 29 | CommandLine, 30 | CommandLineInputSpec, 31 | File, 32 | TraitedSpec, 33 | Undefined, 34 | traits, 35 | ) 36 | 37 | _fs_home = os.getenv('FREESURFER_HOME', None) 38 | _default_model_path = Path(_fs_home) / 'models' / 'synthstrip.1.pt' if _fs_home else Undefined 39 | 40 | if _fs_home and not _default_model_path.exists(): 41 | _default_model_path = Undefined 42 | 43 | 44 | class _SynthStripInputSpec(CommandLineInputSpec): 45 | in_file = File( 46 | exists=True, 47 | mandatory=True, 48 | argstr='-i %s', 49 | desc='Input image to be brain extracted', 50 | ) 51 | use_gpu = traits.Bool(False, usedefault=True, argstr='-g', desc='Use GPU', nohash=True) 52 | model = File( 53 | str(_default_model_path), 54 | usedefault=True, 55 | exists=True, 56 | argstr='--model %s', 57 | desc="file containing model's weights", 58 | ) 59 | border_mm = traits.Int(1, usedefault=True, argstr='-b %d', desc='Mask border threshold in mm') 60 | out_file = File( 61 | name_source=['in_file'], 62 | name_template='%s_desc-brain.nii.gz', 63 | argstr='-o %s', 64 | desc='store brain-extracted input to file', 65 | ) 66 | out_mask = File( 67 | name_source=['in_file'], 68 | name_template='%s_desc-brain_mask.nii.gz', 69 | argstr='-m %s', 70 | desc='store brainmask to file', 71 | ) 72 | num_threads = traits.Int(desc='Number of threads', argstr='-n %d', nohash=True) 73 | 74 | 75 | class _SynthStripOutputSpec(TraitedSpec): 76 | out_file = File(desc='brain-extracted image') 77 | out_mask = File(desc='brain mask') 78 | 79 | 80 | class SynthStrip(CommandLine): 81 | _cmd = 'nipreps-synthstrip' 82 | input_spec = _SynthStripInputSpec 83 | output_spec = _SynthStripOutputSpec 84 | -------------------------------------------------------------------------------- /nipreps/synthstrip/wrappers/pydra.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # vi: set ft=python sts=4 ts=4 sw=4 et: 3 | # 4 | # Copyright 2022 The NiPreps Developers 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # We support and encourage derived works from this project, please read 19 | # about our expectations at 20 | # 21 | # https://www.nipreps.org/community/licensing/ 22 | # 23 | """SynthStrip interface.""" 24 | 25 | import os 26 | from pathlib import Path 27 | 28 | import attr 29 | import pydra 30 | 31 | _fs_home = os.getenv('FREESURFER_HOME', None) 32 | _default_model_path = Path(_fs_home) / 'models' / 'synthstrip.1.pt' if _fs_home else None 33 | 34 | if _fs_home and not _default_model_path.exists(): 35 | _default_model_path = None 36 | 37 | SynthStripInputSpec = pydra.specs.SpecInfo( 38 | name='SynthStripInputSpec', 39 | fields=[ 40 | ( 41 | 'in_file', 42 | attr.ib( 43 | type=str, 44 | metadata={ 45 | 'argstr': '-i', 46 | 'help_string': 'Input image to skullstrip', 47 | 'mandatory': True, 48 | }, 49 | ), 50 | ), 51 | ( 52 | 'out_file', 53 | str, 54 | { 55 | 'argstr': '-o', 56 | 'help_string': 'Save stripped image to path', 57 | 'output_file_template': '{in_file}_desc-brain.nii.gz', 58 | }, 59 | ), 60 | ( 61 | 'out_mask', 62 | str, 63 | { 64 | 'argstr': '-m', 65 | 'help_string': 'Save binary brain mask to path', 66 | 'output_file_template': '{in_file}_desc-brain_mask.nii.gz', 67 | }, 68 | ), 69 | ( 70 | 'use_gpu', 71 | bool, 72 | False, 73 | { 74 | 'argstr': '-g', 75 | 'help_string': 'Use the GPU', 76 | }, 77 | ), 78 | ( 79 | 'border_mm', 80 | int, 81 | 1, 82 | { 83 | 'argstr': '-b', 84 | 'help_string': 'Mask border threshold in mm', 85 | }, 86 | ), 87 | ( 88 | 'no_csf', 89 | bool, 90 | False, 91 | { 92 | 'argstr': '--no-csf', 93 | 'help_string': 'Exclude CSF from brain border', 94 | }, 95 | ), 96 | ( 97 | 'model', 98 | pydra.specs.File, 99 | str(_default_model_path), 100 | { 101 | 'argstr': '--model', 102 | 'help_string': "File containing model's weights", 103 | }, 104 | ), 105 | ( 106 | 'num_threads', 107 | int, 108 | { 109 | 'argstr': '-n', 110 | 'help_string': 'Number of threads', 111 | }, 112 | ), 113 | ], 114 | bases=(pydra.specs.ShellSpec,), 115 | ) 116 | 117 | SynthStrip = pydra.ShellCommandTask( 118 | executable='nipreps-synthstrip', input_spec=SynthStripInputSpec 119 | ) 120 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "nipreps-synthstrip" 7 | description = "NiPreps implementation of FreeSurfer's SynthStrip" 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | maintainers = [ 11 | {name = "Nipreps developers", email = "nipreps@gmail.com"}, 12 | ] 13 | classifiers = [ 14 | "Development Status :: 2 - Pre-Alpha", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: Apache Software License", 17 | "Operating System :: MacOS :: MacOS X", 18 | "Operating System :: POSIX :: Linux", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | ] 25 | requires-python = ">=3.8" 26 | dependencies = [ 27 | "nibabel", 28 | "nitransforms", 29 | "numpy", 30 | "scipy", 31 | "torch >= 1.10.2", 32 | ] 33 | dynamic = ["version"] 34 | 35 | [project.urls] 36 | Homepage = "https://github.com/nipreps/synthstrip" 37 | 38 | [project.optional-dependencies] 39 | dev = [ 40 | "ruff", 41 | "hatch", 42 | ] 43 | nipype = [ 44 | "nipype", 45 | ] 46 | pydra = [ 47 | "pydra", 48 | ] 49 | test = [ 50 | "pytest", 51 | ] 52 | 53 | [project.scripts] 54 | nipreps-synthstrip = "nipreps.synthstrip.cli:main" 55 | 56 | [tool.hatch.version] 57 | source = "vcs" 58 | 59 | [tool.hatch.build.hooks.vcs] 60 | version-file = "nipreps/synthstrip/_version.py" 61 | 62 | [tool.hatch.build.sources] 63 | "nipreps/synthstrip" = "nipreps/synthstrip" 64 | 65 | [tool.hatch.build.targets.wheel] 66 | packages = ["nipreps/synthstrip"] 67 | 68 | [tool.black] 69 | exclude = '.*' 70 | 71 | [tool.ruff] 72 | line-length = 99 73 | 74 | [tool.ruff.lint] 75 | extend-select = [ 76 | "F", 77 | "E", 78 | "W", 79 | "I", 80 | "UP", 81 | "YTT", 82 | "S", 83 | "BLE", 84 | "B", 85 | "A", 86 | # "CPY", 87 | "C4", 88 | "DTZ", 89 | "T10", 90 | # "EM", 91 | "EXE", 92 | "FA", 93 | "ISC", 94 | "ICN", 95 | "PT", 96 | "Q", 97 | ] 98 | ignore = [ 99 | "S311", # We are not using random for cryptographic purposes 100 | "ISC001", 101 | "S603", 102 | ] 103 | 104 | [tool.ruff.lint.flake8-quotes] 105 | inline-quotes = "single" 106 | 107 | [tool.ruff.format] 108 | quote-style = "single" 109 | 110 | [tool.coverage.run] 111 | branch = true 112 | omit = [ 113 | "*/_version.py" 114 | ] 115 | --------------------------------------------------------------------------------