├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MONAIViz ├── CMakeLists.txt ├── MONAIViz.py ├── MONAIVizLib │ ├── __init__.py │ └── utils.py ├── Resources │ ├── Icons │ │ ├── MONAI.png │ │ ├── MONAIViz.png │ │ ├── done.png │ │ ├── download.png │ │ ├── icons8-delete-document-48.png │ │ ├── icons8-delete-row-48.png │ │ ├── icons8-edit-row-48.png │ │ ├── icons8-green-circle-48.png │ │ ├── icons8-insert-row-48.png │ │ ├── icons8-load-48.png │ │ ├── icons8-preview-48.png │ │ ├── icons8-red-circle-48.png │ │ ├── icons8-save-48.png │ │ ├── icons8-yellow-circle-48.png │ │ ├── refresh-icon.png │ │ └── upload.svg │ └── UI │ │ ├── MONAIDictionaryDialog.ui │ │ ├── MONAITransformDialog.ui │ │ └── MONAIViz.ui └── Testing │ ├── CMakeLists.txt │ └── Python │ └── CMakeLists.txt ├── README.md ├── Screenshots ├── 1.jpg ├── 2.jpg ├── 3.jpg └── 4.png └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | #lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # virtualenv 143 | .venv/ 144 | venv/ 145 | ENV/ 146 | ubuntu_venv/ 147 | 148 | # IDE 149 | .idea/ 150 | .vscode/ 151 | 152 | # apps 153 | sample-apps/*/logs 154 | sample-apps/*/train 155 | 156 | # docs build 157 | docs/build 158 | docs/source/apidocs 159 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | ci: 13 | autofix_prs: true 14 | autoupdate_schedule: quarterly 15 | 16 | repos: 17 | - repo: https://github.com/pre-commit/pre-commit-hooks 18 | rev: v4.4.0 19 | hooks: 20 | - id: check-merge-conflict 21 | - id: trailing-whitespace 22 | - id: check-yaml 23 | - id: check-executables-have-shebangs 24 | - id: check-toml 25 | - id: check-case-conflict 26 | - id: check-added-large-files 27 | args: ['--maxkb=1024'] 28 | - id: detect-private-key 29 | - id: forbid-new-submodules 30 | - id: pretty-format-json 31 | args: ['--autofix', '--no-sort-keys', '--indent=4'] 32 | - id: end-of-file-fixer 33 | - id: mixed-line-ending 34 | 35 | - repo: https://github.com/pycqa/isort 36 | rev: 5.12.0 37 | hooks: 38 | - id: isort 39 | name: isort (python) 40 | args: ["--line-length=120", "--profile=black"] 41 | 42 | - repo: https://github.com/pycqa/isort 43 | rev: 5.12.0 44 | hooks: 45 | - id: isort 46 | name: isort (python) (check) 47 | args: ["--line-length=120", "--profile=black", "--check"] 48 | 49 | - repo: https://github.com/psf/black 50 | rev: 23.9.1 51 | hooks: 52 | - id: black 53 | args: ["--line-length=120"] 54 | 55 | - repo: https://github.com/psf/black 56 | rev: 23.9.1 57 | hooks: 58 | - id: black 59 | name: black (check) 60 | args: ["--line-length=120", "--check"] 61 | 62 | - repo: https://github.com/PyCQA/flake8 63 | rev: 6.1.0 64 | hooks: 65 | - id: flake8 66 | args: ["--count", "--statistics", "--config=setup.cfg"] 67 | 68 | - repo: https://github.com/pre-commit/mirrors-mypy 69 | rev: v1.5.1 70 | hooks: 71 | - id: mypy 72 | exclude: (^tests/|^plugins/|MONAIViz/MONAIViz.py) 73 | additional_dependencies: [types-PyYAML,types-filelock,types-requests,types-docutils,types-cachetools] 74 | 75 | - repo: https://github.com/asottile/pyupgrade 76 | rev: v3.14.0 77 | hooks: 78 | - id: pyupgrade 79 | args: [--py37-plus] 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to SlicerMONAIViz are documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). 5 | 6 | 17 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16.3...3.19.7 FATAL_ERROR) 2 | 3 | project(MONAIViz) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://github.com/Project-MONAI/SlicerMONAIViz#slicermonaiviz") 8 | set(EXTENSION_CATEGORY "Developer Tools") 9 | set(EXTENSION_CONTRIBUTORS "MONAI Consortium") 10 | set(EXTENSION_DESCRIPTION "This extension helps to run chain of MONAI transforms and visualize every stage over an image/label. See more information in MONAILabel documentation.") 11 | set(EXTENSION_ICONURL "https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/main/MONAIViz/Resources/Icons/MONAIViz.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://github.com/Project-MONAI/SlicerMONAIViz/raw/main/Screenshots/1.jpg https://github.com/Project-MONAI/SlicerMONAIViz/raw/main/Screenshots/2.jpg https://github.com/Project-MONAI/SlicerMONAIViz/raw/main/Screenshots/3.jpg https://github.com/Project-MONAI/SlicerMONAIViz/raw/main/Screenshots/4.png") 13 | set(EXTENSION_DEPENDS "PyTorch") # Specified as a list or "NA" if no dependencies 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(MONAIViz) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | # Contributor Covenant Code of Conduct 15 | 16 | ## Our Pledge 17 | 18 | In the interest of fostering an open and welcoming environment, we as 19 | contributors and maintainers pledge to making participation in our project and 20 | our community a harassment-free experience for everyone, regardless of age, body 21 | size, disability, ethnicity, sex characteristics, gender identity and expression, 22 | level of experience, education, socio-economic status, nationality, personal 23 | appearance, race, religion, or sexual identity and orientation. 24 | 25 | ## Our Standards 26 | 27 | Examples of behavior that contributes to creating a positive environment 28 | include: 29 | 30 | * Using welcoming and inclusive language 31 | * Being respectful of differing viewpoints and experiences 32 | * Gracefully accepting constructive criticism 33 | * Focusing on what is best for the community 34 | * Showing empathy towards other community members 35 | 36 | Examples of unacceptable behavior by participants include: 37 | 38 | * The use of sexualized language or imagery and unwelcome sexual attention or 39 | advances 40 | * Trolling, insulting/derogatory comments, and personal or political attacks 41 | * Public or private harassment 42 | * Publishing others' private information, such as a physical or electronic 43 | address, without explicit permission 44 | * Other conduct which could reasonably be considered inappropriate in a 45 | professional setting 46 | 47 | ## Our Responsibilities 48 | 49 | Project maintainers are responsible for clarifying the standards of acceptable 50 | behavior and are expected to take appropriate and fair corrective action in 51 | response to any instances of unacceptable behavior. 52 | 53 | Project maintainers have the right and responsibility to remove, edit, or 54 | reject comments, commits, code, wiki edits, issues, and other contributions 55 | that are not aligned to this Code of Conduct, or to ban temporarily or 56 | permanently any contributor for other behaviors that they deem inappropriate, 57 | threatening, offensive, or harmful. 58 | 59 | ## Scope 60 | 61 | This Code of Conduct applies both within project spaces and in public spaces 62 | when an individual is representing the project or its community. Examples of 63 | representing a project or community include using an official project e-mail 64 | address, posting via an official social media account, or acting as an appointed 65 | representative at an online or offline event. Representation of a project may be 66 | further defined and clarified by project maintainers. 67 | 68 | ## Enforcement 69 | 70 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 71 | reported by contacting the project team at monai.contact@gmail.com. All 72 | complaints will be reviewed and investigated and will result in a response that 73 | is deemed necessary and appropriate to the circumstances. The project team is 74 | obligated to maintain confidentiality with regard to the reporter of an incident. 75 | Further details of specific enforcement policies may be posted separately. 76 | 77 | Project maintainers who do not follow or enforce the Code of Conduct in good 78 | faith may face temporary or permanent repercussions as determined by other 79 | members of the project's leadership. 80 | 81 | ## Attribution 82 | 83 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 84 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 85 | 86 | [homepage]: https://www.contributor-covenant.org 87 | 88 | For answers to common questions about this code of conduct, see 89 | https://www.contributor-covenant.org/faq 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - [Introduction](#introduction) 15 | - [The contribution process](#the-contribution-process) 16 | * [Signing your work](#signing-your-work) 17 | 18 | ## Introduction 19 | 20 | 21 | Welcome to Project MONAILabel! We're excited you're here and want to contribute. This documentation is intended for individuals and institutions interested in contributing to MONAILabel. MONAILabel is an open-source project and, as such, its success relies on its community of contributors willing to keep improving it. Your contribution will be a valued addition to the code base; we simply ask that you read this page and understand our contribution process, whether you are a seasoned open-source contributor or whether you are a first-time contributor. 22 | 23 | ### Communicate with us 24 | 25 | We are happy to talk with you about your needs for MONAILabel and your ideas for contributing to the project. One way to do this is to create an issue discussing your thoughts. It might be that a very similar feature is under development or already exists, so an issue is a great starting point. If you are looking for an issue to resolve that will help Project MONAILabel, see the [*good first issue*](https://github.com/Project-MONAI/MONAILabel/labels/good%20first%20issue) and [*Contribution wanted*](https://github.com/Project-MONAI/MONAILabel/labels/Contribution%20wanted) labels. 26 | 27 | ## The contribution process 28 | 29 | >In progress. Please wait for more instructions to follow 30 | 31 | - Before submitting Pull Request make sure basic CI checks are passed. 32 | 33 | Install `pre-commit` if you have not already. 34 | ``` 35 | python -m pip install pre-commit 36 | ``` 37 | Run `pre-commit` check from MonaiLabel working directory. 38 | ``` 39 | python -m pre_commit run --all-files 40 | ``` 41 | 42 | 43 | ### Signing your work 44 | MONAILabel enforces the [Developer Certificate of Origin](https://developercertificate.org/) (DCO) on all pull requests. 45 | All commit messages should contain the `Signed-off-by` line with an email address. The [GitHub DCO app](https://github.com/apps/dco) is deployed on MONAILabel. The pull request's status will be `failed` if commits do not contain a valid `Signed-off-by` line. 46 | 47 | Git has a `-s` (or `--signoff`) command-line option to append this automatically to your commit message: 48 | ```bash 49 | git commit -s -m 'a new commit' 50 | ``` 51 | The commit message will be: 52 | ``` 53 | a new commit 54 | 55 | Signed-off-by: Your Name 56 | ``` 57 | 58 | Full text of the DCO: 59 | ``` 60 | Developer Certificate of Origin 61 | Version 1.1 62 | 63 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 64 | 1 Letterman Drive 65 | Suite D4700 66 | San Francisco, CA, 94129 67 | 68 | Everyone is permitted to copy and distribute verbatim copies of this 69 | license document, but changing it is not allowed. 70 | 71 | 72 | Developer's Certificate of Origin 1.1 73 | 74 | By making a contribution to this project, I certify that: 75 | 76 | (a) The contribution was created in whole or in part by me and I 77 | have the right to submit it under the open source license 78 | indicated in the file; or 79 | 80 | (b) The contribution is based upon previous work that, to the best 81 | of my knowledge, is covered under an appropriate open source 82 | license and I have the right under that license to submit that 83 | work with modifications, whether created in whole or in part 84 | by me, under the same open source license (unless I am 85 | permitted to submit under a different license), as indicated 86 | in the file; or 87 | 88 | (c) The contribution was provided directly to me by some other 89 | person who certified (a), (b) or (c) and I have not modified 90 | it. 91 | 92 | (d) I understand and agree that this project and the contribution 93 | are public and that a record of the contribution (including all 94 | personal information I submit with it, including my sign-off) is 95 | maintained indefinitely and may be redistributed consistent with 96 | this project or the open source license(s) involved. 97 | ``` 98 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /MONAIViz/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME MONAIViz) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ${MODULE_NAME}Lib/__init__.py 8 | ${MODULE_NAME}Lib/utils.py 9 | ) 10 | 11 | set(MODULE_PYTHON_RESOURCES 12 | Resources/Icons/${MODULE_NAME}.png 13 | Resources/Icons/refresh-icon.png 14 | Resources/Icons/upload.svg 15 | Resources/Icons/icons8-insert-row-48.png 16 | Resources/Icons/icons8-preview-48.png 17 | Resources/Icons/icons8-red-circle-48.png 18 | Resources/Icons/icons8-yellow-circle-48.png 19 | Resources/Icons/icons8-delete-document-48.png 20 | Resources/Icons/icons8-delete-row-48.png 21 | Resources/Icons/icons8-edit-row-48.png 22 | Resources/Icons/icons8-green-circle-48.png 23 | Resources/Icons/done.png 24 | Resources/Icons/download.png 25 | Resources/Icons/MONAI.png 26 | Resources/UI/${MODULE_NAME}.ui 27 | Resources/UI/MONAITransformDialog.ui 28 | Resources/UI/MONAIDictionaryDialog.ui 29 | ) 30 | 31 | #----------------------------------------------------------------------------- 32 | slicerMacroBuildScriptedModule( 33 | NAME ${MODULE_NAME} 34 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 35 | RESOURCES ${MODULE_PYTHON_RESOURCES} 36 | WITH_GENERIC_TESTS 37 | ) 38 | 39 | #----------------------------------------------------------------------------- 40 | if(BUILD_TESTING) 41 | 42 | # Register the unittest subclass in the main script as a ctest. 43 | # Note that the test will also be available at runtime. 44 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 45 | 46 | # Additional build-time testing 47 | add_subdirectory(Testing) 48 | endif() 49 | -------------------------------------------------------------------------------- /MONAIViz/MONAIViz.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | import copy 12 | import json 13 | import logging 14 | import os 15 | import pprint 16 | import tempfile 17 | from io import StringIO 18 | 19 | import ctk 20 | import PyTorchUtils 21 | import qt 22 | import requests 23 | import slicer 24 | import vtk 25 | from MONAIVizLib import ClassUtils, MonaiUtils 26 | from slicer.ScriptedLoadableModule import * 27 | from slicer.util import VTKObservationMixin 28 | 29 | 30 | class MONAIViz(ScriptedLoadableModule): 31 | def __init__(self, parent): 32 | ScriptedLoadableModule.__init__(self, parent) 33 | 34 | self.parent.title = "MONAIViz" 35 | self.parent.categories = ["MONAI", "Developer Tools"] 36 | self.parent.dependencies = [] 37 | self.parent.contributors = ["MONAI Consortium"] 38 | self.parent.helpText = """ 39 | This extension helps to run chain of MONAI transforms and visualize every stage over an image/label. 40 | See more information in module documentation. 41 | """ 42 | self.parent.acknowledgementText = """ 43 | Developed by MONAI Consortium 44 | """ 45 | 46 | # Additional initialization step after application startup is complete 47 | slicer.app.connect("startupCompleted()", self.initializeAfterStartup) 48 | 49 | def initializeAfterStartup(self): 50 | if not slicer.app.commandOptions().noMainWindow: 51 | self.settingsPanel = MONAIVizSettingsPanel() 52 | slicer.app.settingsDialog().addPanel("MONAIViz", self.settingsPanel) 53 | 54 | 55 | class _ui_MONAIVizSettingsPanel: 56 | def __init__(self, parent): 57 | vBoxLayout = qt.QVBoxLayout(parent) 58 | 59 | # settings 60 | groupBox = ctk.ctkCollapsibleGroupBox() 61 | groupBox.title = "MONAIViz" 62 | groupLayout = qt.QFormLayout(groupBox) 63 | 64 | bundleAuthToken = qt.QLineEdit() 65 | bundleAuthToken.setText("") 66 | bundleAuthToken.toolTip = "Auth Token for bundles to download from MONAI Model Zoo" 67 | groupLayout.addRow("Bundle Auth Token:", bundleAuthToken) 68 | parent.registerProperty( 69 | "MONAIViz/bundleAuthToken", bundleAuthToken, "text", str(qt.SIGNAL("textChanged(QString)")) 70 | ) 71 | 72 | transformsPath = qt.QLineEdit() 73 | transformsPath.setText("monai.transforms") 74 | transformsPath.toolTip = "Transforms Search Path" 75 | groupLayout.addRow("Transforms Search Path:", transformsPath) 76 | parent.registerProperty( 77 | "MONAIViz/transformsPath", transformsPath, "text", str(qt.SIGNAL("textChanged(QString)")) 78 | ) 79 | 80 | fileExtension = qt.QLineEdit() 81 | fileExtension.setText(".nii.gz") 82 | fileExtension.toolTip = "Default extension for uploading images/labels" 83 | groupLayout.addRow("File Extension:", fileExtension) 84 | parent.registerProperty("MONAIViz/fileExtension", fileExtension, "text", str(qt.SIGNAL("textChanged(QString)"))) 85 | 86 | imageKey = qt.QLineEdit() 87 | imageKey.setText("image") 88 | imageKey.toolTip = "Image Key in Dictionary" 89 | groupLayout.addRow("Image Key:", imageKey) 90 | parent.registerProperty("MONAIViz/imageKey", imageKey, "text", str(qt.SIGNAL("textChanged(QString)"))) 91 | 92 | labelKey = qt.QLineEdit() 93 | labelKey.setText("label") 94 | labelKey.toolTip = "Label Key in Dictionary" 95 | groupLayout.addRow("Label Key:", labelKey) 96 | parent.registerProperty("MONAIViz/labelKey", labelKey, "text", str(qt.SIGNAL("textChanged(QString)"))) 97 | 98 | bufferArgs = qt.QLineEdit() 99 | bufferArgs.setInputMask("00") 100 | bufferArgs.setText("15") 101 | bufferArgs.toolTip = "Buffer/Extra Args for each Transform while adding/editing" 102 | groupLayout.addRow("Buffer Args:", bufferArgs) 103 | parent.registerProperty("MONAIViz/bufferArgs", bufferArgs, "text", str(qt.SIGNAL("textChanged(QString)"))) 104 | 105 | vBoxLayout.addWidget(groupBox) 106 | vBoxLayout.addStretch(1) 107 | 108 | 109 | class MONAIVizSettingsPanel(ctk.ctkSettingsPanel): 110 | def __init__(self, *args, **kwargs): 111 | ctk.ctkSettingsPanel.__init__(self, *args, **kwargs) 112 | self.ui = _ui_MONAIVizSettingsPanel(self) 113 | 114 | 115 | class MONAIVizWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 116 | """Uses ScriptedLoadableModuleWidget base class, available at: 117 | https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py 118 | """ 119 | 120 | def __init__(self, parent=None): 121 | """ 122 | Called when the user opens the module the first time and the widget is initialized. 123 | """ 124 | ScriptedLoadableModuleWidget.__init__(self, parent) 125 | VTKObservationMixin.__init__(self) # needed for parameter node observation 126 | self.logic = None 127 | self._parameterNode = None 128 | self._updatingGUIFromParameterNode = False 129 | self.transforms = None 130 | self.tmpdir = slicer.util.tempDirectory("slicer-monai-viz", includeDateTime=False) 131 | print(f"Using Temp Directory: {self.tmpdir}") 132 | 133 | self.ctx = TransformCtx() 134 | 135 | def setup(self): 136 | ScriptedLoadableModuleWidget.setup(self) 137 | 138 | uiWidget = slicer.util.loadUI(self.resourcePath("UI/MONAIViz.ui")) 139 | self.layout.addWidget(uiWidget) 140 | self.ui = slicer.util.childWidgetVariables(uiWidget) 141 | uiWidget.setMRMLScene(slicer.mrmlScene) 142 | 143 | self.logic = MONAIVizLogic() 144 | 145 | # Connections 146 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) 147 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) 148 | 149 | self.ui.addTransformButton.connect("clicked(bool)", self.onAddTransform) 150 | self.ui.editTransformButton.connect("clicked(bool)", self.onEditTransform) 151 | self.ui.removeTransformButton.connect("clicked(bool)", self.onRemoveTransform) 152 | self.ui.moveUpButton.connect("clicked(bool)", self.onMoveUpTransform) 153 | self.ui.moveDownButton.connect("clicked(bool)", self.onMoveDownTransform) 154 | self.ui.loadTransformButton.connect("clicked(bool)", self.onLoadTransform) 155 | self.ui.saveTransformButton.connect("clicked(bool)", self.onSaveTransform) 156 | self.ui.modulesComboBox.connect("currentIndexChanged(int)", self.onSelectModule) 157 | self.ui.transformTable.connect("cellClicked(int, int)", self.onSelectTransform) 158 | self.ui.transformTable.connect("cellDoubleClicked(int, int)", self.onEditTransform) 159 | self.ui.importBundleButton.connect("clicked(bool)", self.onImportBundle) 160 | self.ui.runTransformButton.connect("clicked(bool)", self.onRunTransform) 161 | self.ui.clearTransformButton.connect("clicked(bool)", self.onClearTransform) 162 | self.ui.previewTransformButton.connect("clicked(bool)", self.onShowDictionary) 163 | 164 | # Make sure parameter node is initialized (needed for module reload) 165 | self.initializeParameterNode() 166 | 167 | self.ui.importBundleButton.setIcon(self.icon("download.png")) 168 | self.ui.addTransformButton.setIcon(self.icon("icons8-insert-row-48.png")) 169 | self.ui.removeTransformButton.setIcon(self.icon("icons8-delete-row-48.png")) 170 | self.ui.editTransformButton.setIcon(self.icon("icons8-edit-row-48.png")) 171 | self.ui.runTransformButton.setIcon(self.icon("icons8-red-circle-48.png")) 172 | self.ui.previewTransformButton.setIcon(self.icon("icons8-preview-48.png")) 173 | self.ui.clearTransformButton.setIcon(self.icon("icons8-delete-document-48.png")) 174 | self.ui.loadTransformButton.setIcon(self.icon("icons8-load-48.png")) 175 | self.ui.saveTransformButton.setIcon(self.icon("icons8-save-48.png")) 176 | 177 | headers = ["Active", "Status", "Target", "Args"] 178 | self.ui.transformTable.setColumnCount(len(headers)) 179 | self.ui.transformTable.setHorizontalHeaderLabels(headers) 180 | self.ui.transformTable.setColumnWidth(0, 40) 181 | self.ui.transformTable.setColumnWidth(1, 60) 182 | self.ui.transformTable.setColumnWidth(2, 160) 183 | self.ui.transformTable.setEditTriggers(qt.QTableWidget.NoEditTriggers) 184 | self.ui.transformTable.setSelectionBehavior(qt.QTableView.SelectRows) 185 | 186 | # self.ui.imagePathLineEdit.setCurrentPath("C:/Dataset/Radiology/Task09_Spleen/imagesTr/spleen_2.nii.gz") 187 | # self.ui.labelPathLineEdit.setCurrentPath("C:/Dataset/Radiology/Task09_Spleen/labelsTr/spleen_2.nii.gz") 188 | self.ui.textEdit.setText("{}") 189 | 190 | self.refreshVersion() 191 | 192 | def cleanup(self): 193 | self.removeObservers() 194 | 195 | def enter(self): 196 | self.initializeParameterNode() 197 | 198 | def exit(self): 199 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 200 | 201 | def onSceneStartClose(self, caller, event): 202 | self.setParameterNode(None) 203 | 204 | def onSceneEndClose(self, caller, event): 205 | if self.parent.isEntered: 206 | self.initializeParameterNode() 207 | 208 | def initializeParameterNode(self): 209 | self.setParameterNode(self.logic.getParameterNode()) 210 | 211 | # Select default input nodes if nothing is selected yet to save a few clicks for the user 212 | if not self._parameterNode.GetNodeReference("InputVolume"): 213 | firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") 214 | if firstVolumeNode: 215 | self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID()) 216 | 217 | def setParameterNode(self, inputParameterNode): 218 | if inputParameterNode: 219 | self.logic.setDefaultParameters(inputParameterNode) 220 | 221 | # Unobserve previously selected parameter node and add an observer to the newly selected. 222 | # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module 223 | # those are reflected immediately in the GUI. 224 | if self._parameterNode is not None: 225 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 226 | self._parameterNode = inputParameterNode 227 | if self._parameterNode is not None: 228 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 229 | 230 | # Initial GUI update 231 | self.updateGUIFromParameterNode() 232 | 233 | def updateGUIFromParameterNode(self, caller=None, event=None): 234 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 235 | return 236 | 237 | # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) 238 | self._updatingGUIFromParameterNode = True 239 | 240 | # All the GUI updates are done 241 | self._updatingGUIFromParameterNode = False 242 | 243 | def updateParameterNodeFromGUI(self, caller=None, event=None): 244 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 245 | return 246 | 247 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch 248 | 249 | # self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value)) 250 | 251 | self._parameterNode.EndModify(wasModified) 252 | 253 | def icon(self, name="MONAILabel.png"): 254 | # It should not be necessary to modify this method 255 | iconPath = os.path.join(os.path.dirname(__file__), "Resources", "Icons", name) 256 | if os.path.exists(iconPath): 257 | return qt.QIcon(iconPath) 258 | return qt.QIcon() 259 | 260 | def refreshVersion(self): 261 | print("Refreshing Version...") 262 | 263 | self.ui.monaiVersionComboBox.clear() 264 | monai = self.logic.importMONAI() 265 | version = monai.__version__ 266 | 267 | self.ui.monaiVersionComboBox.addItem(version) 268 | self.ui.monaiVersionComboBox.setCurrentText(version) 269 | 270 | self.refreshTransforms() 271 | 272 | # bundle names 273 | auth_token = slicer.util.settingsValue("MONAIViz/bundleAuthToken", "") 274 | auth_token = auth_token if auth_token else None 275 | bundles = MonaiUtils.list_bundles(auth_token=auth_token) 276 | 277 | self.ui.bundlesComboBox.clear() 278 | self.ui.bundlesComboBox.addItems(list(sorted({b[0] for b in bundles}))) 279 | idx = max(0, self.ui.bundlesComboBox.findText("spleen_ct_segmentation")) 280 | self.ui.bundlesComboBox.setCurrentIndex(idx) 281 | 282 | self.ui.bundleStageComboBox.clear() 283 | self.ui.bundleStageComboBox.addItems(["pre"]) 284 | 285 | def refreshTransforms(self): 286 | if not self.ui.monaiVersionComboBox.currentText: 287 | return 288 | 289 | module = slicer.util.settingsValue("MONAIViz/transformsPath", "monai.transforms") 290 | print(f"Refreshing Transforms for module: {module}") 291 | self.transforms = MonaiUtils.list_transforms(module) 292 | 293 | self.ui.modulesComboBox.clear() 294 | self.ui.modulesComboBox.addItem("monai.transforms") 295 | self.ui.modulesComboBox.addItems(sorted(list({v["module"] for v in self.transforms.values()}))) 296 | 297 | idx = max(0, self.ui.modulesComboBox.findText("monai.transforms.io.dictionary")) 298 | self.ui.modulesComboBox.setCurrentIndex(idx) 299 | # self.onSelectModule(self.ui.modulesComboBox.currentText) 300 | 301 | def onImportBundle(self): 302 | if not self.ui.monaiVersionComboBox.currentText: 303 | return 304 | 305 | name = self.ui.bundlesComboBox.currentText 306 | bundle_dir = os.path.join(self.tmpdir, "bundle") 307 | this_bundle = os.path.join(bundle_dir, name) 308 | if not os.path.exists(this_bundle): 309 | if not slicer.util.confirmOkCancelDisplay( 310 | f"This will download bundle: {name} from MONAI ZOO.\n\nAre you sure to continue?" 311 | ): 312 | return 313 | 314 | try: 315 | qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) 316 | 317 | print(f"Downloading {name} to {bundle_dir}") 318 | auth_token = slicer.util.settingsValue("MONAIViz/bundleAuthToken", "") 319 | auth_token = auth_token if auth_token else None 320 | MonaiUtils.download_bundle(name, bundle_dir, auth_token=auth_token) 321 | finally: 322 | qt.QApplication.restoreOverrideCursor() 323 | 324 | transforms = MonaiUtils.transforms_from_bundle(name, bundle_dir) 325 | 326 | table = self.ui.transformTable 327 | table.clearContents() 328 | table.setRowCount(len(transforms)) 329 | 330 | # Temporary:: clear current scene 331 | slicer.mrmlScene.Clear(0) 332 | 333 | for pos, t in enumerate(transforms): 334 | name = t["_target_"] 335 | args = copy.copy(t) 336 | args.pop("_target_") 337 | 338 | print(f"Importing Transform: {name} => {args}") 339 | # table.setCellWidget(pos, 0, EditButtonsWidget()) 340 | 341 | box = qt.QCheckBox() 342 | box.setChecked(True) 343 | box.setProperty("row", pos) 344 | widget = qt.QWidget() 345 | box.connect("clicked(bool)", lambda checked: self.onBoxClicked(checked, box.row)) 346 | layout = qt.QHBoxLayout(widget) 347 | layout.addWidget(box) 348 | layout.setAlignment(qt.Qt.AlignCenter) 349 | layout.setContentsMargins(0, 0, 0, 0) 350 | widget.setLayout(layout) 351 | 352 | table.setCellWidget(pos, 0, widget) 353 | 354 | item = qt.QTableWidgetItem() 355 | item.setIcon(self.icon("icons8-yellow-circle-48.png")) 356 | table.setItem(pos, 1, item) 357 | 358 | table.setItem(pos, 2, qt.QTableWidgetItem(name)) 359 | table.setItem(pos, 3, qt.QTableWidgetItem(ClassUtils.args_to_expression(args))) 360 | 361 | def onSelectModule(self): 362 | module = self.ui.modulesComboBox.currentText 363 | print(f"Selected Module: {module}") 364 | 365 | filtered = [k for k, v in self.transforms.items() if module == "monai.transforms" or v["module"] == module] 366 | filtered = sorted([f.split(".")[-1] for f in filtered]) 367 | self.ui.transformsComboBox.clear() 368 | self.ui.transformsComboBox.addItems(filtered) 369 | 370 | def onBoxClicked(self, clicked, current_row): 371 | next_idx = current_row 372 | next_exp = self.get_exp(next_idx) 373 | self.ctx.set_next(next_idx, next_exp) 374 | self.ctx.reset() 375 | 376 | def onSelectTransform(self, row, col): 377 | selected = True if row >= 0 and self.ui.transformTable.rowCount else False 378 | self.ui.editTransformButton.setEnabled(selected) 379 | self.ui.removeTransformButton.setEnabled(selected) 380 | self.ui.moveUpButton.setEnabled(selected and row > 0) 381 | self.ui.moveDownButton.setEnabled(selected and row < self.ui.transformTable.rowCount - 1) 382 | self.ui.runTransformButton.setEnabled(selected) 383 | self.ui.clearTransformButton.setEnabled(self.ctx.valid()) 384 | self.ui.saveTransformButton.setEnabled(selected) 385 | 386 | def onEditTransform(self, row=-1, col=-1): 387 | print(f"Selected Transform for Edit: {row}") 388 | row = self.ui.transformTable.currentRow() if row < 0 else row 389 | if row < 0: 390 | return 391 | 392 | name = str(self.ui.transformTable.item(row, 2).text()) 393 | exp = str(self.ui.transformTable.item(row, 3).text()) 394 | 395 | doc_html = os.path.join(self.tmpdir, "transforms.html") 396 | doc_url = f"https://docs.monai.io/en/{self.ui.monaiVersionComboBox.currentText}/transforms.html" 397 | if not os.path.exists(doc_html): 398 | with open(doc_html, "wb", encoding="utf-8") as fp: 399 | fp.write(requests.get(doc_url).content) 400 | 401 | with open(doc_html, encoding="utf-8") as fp: 402 | contents = fp.readlines() 403 | 404 | doc_section = tempfile.NamedTemporaryFile(suffix=".html").name 405 | short_name = name.split(".")[-1].lower() 406 | 407 | sb = (f'
', f"
") 408 | sc = -1 409 | found = False 410 | with open(doc_section, "w", encoding="utf-8") as fp: 411 | for c in contents: 412 | c = c.rstrip() 413 | if c in sb: 414 | sc = 1 415 | elif sc > 0: 416 | if c.startswith(""): 419 | sc -= 1 420 | 421 | if sc > 0: 422 | c = c.replace('[source]', "") 423 | c = c.replace("#", "") 424 | c = c.replace('href="', 'href="' + doc_url) 425 | fp.write(c) 426 | fp.write(os.linesep) 427 | found = True 428 | if sc == 0: 429 | fp.write(c) 430 | fp.write(os.linesep) 431 | break 432 | 433 | if not found: 434 | fp.write(f'

Visit MONAI Docs for more information

') 435 | 436 | buffer_rows = int(slicer.util.settingsValue("MONAIViz/bufferArgs", "15")) 437 | dlg = CustomDialog(self.resourcePath, name, ClassUtils.expression_to_args(exp), doc_section, buffer_rows) 438 | dlg.exec() 439 | os.unlink(doc_section) 440 | 441 | if dlg.updatedArgs is not None: 442 | new_exp = ClassUtils.args_to_expression(dlg.updatedArgs) 443 | print(f"Old:: {exp}") 444 | print(f"New:: {new_exp}") 445 | if exp != new_exp: 446 | if row < self.ctx.next_idx or row == self.ui.transformTable.rowCount - 1: 447 | self.onClearTransform() 448 | self.ui.transformTable.item(row, 3).setText(new_exp) 449 | print("Updated for new args...") 450 | 451 | def onAddTransform(self): 452 | print(f"Adding Transform: {self.ui.modulesComboBox.currentText}.{self.ui.transformsComboBox.currentText}") 453 | if not self.ui.modulesComboBox.currentText or not self.ui.transformsComboBox.currentText: 454 | return 455 | 456 | t = self.ui.transformsComboBox.currentText 457 | m = self.ui.modulesComboBox.currentText 458 | 459 | v = "" 460 | if t[-1] == "d": # this is a dictionary transform 461 | # now exclude some transforms whose name happens to end with d 462 | if t not in ["AffineGrid", "Decollated", "RandAffineGrid", "RandDeformGrid"]: 463 | image_key = slicer.util.settingsValue("SlicerMONAIViz/imageKey", "image") 464 | label_key = slicer.util.settingsValue("SlicerMONAIViz/labelKey", "label") 465 | v = f"keys=['{image_key}', '{label_key}']" 466 | 467 | self.addTransform(-1, None, t, v) 468 | 469 | def addTransform(self, pos, m, t, v, active=True): 470 | table = self.ui.transformTable 471 | pos = pos if pos >= 0 else table.rowCount if table.currentRow() < 0 else table.currentRow() + 1 472 | 473 | table.insertRow(pos) 474 | # table.setCellWidget(pos, 0, EditButtonsWidget()) 475 | 476 | box = qt.QCheckBox() 477 | box.setChecked(active) 478 | box.setProperty("row", pos) 479 | widget = qt.QWidget() 480 | box.connect("clicked(bool)", lambda checked: self.onBoxClicked(checked, box.row)) 481 | layout = qt.QHBoxLayout(widget) 482 | layout.addWidget(box) 483 | layout.setAlignment(qt.Qt.AlignCenter) 484 | layout.setContentsMargins(0, 0, 0, 0) 485 | widget.setLayout(layout) 486 | 487 | table.setCellWidget(pos, 0, widget) 488 | 489 | item = qt.QTableWidgetItem() 490 | item.setIcon(self.icon("icons8-yellow-circle-48.png")) 491 | table.setItem(pos, 1, item) 492 | 493 | table.setItem(pos, 2, qt.QTableWidgetItem(f"{m}.{t}" if m else t)) 494 | table.setItem(pos, 3, qt.QTableWidgetItem(v if v else "")) 495 | 496 | table.selectRow(pos) 497 | self.onSelectTransform(pos, 0) 498 | 499 | def onRemoveTransform(self): 500 | row = self.ui.transformTable.currentRow() 501 | if row < 0: 502 | return 503 | self.ui.transformTable.removeRow(row) 504 | self.onSelectTransform(-1, -1) 505 | 506 | def onMoveUpTransform(self): 507 | row = self.ui.transformTable.currentRow() 508 | if row < 0: 509 | return 510 | 511 | t = str(self.ui.transformTable.item(row, 2).text()) 512 | v = str(self.ui.transformTable.item(row, 3).text()) 513 | active = self.ui.transformTable.cellWidget(row, 0).findChild("QCheckBox").isChecked() 514 | self.onRemoveTransform() 515 | self.addTransform(row - 1, None, t, v, active) 516 | 517 | def onMoveDownTransform(self): 518 | row = self.ui.transformTable.currentRow() 519 | if row < 0: 520 | return 521 | 522 | t = str(self.ui.transformTable.item(row, 2).text()) 523 | v = str(self.ui.transformTable.item(row, 3).text()) 524 | active = self.ui.transformTable.cellWidget(row, 0).findChild("QCheckBox").isChecked() 525 | self.onRemoveTransform() 526 | self.addTransform(row + 1, None, t, v, active) 527 | 528 | def prepare_dict(self): 529 | image = self.ui.imagePathLineEdit.currentPath 530 | label = self.ui.labelPathLineEdit.currentPath 531 | additional = json.loads(self.ui.textEdit.toPlainText()) 532 | 533 | image_key = slicer.util.settingsValue("MONAIViz/imageKey", "image") 534 | label_key = slicer.util.settingsValue("MONAIViz/labelKey", "label") 535 | 536 | d = {image_key: image, **additional} 537 | if label: 538 | d[label_key] = label 539 | return d 540 | 541 | def get_exp(self, row): 542 | name = str(self.ui.transformTable.item(row, 2).text()) 543 | args = str(self.ui.transformTable.item(row, 3).text()) 544 | return f"monai.transforms.{name}({args})" 545 | 546 | def onRunTransform(self): 547 | if not self.ui.imagePathLineEdit.currentPath: 548 | slicer.util.errorDisplay("Image is not selected!") 549 | return 550 | 551 | current_row = self.ui.transformTable.currentRow() 552 | print(f"Current Row: {current_row}; Total: {self.ui.transformTable.rowCount}") 553 | if current_row < 0: 554 | return 555 | 556 | image_key = slicer.util.settingsValue("MONAIViz/imageKey", "image") 557 | label_key = slicer.util.settingsValue("MONAIViz/labelKey", "label") 558 | 559 | try: 560 | qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) 561 | # Temporary:: clear current scene 562 | slicer.mrmlScene.Clear(0) 563 | 564 | current_exp = self.get_exp(current_row) 565 | d = self.ctx.get_d(current_exp, d=self.prepare_dict()) 566 | 567 | import monai 568 | 569 | print(monai.__version__) 570 | 571 | if self.ctx.last_exp != current_exp: 572 | for row in range(self.ctx.next_idx, current_row + 1): 573 | if self.ui.transformTable.cellWidget(row, 0).findChild("QCheckBox").isChecked(): 574 | exp = self.get_exp(row) 575 | print("") 576 | print("====================================================================") 577 | print(f"Run:: {exp}") 578 | print("====================================================================") 579 | 580 | t = eval(exp) 581 | if isinstance(d, list): 582 | d = [t(dx) for dx in d] # Batched Transforms 583 | else: 584 | d = t(d) 585 | 586 | self.ctx.set_d(d, exp, key=image_key) 587 | self.ui.transformTable.item(row, 1).setIcon(self.icon("icons8-green-circle-48.png")) 588 | else: 589 | self.ui.transformTable.item(row, 1).setIcon(self.icon("icons8-yellow-circle-48.png")) 590 | continue 591 | 592 | next_idx = current_row 593 | next_exp = self.get_exp(next_idx) 594 | if current_row + 1 < self.ui.transformTable.rowCount: 595 | next_idx = current_row + 1 596 | next_exp = self.get_exp(next_idx) 597 | 598 | self.ui.transformTable.selectRow(next_idx) 599 | for row in range(next_idx, self.ui.transformTable.rowCount): 600 | self.ui.transformTable.item(row, 1).setIcon(self.icon("icons8-yellow-circle-48.png")) 601 | 602 | v = self.ctx.get_tensor(key=image_key) 603 | volumeNode = slicer.util.addVolumeFromArray(v) 604 | 605 | origin, spacing, direction = self.ctx.get_tensor_osd(key=image_key) 606 | volumeNode.SetName(os.path.basename(self.ui.imagePathLineEdit.currentPath)) 607 | volumeNode.SetOrigin(origin) 608 | volumeNode.SetSpacing(spacing) 609 | volumeNode.SetIJKToRASDirections(direction) 610 | # logging.info(f"Volume direction: {direction}") 611 | 612 | l = self.ctx.get_tensor(key=label_key) 613 | labelNode = None 614 | if l is not None: 615 | labelNode = slicer.util.addVolumeFromArray(l, nodeClassName="vtkMRMLLabelMapVolumeNode") 616 | origin, spacing, direction = self.ctx.get_tensor_osd(key=label_key) 617 | labelNode.SetName(os.path.basename(self.ui.labelPathLineEdit.currentPath)) 618 | labelNode.SetOrigin(origin) 619 | labelNode.SetSpacing(spacing) 620 | labelNode.SetIJKToRASDirections(direction) 621 | # logging.info(f"Label direction: {direction}") 622 | slicer.util.setSliceViewerLayers(volumeNode, label=labelNode, fit=True) 623 | 624 | self.ctx.set_next(next_idx, next_exp) 625 | self.ui.clearTransformButton.setEnabled(self.ctx.valid()) 626 | finally: 627 | qt.QApplication.restoreOverrideCursor() 628 | 629 | def onClearTransform(self): 630 | self.ctx.reset() 631 | for row in range(0, self.ui.transformTable.rowCount): 632 | self.ui.transformTable.item(row, 1).setIcon(self.icon("icons8-yellow-circle-48.png")) 633 | self.ui.clearTransformButton.setEnabled(self.ctx.valid()) 634 | 635 | def onLoadTransform(self): 636 | fname = qt.QFileDialog().getOpenFileName(None, "Select json file to import", "", "(*.json)") 637 | if fname: 638 | with open(fname) as transformFile: 639 | transforms = json.load(transformFile) 640 | 641 | for idx, transform in transforms.items(): 642 | t = transform["name"] 643 | v = "" 644 | 645 | if t[-1] == "d": # this is a dictionary transform 646 | # now exclude some transforms whose name happens to end with d 647 | if t not in ["AffineGrid", "Decollated", "RandAffineGrid", "RandDeformGrid"]: 648 | v = transform["args"] 649 | 650 | self.addTransform(int(idx), None, t, v) 651 | 652 | def onSaveTransform(self): 653 | fname = qt.QFileDialog().getSaveFileName(None, "Save file", "", "json (*.json)") 654 | if fname: 655 | rows = self.ui.transformTable.rowCount 656 | table = {} 657 | for row in range(rows): 658 | name = str(self.ui.transformTable.item(row, 2).text()) 659 | args = str(self.ui.transformTable.item(row, 3).text()) 660 | table[row] = {"name": name, "args": args} 661 | 662 | with open(fname, "w") as output: 663 | json.dump(table, output) 664 | 665 | def onShowDictionary(self): 666 | dlg = TransformDictDialog(self.ctx.get_d(None, d=self.prepare_dict()), self.resourcePath) 667 | dlg.exec() 668 | 669 | 670 | class EditButtonsWidget(qt.QWidget): 671 | def __init__(self, parent=None): 672 | super().__init__(parent) 673 | 674 | layout = qt.QHBoxLayout() 675 | layout.setContentsMargins(0, 0, 0, 0) 676 | layout.setSpacing(0) 677 | 678 | b1 = qt.QPushButton("") 679 | b1.setIcon(self.icon("icons8-green-circle-16.png")) 680 | b1.setMaximumWidth(20) 681 | layout.addWidget(b1) 682 | 683 | b2 = qt.QPushButton("") 684 | b2.setIcon(self.icon("icons8-preview-16.png")) 685 | b2.setMaximumWidth(20) 686 | layout.addWidget(b2) 687 | 688 | b3 = qt.QPushButton("") 689 | b3.setIcon(self.icon("icons8-delete-document-16.png")) 690 | b3.setMaximumWidth(20) 691 | layout.addWidget(b3) 692 | 693 | self.setLayout(layout) 694 | 695 | def icon(self, name): 696 | # It should not be necessary to modify this method 697 | iconPath = os.path.join(os.path.dirname(__file__), "Resources", "Icons", name) 698 | if os.path.exists(iconPath): 699 | return qt.QIcon(iconPath) 700 | return qt.QIcon() 701 | 702 | 703 | class CustomDialog(qt.QDialog): 704 | def __init__(self, resourcePath, name, args, doc_html, buffer_rows): 705 | super().__init__() 706 | self.name = name 707 | self.args = args 708 | self.updatedArgs = None 709 | self.buffer_rows = buffer_rows 710 | 711 | short_name = name.split(".")[-1] 712 | self.setWindowTitle(f"Edit - {short_name}") 713 | print(f"{name} => {args}") 714 | 715 | layout = qt.QVBoxLayout() 716 | uiWidget = slicer.util.loadUI(resourcePath("UI/MONAITransformDialog.ui")) 717 | layout.addWidget(uiWidget) 718 | 719 | self.ui = slicer.util.childWidgetVariables(uiWidget) 720 | self.setLayout(layout) 721 | 722 | url = f"https://docs.monai.io/en/stable/transforms.html#{short_name.lower()}" 723 | self.ui.nameLabel.setText('' + short_name + "") 724 | 725 | headers = ["Name", "Value"] 726 | table = self.ui.tableWidget 727 | table.setRowCount(len(args) + buffer_rows) 728 | table.setColumnCount(len(headers)) 729 | table.setHorizontalHeaderLabels(headers) 730 | table.setColumnWidth(0, 150) 731 | table.setColumnWidth(1, 200) 732 | 733 | for row, (k, v) in enumerate(args.items()): 734 | table.setItem(row, 0, qt.QTableWidgetItem(k)) 735 | table.setItem(row, 1, qt.QTableWidgetItem(str(v))) 736 | 737 | self.ui.updateButton.connect("clicked(bool)", self.onUpdate) 738 | self.ui.webEngineView.url = qt.QUrl.fromLocalFile(doc_html) 739 | 740 | def onUpdate(self): 741 | args = {} 742 | table = self.ui.tableWidget 743 | for row in range(table.rowCount): 744 | k = table.item(row, 0) 745 | k = str(k.text()) if k else None 746 | v = table.item(row, 1) 747 | v = str(v.text()) if v else None 748 | if k: 749 | print(f"Row: {row} => {k} => {v}") 750 | try: 751 | v = eval(v) if v else v 752 | except: 753 | pass 754 | args[k] = v 755 | 756 | self.updatedArgs = args 757 | self.close() 758 | 759 | 760 | class TransformDictDialog(qt.QDialog): 761 | def __init__(self, data, resourcePath): 762 | super().__init__() 763 | 764 | self.setWindowTitle("Dictionary Data") 765 | print(f"{data.keys()}") 766 | 767 | layout = qt.QVBoxLayout() 768 | uiWidget = slicer.util.loadUI(resourcePath("UI/MONAIDictionaryDialog.ui")) 769 | layout.addWidget(uiWidget) 770 | 771 | self.ui = slicer.util.childWidgetVariables(uiWidget) 772 | self.setLayout(layout) 773 | 774 | s = StringIO() 775 | pprint.pprint(data, s, indent=2) 776 | self.ui.dataTextEdit.setPlainText(s.getvalue()) 777 | 778 | headers = ["Key", "Type", "Shape", "Value"] 779 | tree = self.ui.treeWidget 780 | tree.setColumnCount(len(headers)) 781 | tree.setHeaderLabels(headers) 782 | tree.setColumnWidth(0, 150) 783 | tree.setColumnWidth(1, 75) 784 | tree.setColumnWidth(2, 100) 785 | 786 | def get_val(v): 787 | if type(v) in (int, float, bool, str): 788 | return str(v) 789 | 790 | s = StringIO() 791 | pprint.pprint(v, s, compact=True, indent=1, width=-1) 792 | return s.getvalue().replace("\n", "") 793 | 794 | items = [] 795 | for key, val in data.items(): 796 | if isinstance(val, dict): 797 | item = qt.QTreeWidgetItem([key]) 798 | for k1, v1 in val.items(): 799 | tvals = [k1, type(v1).__name__, v1.shape if hasattr(v1, "shape") else "", get_val(v1)] 800 | child = qt.QTreeWidgetItem(tvals) 801 | item.addChild(child) 802 | else: 803 | tvals = [key, type(val).__name__, val.shape if hasattr(val, "shape") else "", get_val(val)] 804 | item = qt.QTreeWidgetItem(tvals) 805 | items.append(item) 806 | 807 | tree.insertTopLevelItems(0, items) 808 | 809 | 810 | class MONAIVizLogic(ScriptedLoadableModuleLogic): 811 | def __init__(self): 812 | ScriptedLoadableModuleLogic.__init__(self) 813 | self.torchLogic = PyTorchUtils.PyTorchUtilsLogic() 814 | 815 | def setDefaultParameters(self, parameterNode): 816 | # if not parameterNode.GetParameter("Threshold"): 817 | # parameterNode.SetParameter("Threshold", "100.0") 818 | pass 819 | 820 | def process(self): 821 | import time 822 | 823 | startTime = time.time() 824 | logging.info("Processing started") 825 | 826 | stopTime = time.time() 827 | logging.info(f"Processing completed in {stopTime - startTime:.2f} seconds") 828 | 829 | def importMONAI(self): 830 | if not self.torchLogic.torchInstalled(): 831 | logging.info("PyTorch module not found") 832 | torch = self.torchLogic.installTorch(askConfirmation=True) 833 | if torch is None: 834 | slicer.util.errorDisplay( 835 | "PyTorch needs to be installed to use the MONAI extension." 836 | " Please reload this module to install PyTorch." 837 | ) 838 | return None 839 | try: 840 | import monai 841 | except ModuleNotFoundError: 842 | with self.showWaitCursor(), self.peakPythonConsole(): 843 | monai = self.installMONAI() 844 | logging.info(f"MONAI {monai.__version__} imported correctly") 845 | return monai 846 | 847 | @staticmethod 848 | def installMONAI(confirm=True): 849 | if confirm and not slicer.app.commandOptions().testingEnabled: 850 | install = slicer.util.confirmOkCancelDisplay( 851 | "MONAI will be downloaded and installed now. The process might take some minutes." 852 | ) 853 | if not install: 854 | logging.info("Installation of MONAI aborted by user") 855 | return None 856 | slicer.util.pip_install("monai[itk,nibabel,tqdm]") 857 | import monai 858 | 859 | logging.info(f"MONAI {monai.__version__} installed correctly") 860 | return monai 861 | 862 | 863 | class MONAIVizTest(ScriptedLoadableModuleTest): 864 | def setUp(self): 865 | slicer.mrmlScene.Clear() 866 | 867 | def runTest(self): 868 | self.setUp() 869 | self.test_MONAIViz1() 870 | 871 | def test_MONAIViz1(self): 872 | self.delayDisplay("Starting the test") 873 | self.delayDisplay("Test passed") 874 | 875 | 876 | class TransformCtx: 877 | def __init__(self): 878 | self.d = None 879 | self.last_exp = "" 880 | self.next_idx = 0 881 | self.next_exp = "" 882 | self.channel = False 883 | self.bidx = 0 884 | self.original_spatial_shape = None 885 | self.original_affine = None 886 | 887 | def reset(self): 888 | self.__init__() 889 | 890 | def valid(self) -> bool: 891 | return False if self.d is None or self.next_idx == 0 else True 892 | 893 | def valid_for_next(self, exp) -> bool: 894 | return True if exp and self.next_exp and exp == self.next_exp else False 895 | 896 | def get_d(self, exp, d=None): 897 | if exp is None: 898 | if self.valid(): 899 | bidx = self.bidx % len(self.d) if isinstance(self.d, list) else -1 900 | return self.d[bidx] if bidx >= 0 else self.d 901 | return d 902 | 903 | if not self.valid_for_next(exp): 904 | self.reset() 905 | 906 | if not self.valid(): 907 | print(d) 908 | return d 909 | return self.d 910 | 911 | def set_d(self, d, exp, key): 912 | key_tensor = d[self.bidx % len(d)][key] if isinstance(d, list) else d[key] 913 | print(f"{key}: {key_tensor.shape}") 914 | 915 | if self.original_spatial_shape is None: 916 | self.original_spatial_shape = key_tensor.shape 917 | self.original_affine = key_tensor.affine.numpy() 918 | 919 | if "EnsureChannelFirstd" in exp: 920 | self.channel = True 921 | 922 | self.d = d 923 | self.last_exp = exp 924 | 925 | def set_next(self, next_idx, next_exp): 926 | if self.next_idx == next_idx and self.next_exp == next_exp: 927 | self.bidx += 1 928 | else: 929 | self.next_idx = next_idx 930 | self.next_exp = next_exp 931 | 932 | def get_tensor(self, key, transpose=True): 933 | import numpy as np 934 | import torch 935 | 936 | bidx = self.bidx % len(self.d) if isinstance(self.d, list) else -1 937 | d = self.d[bidx] if bidx >= 0 else self.d 938 | if d.get(key) is None: 939 | return None 940 | 941 | key_tensor = d[key] 942 | if isinstance(key_tensor, str) or key_tensor is None: 943 | return None 944 | 945 | v = key_tensor.numpy() if isinstance(key_tensor, torch.Tensor) else key_tensor 946 | v = np.squeeze(v, axis=0) if self.channel else v 947 | v = v.transpose() if transpose else v 948 | 949 | print(f"Display {key}{'[' + str(bidx) + ']' if bidx >= 0 else ''}: {v.shape}") 950 | return v 951 | 952 | def get_tensor_osd(self, key, scale=False): 953 | import numpy as np 954 | from monai.transforms.utils import scale_affine 955 | 956 | bidx = self.bidx % len(self.d) if isinstance(self.d, list) else -1 957 | d = self.d[bidx] if bidx >= 0 else self.d 958 | if d.get(key) is None: 959 | return None 960 | 961 | key_tensor = d[key] 962 | actual_shape = key_tensor.shape[1:] if self.channel else key_tensor.shape 963 | 964 | affine = ( 965 | scale_affine(self.original_affine, self.original_spatial_shape, actual_shape) 966 | if scale 967 | else key_tensor.affine.numpy() 968 | ) 969 | 970 | # convert_aff_mat = np.diag([-1, -1, 1, 1]) # RAS <-> LPS conversion matrix 971 | # affine = convert_aff_mat @ affine # convert from RAS to LPS 972 | 973 | dim = affine.shape[0] - 1 974 | _origin_key = (slice(-1), -1) 975 | _m_key = (slice(-1), slice(-1)) 976 | 977 | origin = affine[_origin_key] 978 | spacing = np.linalg.norm(affine[_m_key] @ np.eye(dim), axis=0) 979 | direction = affine[_m_key] @ np.diag(1 / spacing) 980 | 981 | return origin, spacing, direction 982 | -------------------------------------------------------------------------------- /MONAIViz/MONAIVizLib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from .utils import * 13 | -------------------------------------------------------------------------------- /MONAIViz/MONAIVizLib/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | import importlib 12 | import inspect 13 | import os 14 | from typing import Any 15 | 16 | 17 | class ClassUtils: 18 | @staticmethod 19 | def is_subclass(n, o, base_c): 20 | if inspect.isclass(o) and n != base_c: 21 | b = [cls.__name__ for cls in o.__bases__] 22 | if base_c in b: 23 | return True 24 | return False 25 | 26 | @staticmethod 27 | def get_class_of_subclass(module, base_classes) -> dict[str, Any]: 28 | print(f"{module} => {base_classes}") 29 | res: dict[str, Any] = {} 30 | for n, o in inspect.getmembers(module): 31 | if not inspect.isclass(o) or inspect.isabstract(o): 32 | continue 33 | 34 | # print(f"{n} => {o}") 35 | for base_c in base_classes: 36 | if ClassUtils.is_subclass(n, o, base_c): 37 | cp = f"{o.__module__}.{o.__name__}" 38 | if res.get(cp): 39 | res[cp]["alias"].append(n) 40 | else: 41 | res[cp] = { 42 | "name": o.__name__, 43 | "alias": [n], 44 | "class": cp, 45 | "module": o.__module__, 46 | "dictionary": o.__module__.endswith("dictionary"), 47 | "base_class": base_c, 48 | "category": ".".join(o.__module__.split(".")[:3]), 49 | } 50 | break 51 | 52 | sorted_d: dict[str, Any] = {} 53 | for k in sorted(res.keys()): 54 | v = res[k] 55 | v["alias"] = sorted(v["alias"]) 56 | sorted_d[k] = v 57 | 58 | return sorted_d 59 | 60 | @staticmethod 61 | def args_to_expression(class_args): 62 | key_val = [] 63 | for key in class_args: 64 | val = class_args[key] 65 | if isinstance(val, str): 66 | val = "'" + val + "'" 67 | elif isinstance(val, tuple) or isinstance(val, list): 68 | vals = [] 69 | for v in val: 70 | if isinstance(v, str): 71 | v = "'" + v + "'" 72 | else: 73 | v = str(v) 74 | vals.append(v) 75 | if isinstance(val, tuple): 76 | val = "(" + ", ".join(vals) + ")" 77 | else: 78 | val = "[" + ", ".join(vals) + "]" 79 | else: 80 | val = str(val) 81 | key_val.append(f"{key}={val}") 82 | return ", ".join(key_val) 83 | 84 | @staticmethod 85 | def expression_to_args(exp, handle_bool=True): 86 | if not exp: 87 | return {} 88 | 89 | if handle_bool: 90 | exp = exp.replace("=true", "=True").replace("=false", "=False") # safe to assume 91 | exp = exp.replace(" true", " True").replace(" false", " False") 92 | 93 | def foo(**kwargs): 94 | return kwargs 95 | 96 | return eval("foo(" + exp + ")") 97 | 98 | 99 | class MonaiUtils: 100 | @staticmethod 101 | def list_transforms(module="monai.transforms"): 102 | mt = importlib.import_module(module) 103 | return ClassUtils.get_class_of_subclass(mt, ["Transform", "MapTransform"]) 104 | 105 | @staticmethod 106 | def list_bundles(auth_token=None): 107 | from monai.bundle import get_all_bundles_list 108 | 109 | return get_all_bundles_list(auth_token=auth_token) 110 | 111 | @staticmethod 112 | def download_bundle(name, bundle_dir, auth_token=None): 113 | from monai.bundle import download, get_bundle_versions 114 | 115 | # Support 1.1.0 monai (bug) for downloading bundles on Windows (pass specific version) 116 | version = get_bundle_versions(name, auth_token=auth_token)["latest_version"] 117 | download(name, version=version, bundle_dir=bundle_dir) 118 | 119 | @staticmethod 120 | def transforms_from_bundle(name, bundle_dir): 121 | from monai.bundle import ConfigParser 122 | 123 | bundle_root = os.path.join(bundle_dir, name) 124 | config_path = os.path.join(bundle_root, "configs", "train.json") 125 | if not os.path.exists(config_path): 126 | config_path = os.path.join(bundle_root, "configs", "train.yaml") 127 | 128 | bundle_config = ConfigParser() 129 | bundle_config.read_config(config_path) 130 | bundle_config.config.update({"bundle_root": bundle_root}) # type: ignore 131 | 132 | for k in ["train#preprocessing#transforms", "train#pre_transforms#transforms"]: 133 | if bundle_config.get(k): 134 | c = bundle_config.get_parsed_content(k, instantiate=False) 135 | c = [x.get_config() for x in c] 136 | return c 137 | 138 | return None 139 | 140 | @staticmethod 141 | def run_transform(name, args, data): 142 | import monai 143 | 144 | print(monai.__version__) 145 | 146 | exp = f"monai.transforms.{name}({args if args else ''})" 147 | print(exp) 148 | t = eval(exp) 149 | 150 | print(data) 151 | d = t(data) 152 | return d 153 | 154 | 155 | """ 156 | def main(): 157 | transforms = MonaiUtils.list_transforms() 158 | 159 | print("ALL Transforms....") 160 | print("----------------------------------------------------------------") 161 | for t in transforms: 162 | print(f"{t} => {transforms[t]['module']}") 163 | 164 | modules = sorted(list({v["module"] for v in transforms.values()})) 165 | 166 | print("") 167 | print("ALL Modules....") 168 | print("----------------------------------------------------------------") 169 | for m in modules: 170 | print(f"{m}") 171 | # print(json.dumps(categories, indent=2)) 172 | 173 | print("") 174 | print("ALL Bundles....") 175 | print("----------------------------------------------------------------") 176 | bundles = MonaiUtils.list_bundles() 177 | for b in sorted({b[0] for b in bundles}): 178 | print(b) 179 | 180 | bundle_dir = "/tmp/Slicer-sachi/slicer-monai-transforms/bundle" 181 | bundle_dir = "C:/Users/salle/AppData/Local/Temp/Slicer/slicer-monai-transforms/bundle" 182 | # MonaiUtils.download_bundle( 183 | # "spleen_ct_segmentation", bundle_dir 184 | # ) 185 | 186 | print("") 187 | print("Bundle Transforms....") 188 | print("----------------------------------------------------------------") 189 | b_transforms = MonaiUtils.transforms_from_bundle("spleen_ct_segmentation", bundle_dir) 190 | for t in b_transforms: 191 | print(f"{type(t)} => {t}") 192 | 193 | 194 | def main2(): 195 | data = { 196 | "image": "/localhome/sachi/Datasets/Radiology/Task09_Spleen/imagesTr/spleen_2.nii.gz", 197 | "label": "/localhome/sachi/Datasets/Radiology/Task09_Spleen/labelsTr/spleen_2.nii.gz", 198 | } 199 | MonaiUtils.run_transform(name="LoadImaged", args="keys=['image', 'label']", data=data) 200 | 201 | 202 | if __name__ == "__main__": 203 | # pip_install("monai") 204 | # pip_install("nibabel") 205 | main() 206 | """ 207 | -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/MONAI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/MONAI.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/MONAIViz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/MONAIViz.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/done.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/download.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-delete-document-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-delete-document-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-delete-row-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-delete-row-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-edit-row-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-edit-row-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-green-circle-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-green-circle-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-insert-row-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-insert-row-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-load-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-load-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-preview-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-preview-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-red-circle-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-red-circle-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-save-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-save-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/icons8-yellow-circle-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/icons8-yellow-circle-48.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/refresh-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/MONAIViz/Resources/Icons/refresh-icon.png -------------------------------------------------------------------------------- /MONAIViz/Resources/Icons/upload.svg: -------------------------------------------------------------------------------- 1 | Upload 2 | -------------------------------------------------------------------------------- /MONAIViz/Resources/UI/MONAIDictionaryDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | 15 | 800 16 | 600 17 | 18 | 19 | 20 | Dialog 21 | 22 | 23 | 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | QTreeWidget::item:!selected { border: 1px solid gainsboro; border-left: none; border-top: none; } 34 | 35 | 36 | true 37 | 38 | 39 | QAbstractItemView::SelectItems 40 | 41 | 42 | 10 43 | 44 | 45 | 46 | 1 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | DUMP: 55 | 56 | 57 | 58 | 59 | 60 | 61 | Qt::Horizontal 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /MONAIViz/Resources/UI/MONAITransformDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1000 10 | 450 11 | 12 | 13 | 14 | 15 | 1000 16 | 450 17 | 18 | 19 | 20 | Dialog 21 | 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | Qt::RightToLeft 33 | 34 | 35 | Update 36 | 37 | 38 | 39 | 40 | 41 | 42 | Name: 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 550 56 | 0 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | true 65 | 66 | 67 | 2 68 | 69 | 70 | true 71 | 72 | 73 | 21 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | QWebEngineView 86 | QWidget 87 |
QtWebEngineWidgets/QWebEngineView
88 |
89 |
90 | 91 | 92 |
93 | -------------------------------------------------------------------------------- /MONAIViz/Resources/UI/MONAIViz.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MONAILabelTransforms 4 | 5 | 6 | 7 | 0 8 | 0 9 | 449 10 | 840 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | MONAI Version: 20 | 21 | 22 | 23 | 24 | 25 | 26 | Input 27 | 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | 39 | 0 40 | 0 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Image: 49 | 50 | 51 | 52 | 53 | 54 | 55 | Additional: 56 | 57 | 58 | 59 | 60 | 61 | 62 | Label: 63 | 64 | 65 | 66 | 67 | 68 | 69 | Image path 70 | 71 | 72 | 73 | 74 | 75 | 76 | Label path 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Transform to be added via "Add" button 87 | 88 | 89 | true 90 | 91 | 92 | 93 | 94 | 95 | 96 | List of MONAI modules 97 | 98 | 99 | 100 | 101 | 102 | 103 | Qt::Horizontal 104 | 105 | 106 | 107 | 108 | 109 | 110 | Module: 111 | 112 | 113 | 114 | 115 | 116 | 117 | Transform: 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 48 128 | 16777215 129 | 130 | 131 | 132 | Add a transform 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | false 143 | 144 | 145 | 146 | 48 147 | 16777215 148 | 149 | 150 | 151 | Edit a transform 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | false 162 | 163 | 164 | 165 | 48 166 | 16777215 167 | 168 | 169 | 170 | Remove a transform 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | false 181 | 182 | 183 | 184 | 48 185 | 16777215 186 | 187 | 188 | 189 | Move up in the list 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | false 200 | 201 | 202 | 203 | 48 204 | 16777215 205 | 206 | 207 | 208 | Move down in the list 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | false 219 | 220 | 221 | 222 | 48 223 | 16777215 224 | 225 | 226 | 227 | Run transform 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 48 239 | 16777215 240 | 241 | 242 | 243 | Preview the trasform 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | false 254 | 255 | 256 | 257 | 48 258 | 16777215 259 | 260 | 261 | 262 | Clear a transform 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | true 273 | 274 | 275 | 276 | 48 277 | 16777215 278 | 279 | 280 | 281 | Load transforms 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | false 292 | 293 | 294 | 295 | 48 296 | 16777215 297 | 298 | 299 | 300 | Save transforms as json file 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | Qt::Vertical 311 | 312 | 313 | 314 | 20 315 | 40 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | false 326 | 327 | 328 | 329 | 0 330 | 0 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | The list of transforms 339 | 340 | 341 | true 342 | 343 | 344 | 4 345 | 346 | 347 | true 348 | 349 | 350 | 21 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | Bundle 361 | 362 | 363 | 364 | 0 365 | 366 | 367 | 0 368 | 369 | 370 | 371 | 372 | Name: 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 0 384 | 0 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 48 394 | 16777215 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | Qt::Horizontal 409 | 410 | 411 | 412 | 413 | 414 | 415 | Qt::Horizontal 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | ctkCollapsibleButton 426 | QWidget 427 |
ctkCollapsibleButton.h
428 | 1 429 |
430 | 431 | ctkPathLineEdit 432 | QWidget 433 |
ctkPathLineEdit.h
434 |
435 | 436 | qMRMLWidget 437 | QWidget 438 |
qMRMLWidget.h
439 | 1 440 |
441 |
442 | 443 | 444 |
445 | -------------------------------------------------------------------------------- /MONAIViz/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /MONAIViz/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MONAIViz 2 | 3 | MONAIViz is an extension for 3D Slicer that helps to run chain of MONAI transforms and visualize every stage over an image/label. 4 | 5 | ## Modules 6 | 7 | The extension provides a single module - **MONAIViz** - which currently supports: 8 | - import pre-processing definitions for available bundles from MONAI model zoo 9 | - add/remove/re-order any MONAI transform to the list of transforms 10 | - apply a sequence of transforms step-by-step over input image/label 11 | - visualize image/label outputs for every transform run 12 | - check the data/dictionary stats for every transform run 13 | 14 |
15 | 16 | ![image](Screenshots/1.jpg) 17 | 18 |
19 | 20 | ## Installing Plugin 21 | 22 | ### Installing 3D Slicer 23 | 24 | To use MONAIViz with 3D Slicer, you'll need to download and install 3D Slicer. MONAIViz supports the stable and preview versions of 3D Slicer, version 5.3 or higher. For more information on installing 3D Slicer, check out the [3D Slicer Documentation](https://slicer.readthedocs.io/en/latest/user_guide/getting_started.html#installing-3d-slicer) 25 | 26 | ### Installing MONAIViz Plugin 27 | 28 | - Go to **View** -> **Extension Manager** -> **Developer Tools** -> **MONAIViz** 29 | - Install MONAIViz plugin 30 | - _**Restart**_ 3D Slicer 31 | 32 | **Note:** To update the plugin to the latest version, you have to uninstall the existing 3D Slicer version and download and install the new preview version of 3D Slicer again 33 | 34 | ### Install Plugin in Developer Mode 35 | 36 | - `git clone git@github.com:Project-MONAI/SlicerMONAIViz.git` 37 | - Open 3D Slicer: Go to **Edit** -> **Application Settings** -> **Modules** -> **Additional Module Paths** 38 | - Add New Module Path: __/SlicerMONAIViz/MONAIViz 39 | - _**Restart**_ 3D Slicer 40 | 41 | ## Dependencies 42 | 43 | MONAIViz depends on the following packages: 44 | 45 | * [PyTorch Slicer extension](https://github.com/fepegar/SlicerPyTorch) 46 | * `monai[itk,nibabel]` python packages 47 | 48 | **Note:** After opening the MONAIViz module in 3D Slicer for the first time, you will be asked to confirm the installation of the packages. 49 | -------------------------------------------------------------------------------- /Screenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/Screenshots/1.jpg -------------------------------------------------------------------------------- /Screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/Screenshots/2.jpg -------------------------------------------------------------------------------- /Screenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/Screenshots/3.jpg -------------------------------------------------------------------------------- /Screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Project-MONAI/SlicerMONAIViz/37dcd21d16c8524bc689c00ceda741ef620f8352/Screenshots/4.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Copyright (c) MONAI Consortium 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | [metadata] 13 | name = SlicerMONAIViz 14 | author = MONAI Consortium 15 | author_email = monai.contact@gmail.com 16 | url = https://monai.io/ 17 | description = MONAI Viz 3D-Slicer Plugin for Healthcare Imaging 18 | long_description = file:README.md 19 | long_description_content_type = text/markdown; charset=UTF-8 20 | platforms = OS Independent 21 | license = Apache License 2.0 22 | license_files = 23 | LICENSE 24 | project_urls = 25 | Documentation=https://docs.monai.io/ 26 | Bug Tracker=https://github.com/Project-MONAI/SlicerMONAIViz/issues 27 | Source Code=https://github.com/Project-MONAI/SlicerMONAIViz 28 | 29 | [options] 30 | python_requires = >= 3.7 31 | # for compiling and develop setup only 32 | # no need to specify the versions so that we could 33 | # compile for multiple targeted versions. 34 | setup_requires = 35 | torch 36 | ninja 37 | install_requires = 38 | torch>=1.7 39 | monai[nibabel, torchvision, itk]>=1.1.0 40 | 41 | [flake8] 42 | select = B,C,E,F,N,P,T4,W,B9 43 | max_line_length = 120 44 | # F403 'from module import *' used; unable to detect undefined names 45 | # F405 Name may be undefined, or defined from star import 46 | # E203 whitespace before ':' # incompatible with black style 47 | # E402 module level import not at top of file 48 | # E501 is not flexible enough, we're using B950 instead 49 | # E722 do not use bare 'except' 50 | # C408 ignored because we like the dict keyword argument syntax 51 | # W503 line break before binary operator 52 | ignore = 53 | E203,E302,E303,E305,E402,E501,E721,E722,E741,F403,F405,F821,F841,F999,C408,W291,W503,W504,B008, 54 | # N812 lowercase 'torch.nn.functional' imported as non lowercase 'F' 55 | N812,N818 56 | per_file_ignores = __init__.py: F401 57 | exclude = *.pyi,.git,.eggs,monailabel/_version.py,versioneer.py,venv,.venv,_version.py,slicer,config.py,plugins/ohif 58 | 59 | [isort] 60 | known_first_party = SlicerMONAIViz 61 | profile = black 62 | line_length = 120 63 | skip = .git, .eggs, venv, .venv, versioneer.py, _version.py 64 | skip_glob = *.pyi 65 | 66 | [mypy] 67 | # Suppresses error messages about imports that cannot be resolved. 68 | ignore_missing_imports = True 69 | # Changes the treatment of arguments with a default value of None by not implicitly making their type Optional. 70 | no_implicit_optional = True 71 | # Warns about casting an expression to its inferred type. 72 | warn_redundant_casts = True 73 | # No error on unneeded # type: ignore comments. 74 | warn_unused_ignores = False 75 | # Shows a warning when returning a value with type Any from a function declared with a non-Any return type. 76 | warn_return_any = True 77 | # Prohibit equality checks, identity checks, and container checks between non-overlapping types. 78 | strict_equality = True 79 | # Shows column numbers in error messages. 80 | show_column_numbers = True 81 | # Shows error codes in error messages. 82 | show_error_codes = True 83 | # Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location markers. 84 | pretty = False 85 | # list of files or directories to exclude. 86 | exclude = (plugins|docs|model|tests) 87 | --------------------------------------------------------------------------------