├── .gitattributes ├── .github └── workflows │ ├── notebook-test.yml │ └── publish.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _static │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ └── site.webmanifest │ ├── itkwidgets_logo.png │ └── itkwidgets_logo_small.png ├── advanced.md ├── conf.py ├── deployments.md ├── development.md ├── images │ ├── Hello3DWorld.gif │ ├── colab.png │ ├── dask.png │ ├── dask_stack.png │ ├── imjoy-lab.png │ ├── imjoy-notebook.png │ ├── itkimage.png │ ├── monai_pytorch.png │ ├── numpy.png │ ├── pyimagej.png │ ├── pyvista.png │ ├── terminal_rotate.gif │ ├── vtkpolydata.png │ ├── xarray.png │ ├── xarray2.png │ └── zarr.png ├── index.md ├── integrations.md ├── jupyterlite │ ├── files │ │ └── Hello3DWorld.ipynb │ ├── jupyterlite_config.json │ └── pypi │ │ └── dask_image-2022.9.0-py2.py3-none-any.whl ├── make.bat ├── quick_start_guide.md └── requirements.txt ├── environment.yml ├── examples ├── EnvironmentCheck.ipynb ├── GettersAndSetters.ipynb ├── Hello3DWorld.ipynb ├── NumPyArrayPointSet.ipynb └── integrations │ ├── MONAI │ └── transform_visualization.ipynb │ ├── PyImageJ │ └── ImageJImgLib2.ipynb │ ├── PyVista │ ├── LiDAR.ipynb │ └── UniformGrid.ipynb │ ├── dask │ └── DaskArray.ipynb │ ├── itk │ ├── 3DImage.ipynb │ ├── DICOM.ipynb │ ├── IDC_Seg_Primer_Examples.ipynb │ ├── MulticomponentNumPy.ipynb │ ├── SelectROI.ipynb │ ├── ThinPlateSpline.ipynb │ └── select_roi.gif │ ├── itkwasm │ ├── 3DImage.ipynb │ └── SelectROI.ipynb │ ├── vtk │ ├── vtkImageData.ipynb │ └── vtkPolyDataPointSet.ipynb │ ├── xarray │ └── DataArray.ipynb │ └── zarr │ └── OME-NGFF-Brainstem-MRI.ipynb ├── itkwidgets ├── __init__.py ├── _initialization_params.py ├── _method_types.py ├── _type_aliases.py ├── cell_watcher.py ├── imjoy.py ├── integrations │ ├── __init__.py │ ├── environment.py │ ├── imageio.py │ ├── itk.py │ ├── meshio.py │ ├── monai.py │ ├── numpy.py │ ├── pyimagej.py │ ├── pytorch.py │ ├── pyvista.py │ ├── skan.py │ ├── vedo.py │ ├── vtk.py │ ├── xarray.py │ └── zarr.py ├── render_types.py ├── standalone │ ├── __init__.py │ ├── config.py │ └── index.html ├── standalone_server.py ├── viewer.py └── viewer_config.py ├── pixi.lock ├── pyproject.toml └── utilities └── release-notes.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # GitHub syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/notebook-test.yml: -------------------------------------------------------------------------------- 1 | name: Notebook tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | name: Test notebooks with nbmake 9 | strategy: 10 | matrix: 11 | python-version: ['3.9'] 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - uses: actions/setup-java@v3 23 | with: 24 | java-version: '8' 25 | distribution: 'zulu' 26 | 27 | - name: Install test dependencies 28 | run: | 29 | python3 -m pip install --upgrade pip 30 | python3 -m pip install -e ".[test,all]" 31 | python3 -m pip install pyimagej urllib3 32 | python3 -c "import imagej; ij = imagej.init('2.5.0'); print(ij.getVersion())" 33 | python3 -m pip install "itk>=5.3.0" 34 | 35 | - name: Test notebooks 36 | run: | 37 | pytest --nbmake --nbmake-timeout=3000 examples/EnvironmentCheck.ipynb examples/Hello3DWorld.ipynb examples/NumPyArrayPointSet.ipynb examples/integrations/**/*.ipynb 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Install build dependencies 17 | run: python -m pip install --upgrade hatch 18 | - name: Build 19 | run: hatch build 20 | - name: Publish package 21 | uses: pypa/gh-action-pypi-publish@master 22 | with: 23 | user: __token__ 24 | password: ${{ secrets.PYPI_API_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .ipynb_checkpoints/ 3 | Untitled.ipynb 4 | examples/integrations/itk/005_32months_T2_RegT1_Reg2Atlas_ManualBrainMask_Stripped.nrrd 5 | docs/_* 6 | docs/.jupyterlite.doit.db 7 | docs/.cache 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Environments 15 | .env 16 | .venv 17 | env/ 18 | venv/ 19 | ENV/ 20 | env.bak/ 21 | venv.bak/ 22 | 23 | # JupyterLite build 24 | docs/jupyterlite/.cache/ 25 | docs/jupyterlite/.jupyterlite.doit.db 26 | docs/jupyterlite/_output/ 27 | 28 | # autodoc2 generated 29 | docs/apidocs 30 | 31 | examples/integrations/itk/roi.zarr/ 32 | examples/integrations/itk/roi_gradient.nrrd 33 | examples/integrations/itk/roi_image.nrrd 34 | 35 | examples/integrations/itkwasm/roi.zarr/ 36 | examples/integrations/itkwasm/roi_image.nrrd 37 | # pixi environments 38 | .pixi 39 | *.egg-info 40 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.10" 12 | 13 | sphinx: 14 | configuration: docs/conf.py 15 | 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # itkwidgets 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/itkwidgets/badge/?version=latest)](https://itkwidgets.readthedocs.io/en/latest/?badge=latest) 4 | [![Notebook tests](https://github.com/InsightSoftwareConsortium/itkwidgets/actions/workflows/notebook-test.yml/badge.svg)](https://github.com/InsightSoftwareConsortium/itkwidgets/actions/workflows/notebook-test.yml) 5 | [![DOI](https://zenodo.org/badge/121581663.svg)](https://zenodo.org/doi/10.5281/zenodo.3603358) 6 | 7 | 8 | ITKWidgets is an elegant Python interface for visualization on the web platform to interactively generate insights into multidimensional images, point sets, and geometry. 9 | 10 | ![Hello 3D World](./docs/images/Hello3DWorld.gif) 11 | 12 | # Getting Started 13 | 14 | ## Environment Setup 15 | 16 | The [EnvironmentCheck.ipynb](https://github.com/InsightSoftwareConsortium/itkwidgets/blob/main/examples/EnvironmentCheck.ipynb) checks the environment that you are running in to make sure that all required dependencies and extensions are correctly installed. Ideally run first before any other notebooks to prevent common issues around dependencies and extension loading. 17 | 18 | ## Installation 19 | 20 | To install for all environments: 21 | 22 | ```bash 23 | pip install 'itkwidgets[all]>=1.0a55' 24 | ``` 25 | 26 | ### Jupyter Notebook 27 | 28 | To install the widgets for the Jupyter Notebook with pip: 29 | 30 | ```bash 31 | pip install 'itkwidgets[notebook]>=1.0a55' 32 | ``` 33 | 34 | Then look for the ImJoy icon at the top in the Jupyter Notebook: 35 | 36 | ![ImJoy Icon in Jupyter Notebook](docs/images/imjoy-notebook.png) 37 | 38 | ### Jupyter Lab 39 | 40 | For Jupyter Lab 3 run: 41 | 42 | ```bash 43 | pip install 'itkwidgets[lab]>=1.0a55' 44 | ``` 45 | 46 | Then look for the ImJoy icon at the top in the Jupyter Notebook: 47 | 48 | ![ImJoy Icon in Jupyter Lab](docs/images/imjoy-lab.png) 49 | 50 | ### Google Colab 51 | 52 | For Google Colab run: 53 | 54 | ```bash 55 | pip install 'itkwidgets>=1.0a55' 56 | ``` 57 | 58 | ## Example Notebooks 59 | 60 | Example Notebooks can be accessed locally by cloning the repository: 61 | 62 | ```bash 63 | git clone -b main https://github.com/InsightSoftwareConsortium/itkwidgets.git 64 | ``` 65 | 66 | Then navigate into the examples directory: 67 | 68 | ```bash 69 | cd itkwidgets/examples 70 | ``` 71 | 72 | ## Usage 73 | 74 | In Jupyter, import the view function: 75 | 76 | ```python 77 | from itkwidgets import view 78 | ``` 79 | 80 | Then, call the view function at the end of a cell, passing in the image to examine: 81 | 82 | ```python 83 | view(image) 84 | ``` 85 | 86 | For information on additional options, see the view function docstring: 87 | 88 | ```python 89 | view? 90 | ``` 91 | 92 | See the [deployments](docs/deployments.md) section for a more detailed overview of additional notebook 93 | options as well as other ways to run and interact with your notebooks. 94 | 95 | # Learn more 96 | 97 | Visit the [docs](https://itkwidgets.readthedocs.io/en/latest/) for more information on supported notebooks and integrations. 98 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | clean: 18 | rm -rf _build 19 | rm -rf jupyterlite/_output 20 | 21 | # Catch-all target: route all unknown targets to Sphinx using the new 22 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /docs/_static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/_static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/_static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/_static/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/_static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /docs/_static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /docs/_static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /docs/_static/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/_static/itkwidgets_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/itkwidgets_logo.png -------------------------------------------------------------------------------- /docs/_static/itkwidgets_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/_static/itkwidgets_logo_small.png -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | ## Returning Values 4 | 5 | Communication with the IPython Kernel is asynchronous, which causes challenges with Python code blocks in Jupyter cells that run synchronously. Multiple comm messages cannot be awaited during the synchronous Python code block execution. With regards to ITKWidgets this means that getter functions do not "just work" - the Python code cannot complete until the comm message has resolved with the response and the message cannot resolve until the code has completed. This creates a deadlock that prevents the kernel from progressing. This has been a [documented issue for the Jupyter ipykernel](https://github.com/ipython/ipykernel/issues/65) for many years. 6 | 7 | Libraries like [ipython_blocking](https://github.com/kafonek/ipython_blocking) and [jupyter-ui-poll](https://github.com/Kirill888/jupyter-ui-poll) have made efforts to address this issue and their approaches have been a great source of inspiration for the custom solution that we have chosen for ITKWidgets. 8 | 9 | Our current solution prevents deadlocks but requires that getters be requested in one cell and resolved in another. For example: 10 | 11 | ```python 12 | viewer = view(image) 13 | ``` 14 | ```python 15 | bg_color = viewer.get_background_color() 16 | print(bg_color) 17 | ``` 18 | would simply become: 19 | 20 | ```python 21 | viewer = view(image) 22 | ``` 23 | ```python 24 | bg_color = viewer.get_background_color() 25 | ``` 26 | ```python 27 | print(bg_color) 28 | ``` 29 | 30 | This has [not yet been applied in 31 | JupyterLite](https://github.com/InsightSoftwareConsortium/itkwidgets/issues/730) 32 | so getters are not yet well-behaved in JupyterLite. 33 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | from pathlib import Path 17 | from sphinx.application import Sphinx 18 | import subprocess 19 | import os 20 | import re 21 | from datetime import date 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = 'itkwidgets' 26 | author = 'Matthew McCormick' 27 | copyright = f'{date.today().year}, NumFOCUS' 28 | author = 'Insight Software Consortium' 29 | 30 | # The full version, including alpha/beta/rc tags. 31 | release = re.sub('^v', '', os.popen('git tag --list "v1.0*" --sort=creatordate').readlines()[-1].strip()) 32 | # The short X.Y version. 33 | version = release 34 | 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autosummary', 43 | 'autodoc2', 44 | 'myst_parser', 45 | 'sphinx_copybutton', 46 | 'sphinx.ext.intersphinx', 47 | 'sphinxext.opengraph', 48 | 'sphinx_design', 49 | ] 50 | 51 | autodoc2_packages = [ 52 | { 53 | "path": "../itkwidgets", 54 | "exclude_files": [], 55 | }, 56 | ] 57 | autodoc2_render_plugin = "myst" 58 | 59 | myst_enable_extensions = [ 60 | "colon_fence", 61 | "dollarmath", # Support syntax for inline and block math using `$...$` and `$$...$$` 62 | # (see https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#dollar-delimited-math) 63 | "fieldlist", 64 | "linkify", # convert bare links to hyperlinks 65 | ] 66 | 67 | intersphinx_mapping = { 68 | "itkwasm": ("https://wasm.itk.org/en/latest/", None), 69 | "python": ("https://docs.python.org/3/", None), 70 | "numpy": ("https://numpy.org/doc/stable", None), 71 | } 72 | 73 | html_theme_options = dict( 74 | github_url='https://github.com/InsightSoftwareConsortium/itkwidgets', 75 | icon_links=[], 76 | ) 77 | 78 | # jupyterlite_config = jupyterlite_dir / "jupyterlite_config.json" 79 | 80 | # Add any paths that contain templates here, relative to this directory. 81 | templates_path = ['_templates'] 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This pattern also affects html_static_path and html_extra_path. 86 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 87 | 88 | 89 | # -- Options for HTML output ------------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | # 94 | html_theme = 'furo' 95 | html_logo = "_static/itkwidgets_logo_small.png" 96 | html_favicon = "_static/favicon/favicon.ico" 97 | html_title = f"{project}'s documentation" 98 | 99 | # Furo options 100 | html_theme_options = { 101 | "top_of_page_button": "edit", 102 | "source_repository": "https://github.com/InsightSoftwareConsortium/itkwidgets/", 103 | "source_branch": "main", 104 | "source_directory": "docs", 105 | } 106 | 107 | 108 | # Add any paths that contain custom static files (such as style sheets) here, 109 | # relative to this directory. They are copied after the builtin static files, 110 | # so a file named "default.css" will overwrite the builtin "default.css". 111 | html_static_path = ['_static', 112 | 'jupyterlite/_output'] 113 | 114 | def jupyterlite_build(app: Sphinx, error): 115 | here = Path(__file__).parent.resolve() 116 | jupyterlite_config = here / "jupyterlite" / "jupyterlite_config.json" 117 | subprocess.check_call(['jupyter', 'lite', 'build', '--config', 118 | str(jupyterlite_config)], cwd=str(here / 'jupyterlite')) 119 | 120 | def setup(app): 121 | # For local builds, you can run jupyter lite build manually 122 | # $ cd jupyterlite 123 | # $ jupyter lite serve --config ./jupyterlite_config.json 124 | app.connect("config-inited", jupyterlite_build) 125 | -------------------------------------------------------------------------------- /docs/deployments.md: -------------------------------------------------------------------------------- 1 | # Deployments 2 | 3 | ## JupyterLite 4 | 5 | 6 | Try it! 7 | 8 | ![Jupyterlite](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg) 9 | 10 | 11 | [JupyterLite](https://jupyterlite.readthedocs.io/en/latest/) is a JupyterLab distribution that runs entirely in the browser built from the ground-up using JupyterLab components and extensions. 12 | 13 | To use itkwidgets in a JupyterLite deployment, install the 14 | [imjoy-jupyterlab-extension](https://pypi.org/project/imjoy-jupyterlab-extension/) 15 | JupyterLab 3 federated extension in the environment used to build JupyterLite. 16 | See also [the JupyterLite configuration used for this 17 | documentation](https://github.com/InsightSoftwareConsortium/itkwidgets/blob/main/docs/jupyterlite/jupyterlite_config.json). 18 | Currently, [this dask-image 19 | wheel](https://github.com/InsightSoftwareConsortium/itkwidgets/blob/main/docs/jupyterlite/pypi/dask_image-2022.9.0-py2.py3-none-any.whl) 20 | should also be added to the *pypi* directory of the jupyterlite configuration. 21 | 22 | In the Pyodide notebook, 23 | 24 | ```python 25 | import piplite 26 | await piplite.install("itkwidgets==1.0a55") 27 | ``` 28 | 29 | See also the [Sphinx / ReadTheDocs 30 | configuration](https://github.com/InsightSoftwareConsortium/itkwidgets/blob/main/docs/conf.py) 31 | used for this documentation. 32 | 33 | ## Colab 34 | 35 | [Google Colab](https://research.google.com/colaboratory/) is a free-to-use hosted Jupyter notebook service that provides 36 | computing resources including GPUs and itkwidgets is now supported in Colab 37 | 38 | Or visit the [welcome page](https://colab.research.google.com/?utm_source=scs-index) to upload your own notebook or create one from scratch. 39 | 40 | ![Upload Notebook in Google Colab](images/colab.png) 41 | 42 | Notebooks can be uploaded from a repository, Google Drive, or your local machine. 43 | 44 | 45 | ## Jupyter Notebook 46 | 47 | To use itkwidgets locally first [install Jupyter Notebook](https://jupyter.org/install#jupyter-notebook) and start Jupyter: 48 | 49 | ```bash 50 | pip install notebook 51 | jupyter notebook 52 | ``` 53 | 54 | If you'd rather interact with remotely hosted notebooks you can also open them 55 | in Binder: [![Open in Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/main?urlpath=%2Fnotebooks%2Fexamples%2F) 56 | 57 | ## JupyterLab 58 | 59 | To use itkwidgets locally first [install JupyterLab](https://jupyter.org/install#jupyterlab) and start Jupyter: 60 | 61 | ```bash 62 | pip install jupyterlab 63 | jupyter lab 64 | ``` 65 | 66 | If you'd rather interact with remotely hosted notebooks in JupyterLab you can 67 | also open them in Binder: [![Open in Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/main?labpath=examples%2F) 68 | 69 | ## Command Line (CLI) 70 | 71 | To enable quick inspection of your 3D data in the browser or in your terminal you can install the command-line tool. 72 | 73 | ```bash 74 | pip install 'itkwidgets[cli]>=1.0a55' 75 | playwright install --with-deps chromium 76 | ``` 77 | Previewing data in the terminal requires support for the iterm2 inline image protocol. Examples of terminals with this support include [wezterm](https://wezfurlong.org/wezterm/), [VSCode's Terminal](https://code.visualstudio.com/updates/v1_80#_image-support) (with VSCode >= v1.80), and [iTerm2](https://iterm2.com/index.html). 78 | 79 | **Note**: If you are using VSCode but are not seeing the images output in the terminal confirm that you are on version `1.80` or later. You may also need to make sure that `Integrated: Gpu Acceleration` is set to `on` rather than `auto`. Find this under `File > Preferences > Settings` and search for `Gpu Acceleration`. 80 | 81 | For basic usage the following flags are most commonly used: 82 | 83 | **Data Input** 84 | * `-i, --image`: The path to an image data file. This flag is optional and the image data can also be passed in as the first argument without a flag. 85 | * `-l, --label-image`: Path to a label image data file 86 | * `-p, --point-set`: Path to a point set data file 87 | * `--reader`: Backend to use to read the data file(s) (choices: "ngff_zarr", "zarr", "itk", "tifffile", "imageio") 88 | 89 | **For use with browser output** 90 | * `-b, --browser`: Render to a browser tab instead of the terminal. 91 | * `--repl`: Start interactive REPL after launching viewer. This allows you to programmatically interact with and update the viewer. 92 | 93 | **For use with terminal or browser output** 94 | * `--verbose`: Print all log messages to stdout. Defaults to supressing log messages. 95 | * `-r, --rotate`: Continuously rotate the camera around the scene in volume rendering mode. 96 | * `-m, --view-mode`: Only relevant for 3D scenes (choices: "x", "y", "z", "v") 97 | 98 | ### Examples 99 | 100 | View the data in the terminal while rotating the camera: 101 | ```bash 102 | itkwidgets path/to/data -r 103 | ``` 104 | ![Image rotating in terminal](images/terminal_rotate.gif) 105 | 106 | View the image data in the browser and then programatically set the label image: 107 | ```bash 108 | itkwidgets -i path/to/data --reader itk --repl 109 | ``` 110 | 111 | ```python 112 | >>> import itk 113 | >>> label = itk.imread("path/to/label_image") 114 | >>> viewer.set_label_image(label) 115 | ``` 116 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Package 4 | 5 | Setup your system for development: 6 | 7 | ```bash 8 | git clone https://github.com/InsightSoftwareConsortium/itkwidgets.git 9 | cd itkwidgets 10 | pip install -e ".[test,lab,notebook,cli]" 11 | pytest 12 | pytest --nbmake examples/*.ipynb 13 | ``` 14 | 15 | If Python code is changed, restart the kernel to see the changes. 16 | 17 | **Warning**: This project is under active development. Its API and behavior may change at any time. We mean it 🙃. 18 | 19 | ## Documentation 20 | 21 | Setup your system for documentation development on Unix-like systems: 22 | 23 | ```bash 24 | git clone https://github.com/InsightSoftwareConsortium/itkwidgets.git 25 | cd itkwidgets/docs 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | Build and serve the documentation: 30 | 31 | ```bash 32 | make html 33 | python -m http.server -d _build/html 8787 34 | ``` 35 | 36 | Then visit *http://localhost:8787/* to see the rendered documentation. 37 | 38 | ### JupyterLite 39 | 40 | The documentation includes an embedded JupyterLite deployment. To update the 41 | JupyterLite deployment, it is recommended to call `make clean` before starting 42 | a new build to avoid build caching issues. Also, serve the rendered 43 | documentation on a different port to avoid browser caching issues. 44 | 45 | Notebooks served in the JupyterLite deployment can be found at 46 | *docs/jupyterlite/files*. 47 | 48 | Support package wheels, including the `itkwidgets` wheel are referenced in 49 | *docs/jupyter/jupyterlite_config.json*. To update the URLs there, copy the 50 | download link address for a wheel found at https://pypi.org in a package's *Download 51 | files* page. Additional wheel files, if not on PyPI, can be added directly at 52 | *docs/jupyterlite/files/*. 53 | -------------------------------------------------------------------------------- /docs/images/Hello3DWorld.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/Hello3DWorld.gif -------------------------------------------------------------------------------- /docs/images/colab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/colab.png -------------------------------------------------------------------------------- /docs/images/dask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/dask.png -------------------------------------------------------------------------------- /docs/images/dask_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/dask_stack.png -------------------------------------------------------------------------------- /docs/images/imjoy-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/imjoy-lab.png -------------------------------------------------------------------------------- /docs/images/imjoy-notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/imjoy-notebook.png -------------------------------------------------------------------------------- /docs/images/itkimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/itkimage.png -------------------------------------------------------------------------------- /docs/images/monai_pytorch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/monai_pytorch.png -------------------------------------------------------------------------------- /docs/images/numpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/numpy.png -------------------------------------------------------------------------------- /docs/images/pyimagej.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/pyimagej.png -------------------------------------------------------------------------------- /docs/images/pyvista.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/pyvista.png -------------------------------------------------------------------------------- /docs/images/terminal_rotate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/terminal_rotate.gif -------------------------------------------------------------------------------- /docs/images/vtkpolydata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/vtkpolydata.png -------------------------------------------------------------------------------- /docs/images/xarray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/xarray.png -------------------------------------------------------------------------------- /docs/images/xarray2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/xarray2.png -------------------------------------------------------------------------------- /docs/images/zarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/images/zarr.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to itkwidgets's documentation! 2 | 3 | ```{note} 4 | This is the **pre-release alpha** documentation for **itkwidgets 1.0**, which 5 | has first-class support for 6 | [JupyterLite](https://jupyterlite.readthedocs.io/en/latest/), 7 | [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/), [Google 8 | Colab](https://research.google.com/colaboratory/), and more. itkwidgets-1.0 is 9 | powered by [itk-wasm](https://wasm.itk.org) and [ImJoy](https://imjoy.io/). 10 | *Welcome to The Future* 🔬🚀🛸! 11 | ``` 12 | 13 | 14 | Try it with JupyterLite! 15 | 16 | ![Jupyterlite](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg) 17 | 18 | ![Hello3DWorld](./images/Hello3DWorld.gif) 19 | 20 | 21 | [![DOI](https://zenodo.org/badge/121581663.svg)](https://zenodo.org/doi/10.5281/zenodo.3603358) 22 | 23 | 24 | ```{toctree} 25 | :maxdepth: 4 26 | :caption: 📖 Learn 27 | 28 | quick_start_guide 29 | deployments 30 | integrations 31 | advanced 32 | ``` 33 | 34 | ```{toctree} 35 | :maxdepth: 3 36 | :caption: 📖 Reference 37 | 38 | apidocs/index.rst 39 | ``` 40 | 41 | ```{toctree} 42 | :maxdepth: 2 43 | :caption: 🔨 Contribute 44 | 45 | development 46 | Code of Conduct 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/integrations.md: -------------------------------------------------------------------------------- 1 | Integrations 2 | ============ 3 | 4 | NumPy 5 | ----- 6 | 7 | Install NumPy: 8 | 9 | ```bash 10 | pip install numpy 11 | ``` 12 | 13 | Or see the [NumPy docs](https://numpy.org/install/) for advanced installation options. 14 | 15 | Use NumPy to build and view your data: 16 | 17 | ```python 18 | import numpy as np 19 | from itkwidgets import view 20 | 21 | number_of_points = 3000 22 | gaussian_mean = [0.0, 0.0, 0.0] 23 | gaussian_cov = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 0.5]] 24 | point_set = np.random.multivariate_normal(gaussian_mean, gaussian_cov, number_of_points) 25 | 26 | view(point_set=point_set) 27 | ``` 28 | 29 | Or check out the [NumPyArrayPointSet](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/NumPyArrayPointSet.ipynb) example notebook to try it out for yourself! 30 | 31 | ![NumPy Array Point Set](images/numpy.png) 32 | 33 | ITK-Wasm 34 | -------- 35 | 36 | [ITK-Wasm](https://wasm.itk.org) combines ITK and [WebAssembly](https://webassembly.org/) to enable high-performance spatial analysis in a web browser or system-level environments and reproducible execution across programming languages and hardware architectures. 37 | 38 | In Python, ITK-Wasm works with [simple, Pythonic data 39 | structures](https://wasm.itk.org/en/latest/python/numpy.html) comprised of 40 | Python dictionaries, lists, and NumPy arrays. 41 | 42 | Install an ITK-Wasm package. For example: 43 | 44 | ```bash 45 | pip install itkwasm-image-io 46 | ``` 47 | 48 | You can use ITK-Wasm to read in and filter your data before displaying and interacting with it with the Viewer. 49 | 50 | ```python 51 | import os 52 | from itkwasm_image_io import imread 53 | from itkwidgets import view 54 | from urllib.request import urlretrieve 55 | 56 | # Download data 57 | file_name = '005_32months_T2_RegT1_Reg2Atlas_ManualBrainMask_Stripped.nrrd' 58 | if not os.path.exists(file_name): 59 | url = 'https://data.kitware.com/api/v1/file/564a5b078d777f7522dbfaa6/download' 60 | urlretrieve(url, file_name) 61 | 62 | image = imread(file_name) 63 | view(image, rotate=True, gradient_opacity=0.4) 64 | ``` 65 | 66 | Get started with ITK in the [3DImage](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/itkwasm/3DImage.ipynb) notebook. You can also visit the [ITK-Wasm docs](https://wasm.itk.org/en/latest/python/introduction.html) for more information and see the [ITK-Wasm packages](https://wasm.itk.org/en/latest/introduction/packages.html) for additional examples. 67 | 68 | ![ITK 3D Image](images/itkimage.png) 69 | 70 | ITK 71 | --- 72 | 73 | Install ITK: 74 | 75 | ```bash 76 | pip install itk-io 77 | ``` 78 | 79 | You can use ITK to read in and filter your data before displaying and interacting with it with the Viewer. 80 | 81 | ```python 82 | import os 83 | import itk 84 | from itkwidgets import view 85 | from urllib.request import urlretrieve 86 | 87 | # Download data 88 | file_name = '005_32months_T2_RegT1_Reg2Atlas_ManualBrainMask_Stripped.nrrd' 89 | if not os.path.exists(file_name): 90 | url = 'https://data.kitware.com/api/v1/file/564a5b078d777f7522dbfaa6/download' 91 | urlretrieve(url, file_name) 92 | 93 | image = itk.imread(file_name) 94 | view(image, rotate=True, gradient_opacity=0.4) 95 | ``` 96 | 97 | Get started with ITK in the [3DImage](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/itk/3DImage.ipynb) notebook. You can also visit the [ITK docs](https://docs.itk.org/en/latest/learn/python_quick_start.html) for additional examples for getting started. 98 | 99 | ![ITK 3D Image](images/itkimage.png) 100 | 101 | VTK 102 | --- 103 | 104 | Install VTK: 105 | 106 | ```bash 107 | pip install vtk 108 | ``` 109 | 110 | You can build you own VTK data or read in a file to pass to the Viewer. 111 | 112 | ```python 113 | import os 114 | import vtk 115 | from itkwidgets import view 116 | from urllib.request import urlretrieve 117 | 118 | # Download data 119 | file_name = 'vase.vti' 120 | if not os.path.exists(file_name): 121 | url = 'https://data.kitware.com/api/v1/file/5a826bdc8d777f0685782960/download' 122 | urlretrieve(url, file_name) 123 | 124 | reader = vtk.vtkXMLImageDataReader() 125 | reader.SetFileName(file_name) 126 | reader.Update() 127 | vtk_image = reader.GetOutput() 128 | 129 | viewer = view(vtk_image) 130 | ``` 131 | 132 | Please be sure to check out the extensive list of [Python VTK examples](https://kitware.github.io/vtk-examples/site/Python/) that are available for the majority of the available VTK classes, or jump right in with the [vtkImageData](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/vtk/vtkImageData.ipynb) or [vtkPolyDataPointSet](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/vtk/vtkPolyDataPointSet.ipynb) example notebooks. 133 | 134 | ![vtkPolyData as a Point Set](images/vtkpolydata.png) 135 | 136 | MONAI 137 | ----- 138 | 139 | MONAI is a PyTorch-based, open-source framework for deep learning in healthcare imaging. Get started by installing MONAI: 140 | 141 | ```bash 142 | pip install monai 143 | ``` 144 | 145 | By default only the minimal requirements are installed. The extras syntax can be used to install optional dependencies. For example, 146 | 147 | ```bash 148 | pip install 'monai[nibabel, skimage]' 149 | ``` 150 | 151 | For a full list of available options visit the [MONAI docs](https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies). 152 | 153 | Check out the [transform_visualization](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/MONAI/transform_visualization.ipynb) notebook for an example of visualize PyTorch tensors. 154 | 155 | ![MONAI transformed tensor](images/monai_pytorch.png) 156 | 157 | dask 158 | ---- 159 | 160 | Dask offers options for installation so that you include only as much or little as you need: 161 | 162 | ```bash 163 | pip install "dask[complete]" # Install everything 164 | pip install dask # Install only core parts of dask 165 | pip install "dask[array]" # Install requirements for dask array 166 | pip install "dask[dataframe]" # Install requirements for dask dataframe 167 | ``` 168 | 169 | See the [full documentation](https://docs.dask.org/en/stable/install.html#dask-installation) for additional dependency sets and installation options. 170 | 171 | You can read in and visualize a dask array in just a few lines of code: 172 | 173 | ```python 174 | import os 175 | import zipfile 176 | import dask.array.image 177 | from itkwidgets import view 178 | from urllib.request import urlretrieve 179 | 180 | # Download data 181 | file_name = 'emdata_janelia_822252.zip' 182 | if not os.path.exists(file_name): 183 | url = 'https://data.kitware.com/api/v1/file/5bf232498d777f2179b18acc/download' 184 | urlretrieve(url, file_name) 185 | with zipfile.ZipFile(file_name, 'r') as zip_ref: 186 | zip_ref.extractall() 187 | 188 | stack = dask.array.image.imread('emdata_janelia_822252/*') 189 | 190 | view(stack, shadow=False, gradient_opacity=0.4, ui_collapsed=True) 191 | ``` 192 | 193 | Try it yourself in the [DaskArray](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/dask/DaskArray.ipynb) notebook. 194 | 195 | ![Dask stack](images/dask_stack.png) 196 | ![Dask data](images/dask.png) 197 | 198 | xarray 199 | ------ 200 | 201 | Xarray uses labels (dimensions, coordinates and attributes) on top of raw data to provide a powerful, concise interface with operations like 202 | 203 | ```python 204 | x.sum('time') 205 | ``` 206 | 207 | Xarray has a few required dependencies that must be installed as well: 208 | 209 | ```bash 210 | pip install numpy # 1.18 or later 211 | pip install packaging # 20.0 or later 212 | pip install pandas # 1.1 or later 213 | pip install xarray 214 | ``` 215 | 216 | Build your own xarray DataArray or Dataset or check out [xarray-data](https://github.com/pydata/xarray-data) for sample data to visualize. 217 | 218 | ```python 219 | import numpy as np 220 | import xarray as xr 221 | from itkwidgets import view 222 | 223 | ds = xr.tutorial.open_dataset("ROMS_example.nc", chunks={"ocean_time": 1}) 224 | 225 | view(ds.zeta, ui_collapsed=False, cmap="Asymmtrical Earth Tones (6_21b)", sample_distance=0) 226 | ``` 227 | 228 | ![xarray ROMS example data](images/xarray.png) 229 | 230 | The [DataArray](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/xarray/DataArray.ipynb) notebook provides an example using the ROMS_example provided by xarray-data. 231 | 232 | ![xarray ROMS example data](images/xarray2.png) 233 | 234 | PyVista 235 | ------- 236 | 237 | PyVista is Pythonic VTK, providing mesh data structures and filtering methods for spatial datasets and is easy to install and get started with: 238 | 239 | ```bash 240 | pip install pyvista 241 | ``` 242 | 243 | The [Core API](https://docs.pyvista.org/api/core/index.html) provides an overview of the supported data types and the [examples](https://docs.pyvista.org/api/examples/_autosummary/pyvista.examples.examples.html#module-pyvista.examples.examples) module provides a nice selection of sample data that you can use 244 | to get started. 245 | 246 | The [UniformGrid](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/PyVista/UniformGrid.ipynb) and [LiDAR](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/PyVista/LiDAR.ipynb) notebooks demonstrate PyVista data being visualized with the Viewer. 247 | 248 | ![PyVista LiDAR point set](images/pyvista.png) 249 | 250 | PyImageJ 251 | -------- 252 | 253 | PyImageJ provides a set of wrapper functions for integration between ImageJ2 and Python and the simplest way to install PyImageJ is with Conda because if you use pip you will need to manage the OpenJDK and Maven dependencies separately. See the [Conda docs](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html) for installation on your system or follow 254 | PyImageJ's suggestion of using Mamba ([install Mambaforge](https://github.com/conda-forge/miniforge#mambaforge)). 255 | 256 | ```bash 257 | mamba create -n pyimagej pyimagej openjdk=8 258 | ``` 259 | 260 | For more detatiled installation instructions and alternativate options like pip, see the [PyImageJ installation docs](https://github.com/imagej/pyimagej/blob/master/doc/Install.md). 261 | 262 | 263 | Run the ImageJImgLib2 notebook to see how we can load images and apply filters before viewing them in the Viewer. 264 | 265 | ![PyImageJ Filtered blood vessels image](images/pyimagej.png) 266 | 267 | Zarr 268 | ---- 269 | 270 | Zarr is a format for the storage of chunked, compressed, N-dimensional arrays that supports chunking arrays along any dimension, reading or writing arrays concurrently from multiple threads or processes, as well as organizing arrays into hierarchies via groups. 271 | 272 | To install Zarr: 273 | 274 | ```bash 275 | pip install zarr 276 | ``` 277 | 278 | You can use Zarr to read data stored locally or on S3, as we do in the [OME-NGFF-Brainstem-MRI](https://colab.research.google.com/github/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/zarr/OME-NGFF-Brainstem-MRI.ipynb) example notebook. 279 | 280 | ```python 281 | from zarr.storage import FSStore 282 | 283 | fsstore = FSStore('https://dandiarchive.s3.amazonaws.com/zarr/7723d02f-1f71-4553-a7b0-47bda1ae8b42') 284 | brainstem = zarr.open_group(fsstore, mode='r') 285 | 286 | view(brainstem) 287 | ``` 288 | 289 | ![Brainstem image from zarr](images/zarr.png) 290 | -------------------------------------------------------------------------------- /docs/jupyterlite/jupyterlite_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "PipliteAddon": { 3 | "piplite_urls": [ 4 | "https://files.pythonhosted.org/packages/0e/9b/2987677aa4dfadd8ad2636f2e90c3b1e527cddb8b0789fa6151f47f832fa/itkwasm-1.0b145-py3-none-any.whl", 5 | "https://files.pythonhosted.org/packages/80/48/1232756c08ecdc3305fecfec40ef13ac7ea2dd94a6203806595a696cc110/imjoy_rpc-0.5.46-py3-none-any.whl" 6 | "https://files.pythonhosted.org/packages/69/d9/5a6c8af2f4b4f49a809ae316ae4c12937d7dfda4e5b2f9e4167df5f15c0e/imjoy_utils-0.1.2-py3-none-any.whl", 7 | "https://files.pythonhosted.org/packages/ba/9f/ec8509396a847a34dfba58e7cf5e96750242eda8f7f45cdfe5c04013f204/itkwidgets-1.0a46-py3-none-any.whl", 8 | "https://files.pythonhosted.org/packages/b4/ba/2f2cec7283edd5666f9ac912eacc03ab62df3ae3c83b3718db5cd040dd7a/ngff_zarr-0.6.0-py3-none-any.whl" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/jupyterlite/pypi/dask_image-2022.9.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/docs/jupyterlite/pypi/dask_image-2022.9.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/quick_start_guide.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | ## Environment Setup 4 | 5 | The [EnvironmentCheck.ipynb](https://github.com/InsightSoftwareConsortium/itkwidgets/blob/main/examples/EnvironmentCheck.ipynb) checks the environment that you are running in to make sure that all required dependencies and extensions are correctly installed. Ideally run first before any other notebooks to prevent common issues around dependencies and extension loading. 6 | 7 | ## Installation 8 | 9 | To install for all environments: 10 | 11 | ```bash 12 | pip install 'itkwidgets[all]>=1.0a55' 13 | ``` 14 | 15 | ### Jupyter Notebook 16 | 17 | To install the widgets for the Jupyter Notebook with pip: 18 | 19 | ```bash 20 | pip install 'itkwidgets[notebook]>=1.0a55' 21 | ``` 22 | 23 | Then look for the ImJoy icon at the top in the Jupyter Notebook: 24 | 25 | ![ImJoy Icon in Jupyter Notebook](images/imjoy-notebook.png) 26 | 27 | ### Jupyter Lab 28 | 29 | For Jupyter Lab 3 run: 30 | 31 | ```bash 32 | pip install 'itkwidgets[lab]>=1.0a55' 33 | ``` 34 | 35 | Then look for the ImJoy icon at the top in the Jupyter Notebook: 36 | 37 | ![ImJoy Icon in Jupyter Lab](images/imjoy-lab.png) 38 | 39 | ### Google Colab 40 | 41 | For Google Colab run: 42 | 43 | ```bash 44 | pip install 'itkwidgets>=1.0a55' 45 | ``` 46 | 47 | ### Command Line (CLI) 48 | 49 | ```bash 50 | pip install 'itkwidgets[cli]>=1.0a55' 51 | playwright install --with-deps chromium 52 | ``` 53 | 54 | ## Example Notebooks 55 | 56 | Example Notebooks can be accessed locally by cloning the repository: 57 | 58 | ```bash 59 | git clone -b main https://github.com/InsightSoftwareConsortium/itkwidgets.git 60 | ``` 61 | 62 | Then navigate into the examples directory: 63 | 64 | ```bash 65 | cd itkwidgets/examples 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Notebook 71 | 72 | In Jupyter, import the {py:obj}`view ` function: 73 | 74 | ```python 75 | from itkwidgets import view 76 | ``` 77 | 78 | Then, call the {py:obj}`view ` function at the end of a cell, passing in the image to examine: 79 | 80 | ```python 81 | view(image) 82 | ``` 83 | 84 | For information on additional options, see the {py:obj}`view ` function docstring: 85 | 86 | ```python 87 | view? 88 | ``` 89 | 90 | ### CLI 91 | 92 | ```bash 93 | itkwidgets path/to/image -b # open viewer in browser -OR- 94 | 95 | itkwidgets path/to/image # display preview in terminal 96 | ``` 97 | 98 | For information on additional options, see the help: 99 | 100 | ```bash 101 | itkwidgets --help 102 | ``` 103 | 104 | See the [deployments](deployments.md) section for a more detailed overview of additional options as well as other ways to run and interact with the itkwidgets. 105 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo 2 | imjoy_jupyterlab_extension 3 | jupyterlite[all]==0.1.0b17 4 | myst-parser[linkify] 5 | sphinx-autodoc2>=0.5.0 6 | sphinx-copybutton 7 | sphinx-design 8 | sphinxext-opengraph 9 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: itkwidgets 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.10 6 | - pip 7 | - nodejs 8 | - pyimagej 9 | - pip: 10 | - itk>=5.3.0 11 | - itkwidgets[all]>=1.0a55 12 | - imjoy-elfinder 13 | - imjoy-jupyter-extension 14 | - imjoy-jupyterlab-extension 15 | - monai[nibabel, matplotlib, tqdm] 16 | - imageio 17 | - pyvista 18 | - dask[diagnostics] 19 | - toolz 20 | - scikit-image 21 | - pooch 22 | - matplotlib 23 | - tqdm 24 | - vtk 25 | - netCDF4 26 | - xarray 27 | - zarr 28 | - fsspec[http] 29 | -------------------------------------------------------------------------------- /examples/EnvironmentCheck.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "61d4e4b4-80ae-49f9-b08a-354e8d92cc78", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is intended to be downloaded and run locally, or run in cloud environments with persistent environments, like Sagemaker Studio Lab:\n", 9 | "\n", 10 | "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/HEAD?labpath=examples%2FEnvironmentCheck.ipynb)\n", 11 | "[![Open In SageMaker Studio Lab](https://studiolab.sagemaker.aws/studiolab.svg)](https://studiolab.sagemaker.aws/import/github.com/InsightSoftwareConsortium/itkwidgets/blob/main/examples/EnvironmentCheck.ipynb)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "58193a82-4e2d-4f04-8929-e2566b1b13c2", 17 | "metadata": {}, 18 | "source": [ 19 | "# Environment Check\n", 20 | "\n", 21 | "#### This notebook is designed to check the environment that you are running in to make sure that all example notebook dependencies and extensions are correctly installed. Simply select Run All Cells and let everything complete before running the example notebooks in this repository." 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "36eaad49-3b68-4806-8ba6-56eb0384d1da", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import os, sys, re\n", 32 | "import importlib.util\n", 33 | "try:\n", 34 | " import importlib.metadata as importlib_metadata\n", 35 | "except:\n", 36 | " import importlib_metadata" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "7e09f10e-535d-4031-a092-96063969dc9c", 42 | "metadata": {}, 43 | "source": [ 44 | "#### Define the function to do the checking and install any missing dependencies" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "id": "8b49c580-719a-4229-8785-4037162426d7", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "def _get_version(pkg):\n", 55 | " try:\n", 56 | " return importlib_metadata.version(pkg)\n", 57 | " except:\n", 58 | " pass\n", 59 | " try:\n", 60 | " return sys.modules[pkg].__version__\n", 61 | " except:\n", 62 | " return ''\n", 63 | "\n", 64 | "def _pkg_version_pre(values):\n", 65 | " version, pre = None, False\n", 66 | " if len(values) == 3:\n", 67 | " version, pre = values[1:]\n", 68 | " elif len(values) == 2:\n", 69 | " pre = (values[1] == \"pre\")\n", 70 | " version = values[1] if not pre else version\n", 71 | " pkg = values[0]\n", 72 | "\n", 73 | " return pkg, version, pre\n", 74 | "\n", 75 | "def check_for_package(req):\n", 76 | " values = list(filter(None, re.split(r\"\\[.*\\]|==|>=|--| \", req))) # Grab the package name, version, and pre-release status\n", 77 | " install_req = re.split(r\" --pre\", req)[0] # Grab the string we need for installation\n", 78 | " pkg, version, pre = _pkg_version_pre(values)\n", 79 | " if (importlib.util.find_spec(pkg.replace(\"-\", \"_\")) is None\n", 80 | " or (version and _get_version(pkg) != version)):\n", 81 | " print(f\"{install_req} not found, installing {pkg} now...\")\n", 82 | " try:\n", 83 | " if pre:\n", 84 | " !{sys.executable} -m pip install --upgrade --pre -q \"{install_req}\"\n", 85 | " else:\n", 86 | " !{sys.executable} -m pip install --upgrade -q \"{install_req}\"\n", 87 | " except Exception as e:\n", 88 | " print(f'ERROR: {e}')\n", 89 | " print(f\"{pkg} version {_get_version(pkg)} installed.\")\n", 90 | " print(\"-----\")" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "id": "871e8e65-5386-4c62-8ec3-bc4d80fada1a", 96 | "metadata": {}, 97 | "source": [ 98 | "#### List of notebook requirements" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "id": "ba6f3698-5af6-4b1f-b433-3804cde13494", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "reqs = [\n", 109 | " \"itkwidgets[all]>=1.0a55\",\n", 110 | " \"imjoy-elfinder\",\n", 111 | " \"imjoy-jupyter-extension\",\n", 112 | " \"imjoy-jupyterlab-extension\",\n", 113 | " \"itk\",\n", 114 | " \"monai[nibabel, matplotlib, tqdm]\",\n", 115 | " \"imageio\",\n", 116 | " \"pyvista\",\n", 117 | " \"dask[diagnostics]\",\n", 118 | " \"toolz\",\n", 119 | " \"scikit-image\",\n", 120 | " \"pooch\",\n", 121 | " \"matplotlib\",\n", 122 | " \"tqdm\",\n", 123 | " \"vtk\",\n", 124 | " \"netCDF4\",\n", 125 | " \"xarray\",\n", 126 | " \"zarr\",\n", 127 | " \"fsspec[http]\",\n", 128 | "]" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "id": "63fb64d6-52bc-41a4-a7a6-6f9cf04ef0e1", 134 | "metadata": {}, 135 | "source": [ 136 | "#### Upgrade pip, just in case." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "id": "45d3f0e8-bf13-4c98-ac43-66901a9a704d", 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "!{sys.executable} -m pip install --upgrade -q pip" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "id": "ff0514d1-53ca-4ff6-9fe4-142947a6aa86", 152 | "metadata": {}, 153 | "source": [ 154 | "#### Make sure that the package is installed and that it is the correct version." 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "id": "040c7e52-7e66-4063-838a-8eed90ac3be9", 160 | "metadata": {}, 161 | "source": [ 162 | "**WARNING**: Pip will sometimes raise errors for dependency conflicts. This errors can typically be safely ignored, but often times these issues can be avoided all together by creating a new, clean [python virtual environment](https://docs.python.org/3/library/venv.html) or [conda environment](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html#managing-environments). You can follow the [Getting Started](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html#) instructions if you are setting up conda for the fist time. If you continue to see errors or are unable to run the notebooks in this repo after running this notebook you can also [open an issue](https://github.com/InsightSoftwareConsortium/itkwidgets/issues/new)." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "id": "3e18e98c-575b-405e-bc49-1a7035882aa1", 169 | "metadata": { 170 | "tags": [] 171 | }, 172 | "outputs": [], 173 | "source": [ 174 | "for req in reqs:\n", 175 | " check_for_package(req)" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "id": "44f4cd3b-b26f-4d0e-988b-9f9e1b9fac67", 182 | "metadata": { 183 | "tags": [ 184 | "raises-exception" 185 | ] 186 | }, 187 | "outputs": [], 188 | "source": [ 189 | "if os.environ.get('CONDA_DEFAULT_ENV', None):\n", 190 | " !conda install --yes -q --prefix {sys.prefix} -c conda-forge pyimagej\n", 191 | "else:\n", 192 | " raise RuntimeError(\"No conda environment is activated, currently unable to install pyimagej. Please activate a conda environment and re-run this cell.\")" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "id": "9218a450-0518-4ddb-8fe4-4b3d31bbb50c", 198 | "metadata": {}, 199 | "source": [ 200 | "#### Special case specific to running in AWS StudioLab" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "id": "556bdaee-a741-4e17-a274-b8f8e1122f44", 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "if \"studio-lab-user\" in os.getcwd():\n", 211 | " # Make sure that the imjoy extension is installed in the Jupyter environment\n", 212 | " # and not just the kernel environment since they may not be the same\n", 213 | " !conda env update -n studiolab -f ../environment.yml\n", 214 | " !conda install --yes -q --prefix {sys.prefix} -c conda-forge opencv nodejs" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "id": "79cacab0-da6c-47f2-b007-d2a1c9305b7f", 220 | "metadata": {}, 221 | "source": [ 222 | "#### Make sure that the required extension(s) are loaded." 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "id": "00ad1eed-5fab-4076-8278-cdd393d2634f", 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "%%javascript\n", 233 | "let needReload = (typeof window.loadImJoyRPC === \"undefined\");\n", 234 | "if (needReload) {\n", 235 | " needReload = false;\n", 236 | " location.reload();\n", 237 | "}" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "id": "d5c99d93-c4b9-4e29-a1df-d116e18c869d", 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [] 247 | } 248 | ], 249 | "metadata": { 250 | "kernelspec": { 251 | "display_name": "Python 3 (ipykernel)", 252 | "language": "python", 253 | "name": "python3" 254 | }, 255 | "language_info": { 256 | "codemirror_mode": { 257 | "name": "ipython", 258 | "version": 3 259 | }, 260 | "file_extension": ".py", 261 | "mimetype": "text/x-python", 262 | "name": "python", 263 | "nbconvert_exporter": "python", 264 | "pygments_lexer": "ipython3", 265 | "version": "3.9.12" 266 | } 267 | }, 268 | "nbformat": 4, 269 | "nbformat_minor": 5 270 | } 271 | -------------------------------------------------------------------------------- /examples/integrations/PyImageJ/ImageJImgLib2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0fa4d687-14f1-44ed-9306-dd1bd7cdbe2e", 6 | "metadata": {}, 7 | "source": [ 8 | "# ImageJ, Python, and itkwidgets\n", 9 | "\n", 10 | "### Try this notebook in Binder or SageMaker!\n", 11 | "\n", 12 | "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/HEAD?labpath=examples%2Fintegrations%2FPyImageJ%2FImageJImgLib2.ipynb)\n", 13 | "[![Open In SageMaker Studio Lab](https://studiolab.sagemaker.aws/studiolab.svg)](https://studiolab.sagemaker.aws/import/github.com/InsightSoftwareConsortium/itkwidgets/blob/main/examples/integrations/PyImageJ/ImageJImgLib2.ipynb)\n", 14 | "\n", 15 | "This example demonstrates how to use ImageJ from CPython and how it can be used with itkwidgets.\n", 16 | "\n", 17 | "To run this example, use the conda cross-platform package manager and install the pyimagej package from conda-forge.\n", 18 | "```\n", 19 | "conda install -c conda-forge pyimagej itk\n", 20 | "```" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "1f72f7cd-87b1-42b2-bf17-c5559a0c7c7a", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "# Install dependencies for this example\n", 31 | "import sys\n", 32 | "\n", 33 | "!conda install --yes --prefix {sys.prefix} -c conda-forge pyimagej\n", 34 | "!{sys.executable} -m pip install -q \"itkwidgets[all]>=1.0a55\"" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "id": "6d179820-c655-46fe-ad21-da1166907547", 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "from urllib.request import urlretrieve\n", 45 | "import os\n", 46 | "\n", 47 | "import itk\n", 48 | "import imagej\n", 49 | "import numpy as np\n", 50 | "\n", 51 | "from itkwidgets import view" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "id": "9bfbf59b-59d4-46eb-84d0-344c54177391", 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "# Initialize imagej\n", 62 | "ij = imagej.init()\n", 63 | "print(ij.getVersion())" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "dde35ec8-e294-412a-b809-3f569b6d087c", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "# Download data\n", 74 | "file_name = 'General_EduRes_Heart_BloodVessels_0.jpg'\n", 75 | "if not os.path.exists(file_name):\n", 76 | " url = 'https://data.kitware.com/api/v1/file/5afe74408d777f15ebe1d701/download'\n", 77 | " urlretrieve(url, file_name)\n", 78 | "image = itk.imread(file_name, itk.ctype('float'))" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "6c58b5a6-2d3d-4511-af76-e9b1432fee21", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "view(image)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "id": "48b151f6-cfc4-4522-aa02-04c8301d3667", 95 | "metadata": { "tags": ["skip-execution"] }, 96 | "outputs": [], 97 | "source": [ 98 | "print(type(image))\n", 99 | "\n", 100 | "image_arr = itk.array_view_from_image(image)\n", 101 | "print(type(image_arr))\n", 102 | "\n", 103 | "image_java = ij.py.to_java(image_arr)\n", 104 | "print(type(image_java))" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "id": "b4db443d-f430-484b-a738-11b493944d8e", 111 | "metadata": { "tags": ["skip-execution"] }, 112 | "outputs": [], 113 | "source": [ 114 | "# Invoke the Frangi vesselness op.\n", 115 | "vessels = np.zeros(image_arr.shape, dtype=np.float32)\n", 116 | "ij.op().filter().frangiVesselness(ij.py.to_java(vessels),\n", 117 | " image_java,\n", 118 | " [1, 1],\n", 119 | " 20)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "id": "1afc55a1-35d3-475d-83fa-9ec04fb6fa28", 126 | "metadata": { "tags": ["skip-execution"] }, 127 | "outputs": [], 128 | "source": [ 129 | "view(vessels)" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "id": "cbc436a7-5928-4017-b5d4-07f3ca21f3a0", 136 | "metadata": { "tags": ["skip-execution"] }, 137 | "outputs": [], 138 | "source": [] 139 | } 140 | ], 141 | "metadata": { 142 | "kernelspec": { 143 | "display_name": "Python 3 (ipykernel)", 144 | "language": "python", 145 | "name": "python3" 146 | }, 147 | "language_info": { 148 | "codemirror_mode": { 149 | "name": "ipython", 150 | "version": 3 151 | }, 152 | "file_extension": ".py", 153 | "mimetype": "text/x-python", 154 | "name": "python", 155 | "nbconvert_exporter": "python", 156 | "pygments_lexer": "ipython3", 157 | "version": "3.10.5" 158 | } 159 | }, 160 | "nbformat": 4, 161 | "nbformat_minor": 5 162 | } 163 | -------------------------------------------------------------------------------- /examples/integrations/itk/select_roi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/examples/integrations/itk/select_roi.gif -------------------------------------------------------------------------------- /itkwidgets/__init__.py: -------------------------------------------------------------------------------- 1 | """itkwidgets: an elegant Python interface for visualization on the web platform 2 | to interactively generate insights into multidimensional images, point sets, and geometry.""" 3 | from .integrations.environment import ENVIRONMENT, Env 4 | 5 | if ENVIRONMENT is not Env.HYPHA: 6 | from imjoy_rpc import register_default_codecs 7 | register_default_codecs() 8 | 9 | from .imjoy import register_itkwasm_imjoy_codecs 10 | register_itkwasm_imjoy_codecs() 11 | 12 | from .viewer import Viewer, view, compare_images 13 | from .standalone_server import standalone_viewer 14 | 15 | __all__ = [ 16 | "Viewer", 17 | "view", 18 | "compare_images", 19 | "standalone_viewer", 20 | ] 21 | -------------------------------------------------------------------------------- /itkwidgets/_initialization_params.py: -------------------------------------------------------------------------------- 1 | import os 2 | from itkwidgets.integrations import _detect_render_type, _get_viewer_image, _get_viewer_point_set 3 | from itkwidgets.render_types import RenderType 4 | from itkwidgets.viewer_config import MUI_HREF, PYDATA_SPHINX_HREF 5 | 6 | 7 | DATA_OPTIONS = ["image", "label_image", "point_set", "data", "fixed_image"] 8 | INPUT_OPTIONS = [*DATA_OPTIONS, "compare"] 9 | 10 | def init_params_dict(itk_viewer): 11 | return { 12 | 'annotations': itk_viewer.setAnnotationsEnabled, 13 | 'axes': itk_viewer.setAxesEnabled, 14 | 'bg_color': itk_viewer.setBackgroundColor, 15 | 'blend_mode': itk_viewer.setImageBlendMode, 16 | 'cmap': itk_viewer.setImageColorMap, 17 | 'color_range': itk_viewer.setImageColorRange, 18 | 'vmin': itk_viewer.setImageColorRangeMin, 19 | 'vmax': itk_viewer.setImageColorRangeMax, 20 | 'color_bounds': itk_viewer.setImageColorRangeBounds, 21 | 'component_visible': itk_viewer.setImageComponentVisibility, 22 | 'gradient_opacity': itk_viewer.setImageGradientOpacity, 23 | 'gradient_opacity_scale': itk_viewer.setImageGradientOpacityScale, 24 | 'interpolation': itk_viewer.setImageInterpolationEnabled, 25 | 'transfer_function': itk_viewer.setImagePiecewiseFunctionPoints, 26 | 'shadow_enabled': itk_viewer.setImageShadowEnabled, 27 | 'sample_distance': itk_viewer.setImageVolumeSampleDistance, 28 | 'label_blend': itk_viewer.setLabelImageBlend, 29 | 'label_names': itk_viewer.setLabelImageLabelNames, 30 | 'label_lut': itk_viewer.setLabelImageLookupTable, 31 | 'label_weights': itk_viewer.setLabelImageWeights, 32 | 'layer': itk_viewer.selectLayer, 33 | 'layer_visible': itk_viewer.setLayerVisibility, 34 | 'container_style': itk_viewer.setRenderingViewContainerStyle, 35 | 'rotate': itk_viewer.setRotateEnabled, 36 | 'ui_collapsed': itk_viewer.setUICollapsed, 37 | 'units': itk_viewer.setUnits, 38 | 'view_mode': itk_viewer.setViewMode, 39 | 'x_slice': itk_viewer.setXSlice, 40 | 'y_slice': itk_viewer.setYSlice, 41 | 'z_slice': itk_viewer.setZSlice, 42 | } 43 | 44 | 45 | def build_config(ui=None): 46 | if ui == "pydata-sphinx": 47 | config = { 48 | "uiMachineOptions": { 49 | "href": PYDATA_SPHINX_HREF, 50 | "export": "default", 51 | } 52 | } 53 | elif ui == "mui": 54 | config = { 55 | "uiMachineOptions": { 56 | "href": MUI_HREF, 57 | "export": "default", 58 | } 59 | } 60 | elif ui != "reference": 61 | config = ui 62 | else: 63 | config = {} 64 | config['maxConcurrency'] = os.cpu_count() * 2 65 | 66 | return config 67 | 68 | 69 | def parse_input_data(init_data_kwargs): 70 | inputs = {} 71 | for option in INPUT_OPTIONS: 72 | data = init_data_kwargs.get(option, None) 73 | if data is not None: 74 | inputs[option] = data 75 | return inputs 76 | 77 | 78 | def build_init_data(input_data, stores): 79 | result= None 80 | for input_type in DATA_OPTIONS: 81 | data = input_data.pop(input_type, None) 82 | if data is None: 83 | continue 84 | render_type = _detect_render_type(data, input_type) 85 | if render_type is RenderType.IMAGE: 86 | if input_type == 'label_image': 87 | result = _get_viewer_image(data, label=True) 88 | stores['LabelImage'] = result 89 | render_type = RenderType.LABELIMAGE 90 | elif input_type == 'fixed_image': 91 | result = _get_viewer_image(data) 92 | stores['Fixed'] = result 93 | render_type = RenderType.FIXEDIMAGE 94 | else: 95 | result = _get_viewer_image(data, label=False) 96 | stores['Image'] = result 97 | elif render_type is RenderType.POINT_SET: 98 | result = _get_viewer_point_set(data) 99 | if result is None: 100 | raise RuntimeError(f"Could not process the viewer {input_type}") 101 | input_data[render_type.value] = result 102 | return input_data 103 | 104 | 105 | def defer_for_data_render(init_data): 106 | deferred_keys = ['image', 'labelImage', 'fixedImage'] 107 | return any([k in init_data.keys() for k in deferred_keys]) 108 | -------------------------------------------------------------------------------- /itkwidgets/_method_types.py: -------------------------------------------------------------------------------- 1 | def deferred_methods(): 2 | return [ 3 | 'setAxesEnabledMode', 4 | 'setImageBlendMode', 5 | 'setImageColorRange', 6 | 'setImageColorRangeBounds', 7 | 'setImageComponentVisibility', 8 | 'setImageGradientOpacity', 9 | 'setImageGradientOpacityScale', 10 | 'setImageInterpolationEnabled', 11 | 'setImagePiecewiseFunctionPoints', 12 | 'setImageVolumeSampleDistance', 13 | 'setImageVolumeScatteringBlend', 14 | 'setLabelImageBlend', 15 | 'setLabelImageLabelNames', 16 | 'setLabelImageLookupTable', 17 | 'setLabelImageWeights', 18 | 'selectLayer', 19 | 'setLayerVisibility', 20 | 'setUnits', 21 | 'setViewMode', 22 | 'setXSlice', 23 | 'setYSlice', 24 | 'setZSlice', 25 | 'getAxesEnabledMode', 26 | 'getImageBlendMode', 27 | 'getImageColorRange', 28 | 'getImageColorRangeBounds', 29 | 'getImageComponentVisibility', 30 | 'getImageGradientOpacity', 31 | 'getImageGradientOpacityScale', 32 | 'getImageInterpolationEnabled', 33 | 'getImagePiecewiseFunctionPoints', 34 | 'getImageVolumeSampleDistance', 35 | 'getImageVolumeScatteringBlend', 36 | 'getLabelImageBlend', 37 | 'getLabelImageLabelNames', 38 | 'getLabelImageLookupTable', 39 | 'getLabelImageWeights', 40 | 'selectLayer', 41 | 'getLayerVisibility', 42 | 'getUnits', 43 | 'getViewMode', 44 | 'getXSlice', 45 | 'getYSlice', 46 | 'getZSlice', 47 | ] -------------------------------------------------------------------------------- /itkwidgets/_type_aliases.py: -------------------------------------------------------------------------------- 1 | import itkwasm 2 | import zarr 3 | import numpy as np 4 | import dask 5 | 6 | from .integrations.itk import HAVE_ITK 7 | from .integrations.pytorch import HAVE_TORCH 8 | from .integrations.vtk import HAVE_VTK 9 | from .integrations.xarray import HAVE_XARRAY 10 | from typing import Dict, List, Literal, Union, Sequence 11 | 12 | Points2d = Sequence[Sequence[float]] 13 | 14 | Style = Dict[str, str] 15 | 16 | Image = Union[np.ndarray, itkwasm.Image, zarr.Group] 17 | PointSet = Union[np.ndarray, itkwasm.PointSet, zarr.Group] 18 | CroppingPlanes = {Literal['origin']: List[float], Literal['normal']: List[int]} 19 | 20 | if HAVE_ITK: 21 | import itk 22 | Image = Union[Image, itk.Image] 23 | if HAVE_VTK: 24 | import vtk 25 | Image = Union[Image, vtk.vtkImageData] 26 | PointSet = Union[PointSet, vtk.vtkPolyData] 27 | Image = Union[Image, dask.array.core.Array] 28 | PointSet = Union[PointSet, dask.array.core.Array] 29 | if HAVE_TORCH: 30 | import torch 31 | Image = Union[Image, torch.Tensor] 32 | PointSet = Union[PointSet, torch.Tensor] 33 | if HAVE_XARRAY: 34 | import xarray 35 | Image = Union[Image, xarray.DataArray, xarray.Dataset] 36 | PointSet = Union[PointSet, xarray.DataArray, xarray.Dataset] 37 | -------------------------------------------------------------------------------- /itkwidgets/cell_watcher.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from inspect import isawaitable, iscoroutinefunction 4 | from typing import Callable, Dict, List 5 | from IPython import get_ipython 6 | from IPython.core.interactiveshell import ExecutionResult 7 | from queue import Queue 8 | from imjoy_rpc.utils import FuturePromise 9 | from zmq.eventloop.zmqstream import ZMQStream 10 | 11 | background_tasks = set() 12 | 13 | 14 | class Viewers(object): 15 | """This class is designed to track each instance of the Viewer class that 16 | is instantiated as well as whether or not that instance is available for 17 | updates or requests. 18 | """ 19 | def __init__(self): 20 | self._data = {} 21 | 22 | @property 23 | def data(self) -> Dict[str, Dict[str, bool]]: 24 | """Get the underlying data dict containg all viewer data 25 | 26 | :return: A dict of key, value pairs mapping the unique Viewer name to a 27 | dictionary containing a 'ready' key and a boolean value reflecting the 28 | ready state of the Viewer. 29 | :rtype: Dict[str, Dict[str, bool]] 30 | """ 31 | return self._data 32 | 33 | @property 34 | def not_created(self) -> List[str]: 35 | """Return a list of all unavailable viewers 36 | 37 | :return: A list of names of viewers that have not yet been created. 38 | :rtype: List[str] 39 | """ 40 | return [k for k in self.data.keys() if not self.viewer_ready(k)] 41 | 42 | def add_viewer(self, view: str) -> None: 43 | """Add a new Viewer object to track. 44 | 45 | :param view: The unique string identifier for the Viewer object 46 | :type view: str 47 | """ 48 | self.data[view] = {"ready": False} 49 | 50 | def update_viewer_status(self, view: str, status: bool) -> None: 51 | """Update a Viewer's 'ready' status. 52 | 53 | :param view: The unique string identifier for the Viewer object 54 | :type view: str 55 | :param status: Boolean value indicating whether or not the viewer is 56 | available for requests or updates. This should be false when the plugin 57 | API is not yet available or new data is not yet rendered. 58 | :type status: bool 59 | """ 60 | if view not in self.data.keys(): 61 | self.add_viewer(view) 62 | self.data[view]["ready"] = status 63 | 64 | def viewer_ready(self, view: str) -> bool: 65 | """Request the 'ready' status of a viewer. 66 | 67 | :param view: The unique string identifier for the Viewer object 68 | :type view: str 69 | 70 | :return: Boolean value indicating whether or not the viewer is 71 | available for requests or updates. This will be false when the plugin 72 | API is not yet available or new data is not yet rendered. 73 | :rtype: bool 74 | """ 75 | return self.data.get(view, {}).get("ready", False) 76 | 77 | 78 | class CellWatcher(object): 79 | """A singleton class used in interactive Jupyter notebooks in order to 80 | support asynchronous network communication that would otherwise be blocked 81 | by the IPython kernel. 82 | """ 83 | 84 | def __new__(cls): 85 | """Create a singleton class.""" 86 | if not hasattr(cls, "_instance"): 87 | cls._instance = super(CellWatcher, cls).__new__(cls) 88 | cls._instance.setup() 89 | return cls._instance 90 | 91 | def setup(self) -> None: 92 | """Perform the initial setup, including intercepting 'execute_request' 93 | handlers so that we can handle them internally before the IPython 94 | kernel does. 95 | """ 96 | self.viewers = Viewers() 97 | self.shell = get_ipython() 98 | self.kernel = self.shell.kernel 99 | self.shell_stream = getattr(self.kernel, "shell_stream", None) 100 | # Keep a reference to the ipykernel execute_request function 101 | self.execute_request_handler = self.kernel.shell_handlers["execute_request"] 102 | self.current_request = None 103 | self.waiting_on_viewer = False 104 | self.results = {} 105 | self.abort_all = False 106 | 107 | self._events = Queue() 108 | 109 | # Replace the ipykernel shell_handler for execute_request with our own 110 | # function, which we can use to capture, queue and process future cells 111 | if iscoroutinefunction(self.execute_request_handler): # ipykernel 6+ 112 | self.kernel.shell_handlers["execute_request"] = self.capture_event_async 113 | else: 114 | # ipykernel < 6 115 | self.kernel.shell_handlers["execute_request"] = self.capture_event 116 | 117 | # Call self.post_run_cell every time the post_run_cell signal is emitted 118 | # post_run_cell runs after interactive execution (e.g. a cell in a notebook) 119 | self.shell.events.register("post_run_cell", self.post_run_cell) 120 | 121 | def add_viewer(self, view: str) -> None: 122 | """Add a new Viewer object to track. 123 | 124 | :param view: The unique string identifier for the Viewer object 125 | :type view: str 126 | """ 127 | # Track all Viewer instances 128 | self.viewers.add_viewer(view) 129 | 130 | def update_viewer_status(self, view: str, status: bool) -> None: 131 | """Update a Viewer's 'ready' status. If the last cell run failed 132 | because the viewer was unavailable try to run the cell again. 133 | 134 | :param view: The unique string identifier for the Viewer object 135 | :type view: str 136 | :param status: Boolean value indicating whether or not the viewer is 137 | available for requests or updates. This should be false when the plugin 138 | API is not yet available or new data is not yet rendered. 139 | :type status: bool 140 | """ 141 | self.viewers.update_viewer_status(view, status) 142 | if status and self.waiting_on_viewer: 143 | # Might be ready now, try again 144 | self.create_task(self.execute_next_request) 145 | 146 | def viewer_ready(self, view: str) -> bool: 147 | """Request the 'ready' status of a viewer. 148 | 149 | :param view: The unique string identifier for the Viewer object 150 | :type view: str 151 | 152 | :return: Boolean value indicating whether or not the viewer is 153 | available for requests or updates. This will be false when the plugin 154 | API is not yet available or new data is not yet rendered. 155 | :rtype: bool 156 | """ 157 | return self.viewers.viewer_ready(view) 158 | 159 | def _task_cleanup(self, task: asyncio.Task) -> None: 160 | """Callback to discard references to tasks once they've completed. 161 | 162 | :param task: Completed task that no longer needs a strong reference 163 | :type task: asyncio.Task 164 | """ 165 | global background_tasks 166 | try: 167 | # "Handle" exceptions here to prevent further errors. Exceptions 168 | # thrown will be actually be raised in the Viewer._fetch_value 169 | # decorator. 170 | _ = task.exception() 171 | except: 172 | background_tasks.discard(task) 173 | 174 | def create_task(self, fn: Callable) -> None: 175 | """Create a task from the function passed in. 176 | 177 | :param fn: Coroutine to run concurrently as a Task 178 | :type fn: Callable 179 | """ 180 | global background_tasks 181 | # The event loop only keeps weak references to tasks. 182 | # Gather them into a set to avoid garbage collection mid-task. 183 | task = asyncio.create_task(fn()) 184 | background_tasks.add(task) 185 | task.add_done_callback(self._task_cleanup) 186 | 187 | def capture_event(self, stream: ZMQStream, ident: list, parent: dict) -> None: 188 | """Capture execute_request messages so that we can queue and process 189 | them concurrently as tasks to prevent blocking. 190 | 191 | :param stream: Class to manage event-based messaging on a zmq socket 192 | :type stream: ZMQStream 193 | :param ident: ZeroMQ routing prefix, which can be zero or more socket 194 | identities 195 | :type ident: list 196 | :param parent: A dictonary of dictionaries representing a complete 197 | message as defined by the Jupyter message specification 198 | :type parent: dict 199 | """ 200 | if self._events.empty() and self.abort_all: 201 | # We've processed all pending cells, reset the abort flag 202 | self.abort_all = False 203 | 204 | self._events.put((stream, ident, parent)) 205 | if self._events.qsize() == 1 and self.ready_to_run_next_cell(): 206 | # We've added a new task to an empty queue. 207 | # Begin executing tasks again. 208 | self.create_task(self.execute_next_request) 209 | 210 | async def capture_event_async( 211 | self, stream: ZMQStream, ident: list, parent: dict 212 | ) -> None: 213 | """Capture execute_request messages so that we can queue and process 214 | them concurrently as tasks to prevent blocking. 215 | Asynchronous for ipykernel 6+. 216 | 217 | :param stream: Class to manage event-based messaging on a zmq socket 218 | :type stream: ZMQStream 219 | :param ident: ZeroMQ routing prefix, which can be zero or more socket 220 | identities 221 | :type ident: list 222 | :param parent: A dictonary of dictionaries representing a complete 223 | message as defined by the Jupyter message specification 224 | :type parent: dict 225 | """ 226 | # ipykernel 6+ 227 | self.capture_event(stream, ident, parent) 228 | 229 | @property 230 | def all_getters_resolved(self) -> bool: 231 | """Determine if all tasks representing asynchronous network calls that 232 | fetch values have resolved. 233 | 234 | :return: Whether or not all tasks for the current cell have resolved 235 | :rtype: bool 236 | """ 237 | getters_resolved = [f.done() for f in self.results.values()] 238 | return all(getters_resolved) 239 | 240 | def ready_to_run_next_cell(self) -> bool: 241 | """Determine if we are ready to run the next cell in the queue. 242 | 243 | :return: If created Viewer objects are available and all futures are 244 | resolved. 245 | :rtype: bool 246 | """ 247 | self.waiting_on_viewer = len(self.viewers.not_created) 248 | return self.all_getters_resolved and not self.waiting_on_viewer 249 | 250 | async def execute_next_request(self) -> None: 251 | """Grab the next request if needed and then run the cell if it it ready 252 | to be run. Modeled after the approach used in jupyter-ui-poll. 253 | :ref: https://github.com/Kirill888/jupyter-ui-poll/blob/f65b81f95623c699ed7fd66a92be6d40feb73cde/jupyter_ui_poll/_poll.py#L75-L101 254 | """ 255 | if self._events.empty(): 256 | self.abort_all = False 257 | 258 | if self.current_request is None and not self._events.empty(): 259 | # Fetch the next request if we haven't already 260 | self.current_request = self._events.get() 261 | 262 | if self.ready_to_run_next_cell(): 263 | # Continue processing the remaining queued tasks 264 | await self._execute_next_request() 265 | 266 | async def _execute_next_request(self) -> None: 267 | """Run the cell with the ipykernel shell_handler for execute_request""" 268 | # Here we actually run the queued cell as it would have been run 269 | stream, ident, parent = self.current_request 270 | 271 | # Set I/O to the correct cell 272 | self.kernel.set_parent(ident, parent) 273 | if self.abort_all or self.kernel._aborting: 274 | self.kernel._send_abort_reply(stream, parent, ident) 275 | else: 276 | # Use the original kernel execute_request method to run the cell 277 | rr = self.execute_request_handler(stream, ident, parent) 278 | if isawaitable(rr): 279 | rr = await rr 280 | 281 | # Make sure we print all output to the correct cell 282 | sys.stdout.flush() 283 | sys.stderr.flush() 284 | if self.shell_stream is not None: 285 | # ipykernel 6 286 | self.kernel._publish_status("idle", "shell") 287 | self.shell_stream.flush(2) 288 | else: 289 | self.kernel._publish_status("idle") 290 | 291 | if not self.results: 292 | self.current_request = None 293 | if self.all_getters_resolved and not self._events.empty(): 294 | # Continue processing the remaining queued tasks 295 | self.create_task(self.execute_next_request) 296 | 297 | def update_namespace(self) -> None: 298 | """Update the namespace variables with the results from the getters""" 299 | # FIXME: This is a temporary "fix" and does not handle updating output 300 | keys = [k for k in self.shell.user_ns.keys()] 301 | try: 302 | for key in keys: 303 | value = self.shell.user_ns[key] 304 | if asyncio.isfuture(value) and (isinstance(value, FuturePromise) or isinstance(value, asyncio.Task)): 305 | # Getters/setters return futures 306 | # They should all be resolved now, so use the result 307 | self.shell.user_ns[key] = value.result() 308 | self.results.clear() 309 | except Exception as e: 310 | self.shell.user_ns[key] = e 311 | self.results.clear() 312 | self.abort_all = True 313 | self.create_task(self._execute_next_request) 314 | raise e 315 | 316 | def _callback(self, *args, **kwargs) -> None: 317 | """After each future resolves check to see if they are all resolved. If 318 | so, update the namespace and run the next cell in the queue. 319 | """ 320 | # After each getter/setter resolves check if they've all resolved 321 | if self.all_getters_resolved: 322 | self.update_namespace() 323 | self.current_request = None 324 | self.create_task(self.execute_next_request) 325 | 326 | def post_run_cell(self, response: ExecutionResult) -> None: 327 | """Runs after interactive execution (e.g. a cell in a notebook). Set 328 | the abort flag if there are errors produced by cell execution. 329 | 330 | :param response: The response message produced by cell execution 331 | :type response: ExecutionResult 332 | """ 333 | # Abort remaining cells on error in execution 334 | if response.error_in_exec is not None: 335 | self.abort_all = True 336 | -------------------------------------------------------------------------------- /itkwidgets/imjoy.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | 3 | from typing import Dict 4 | 5 | import itkwasm 6 | import numcodecs 7 | from imjoy_rpc import api 8 | import zarr 9 | 10 | _numcodec_encoder = numcodecs.Blosc(cname='lz4', clevel=3) 11 | _numcodec_config = _numcodec_encoder.get_config() 12 | 13 | def encode_itkwasm_image(image): 14 | global _numcodec_encoder 15 | 16 | image_dict = asdict(image) 17 | 18 | image_data = image_dict['data'] 19 | encoded_data = _numcodec_encoder.encode(image_data) 20 | image_dict['data'] = { 'buffer': encoded_data, 'config': _numcodec_config, 'nbytes': image_data.nbytes } 21 | 22 | image_direction = image_dict['direction'] 23 | encoded_direction = _numcodec_encoder.encode(image_direction) 24 | image_dict['direction'] = { 'buffer': encoded_direction, 'config': _numcodec_config, 'nbytes': image_direction.nbytes } 25 | 26 | return image_dict 27 | 28 | def encode_zarr_store(store): 29 | def getItem(key): 30 | return store[key] 31 | 32 | def setItem(key, value): 33 | store[key] = value 34 | 35 | def containsItem(key): 36 | return key in store 37 | 38 | return { 39 | "_rintf": True, 40 | "_rtype": 'zarr-store', 41 | "getItem": getItem, 42 | "setItem": setItem, 43 | "containsItem": containsItem, 44 | } 45 | 46 | def register_itkwasm_imjoy_codecs(): 47 | 48 | api.registerCodec({'name': 'itkwasm-image', 'type': itkwasm.Image, 'encoder': encode_itkwasm_image}) 49 | api.registerCodec({'name': 'zarr-store', 'type': zarr.storage.BaseStore, 'encoder': encode_zarr_store}) 50 | 51 | 52 | def register_itkwasm_imjoy_codecs_cli(server): 53 | server.register_codec({'name': 'itkwasm-image', 'type': itkwasm.Image, 'encoder': encode_itkwasm_image}) 54 | server.register_codec({'name': 'zarr-store', 'type': zarr.storage.BaseStore, 'encoder': encode_zarr_store}) 55 | -------------------------------------------------------------------------------- /itkwidgets/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | import itkwasm 2 | import numpy as np 3 | import zarr 4 | from ngff_zarr import to_multiscales, to_ngff_zarr, to_ngff_image, itk_image_to_ngff_image, Methods, NgffImage, Multiscales 5 | 6 | import dask 7 | from .itk import HAVE_ITK 8 | from .pytorch import HAVE_TORCH 9 | from .monai import HAVE_MONAI 10 | from .vtk import HAVE_VTK, vtk_image_to_ngff_image, vtk_polydata_to_vtkjs 11 | from .xarray import HAVE_XARRAY, HAVE_MULTISCALE_SPATIAL_IMAGE, xarray_data_array_to_numpy, xarray_data_set_to_numpy 12 | from ..render_types import RenderType 13 | from .environment import ENVIRONMENT, Env 14 | 15 | def _spatial_image_scale_factors(spatial_image, min_length): 16 | sizes = dict(spatial_image.sizes) 17 | scale_factors = [] 18 | dims = spatial_image.dims 19 | previous = { d: 1 for d in { 'x', 'y', 'z' }.intersection(dims) } 20 | while (np.array(list(sizes.values())) > min_length).any(): 21 | max_size = np.array(list(sizes.values())).max() 22 | to_skip = { d: sizes[d] <= max_size / 2 for d in previous.keys() } 23 | scale_factor = {} 24 | for dim in previous.keys(): 25 | if to_skip[dim]: 26 | scale_factor[dim] = previous[dim] 27 | continue 28 | scale_factor[dim] = 2 * previous[dim] 29 | 30 | sizes[dim] = int(sizes[dim] / 2) 31 | previous = scale_factor 32 | scale_factors.append(scale_factor) 33 | 34 | return scale_factors 35 | 36 | def _make_multiscale_store(): 37 | # Todo: for very large images serialize to disk cache 38 | # -> create DirectoryStore in cache directory and return as the chunk_store 39 | store = zarr.storage.MemoryStore(dimension_separator='/') 40 | return store, None 41 | 42 | def _get_viewer_image(image, label=False): 43 | # NGFF Zarr 44 | if isinstance(image, zarr.Group) and 'multiscales' in image.attrs: 45 | return image.store 46 | 47 | min_length = 64 48 | # ITKWASM methods are currently only async in pyodide 49 | if ENVIRONMENT is Env.JUPYTERLITE: 50 | if label: 51 | method = Methods.DASK_IMAGE_NEAREST 52 | else: 53 | method = Methods.DASK_IMAGE_GAUSSIAN 54 | else: 55 | if label: 56 | method = Methods.ITKWASM_LABEL_IMAGE 57 | else: 58 | method = Methods.ITKWASM_GAUSSIAN 59 | 60 | store, chunk_store = _make_multiscale_store() 61 | 62 | if isinstance(image, NgffImage): 63 | multiscales = to_multiscales(image, method=method) 64 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 65 | return store 66 | 67 | if isinstance(image, Multiscales): 68 | to_ngff_zarr(store, image, chunk_store=chunk_store) 69 | return store 70 | 71 | if isinstance(image, zarr.storage.BaseStore): 72 | return image 73 | 74 | if HAVE_MULTISCALE_SPATIAL_IMAGE: 75 | from multiscale_spatial_image import MultiscaleSpatialImage 76 | if isinstance(image, MultiscaleSpatialImage): 77 | image.to_zarr(store, compute=True) 78 | return store 79 | 80 | if isinstance(image, itkwasm.Image): 81 | ngff_image = itk_image_to_ngff_image(image) 82 | multiscales = to_multiscales(ngff_image, method=method) 83 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 84 | return store 85 | 86 | if HAVE_ITK: 87 | import itk 88 | if isinstance(image, itk.Image) or isinstance(image, itk.VectorImage): 89 | ngff_image = itk_image_to_ngff_image(image) 90 | multiscales = to_multiscales(ngff_image, method=method) 91 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 92 | return store 93 | 94 | if HAVE_VTK: 95 | import vtk 96 | if isinstance(image, vtk.vtkImageData): 97 | ngff_image = vtk_image_to_ngff_image(image) 98 | multiscales = to_multiscales(ngff_image, method=method) 99 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 100 | return store 101 | 102 | if isinstance(image, dask.array.core.Array): 103 | ngff_image = to_ngff_image(image) 104 | multiscales = to_multiscales(ngff_image, method=method) 105 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 106 | return store 107 | 108 | if isinstance(image, zarr.Array): 109 | ngff_image = to_ngff_image(image) 110 | multiscales = to_multiscales(ngff_image, method=method) 111 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 112 | return store 113 | 114 | if HAVE_MONAI: 115 | from monai.data import MetaTensor, metatensor_to_itk_image 116 | if isinstance(image, MetaTensor): 117 | itk_image = metatensor_to_itk_image(image) 118 | ngff_image = itk_image_to_ngff_image(itk_image) 119 | multiscales = to_multiscales(ngff_image, method=method) 120 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 121 | return store 122 | 123 | if HAVE_TORCH: 124 | import torch 125 | if isinstance(image, torch.Tensor): 126 | ngff_image = to_ngff_image(image.numpy()) 127 | multiscales = to_multiscales(ngff_image, method=method) 128 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 129 | return store 130 | 131 | # Todo: preserve dask Array, if present, check if dims are NGFF -> use dims, coords 132 | # Check if coords are uniform, if not, resample 133 | if HAVE_XARRAY: 134 | import xarray as xr 135 | if isinstance(image, xr.DataArray): 136 | # if HAVE_MULTISCALE_SPATIAL_IMAGE: 137 | # from spatial_image import is_spatial_image 138 | # if is_spatial_image(image): 139 | # from multiscale_spatial_image import to_multiscale 140 | # scale_factors = _spatial_image_scale_factors(image, min_length) 141 | # multiscale = to_multiscale(image, scale_factors, method=method) 142 | # return _make_multiscale_store(multiscale) 143 | 144 | return xarray_data_array_to_numpy(image) 145 | if isinstance(image, xr.Dataset): 146 | # da = image[next(iter(image.variables.keys()))] 147 | # if is_spatial_image(da): 148 | # scale_factors = _spatial_image_scale_factors(da, min_length) 149 | # multiscale = to_multiscale(da, scale_factors, method=method) 150 | # return _make_multiscale_store(multiscale) 151 | return xarray_data_set_to_numpy(image) 152 | 153 | if isinstance(image, np.ndarray): 154 | ngff_image = to_ngff_image(image) 155 | multiscales = to_multiscales(ngff_image, method=method) 156 | to_ngff_zarr(store, multiscales, chunk_store=chunk_store) 157 | return store 158 | 159 | raise RuntimeError("Could not process the viewer image") 160 | 161 | 162 | def _get_viewer_point_set(point_set): 163 | if HAVE_VTK: 164 | import vtk 165 | if isinstance(point_set, vtk.vtkPolyData): 166 | return vtk_polydata_to_vtkjs(point_set) 167 | if isinstance(point_set, dask.array.core.Array): 168 | return np.asarray(point_set) 169 | if HAVE_TORCH: 170 | import torch 171 | if isinstance(point_set, torch.Tensor): 172 | return point_set.numpy() 173 | if HAVE_XARRAY: 174 | import xarray as xr 175 | if isinstance(point_set, xr.DataArray): 176 | return xarray_data_array_to_numpy(point_set) 177 | if isinstance(point_set, xr.Dataset): 178 | return xarray_data_set_to_numpy(point_set) 179 | if HAVE_ITK: 180 | import itk 181 | if isinstance(point_set, itk.PointSet): 182 | return itk.array_from_vector_container(point_set.GetPoints()) 183 | return point_set 184 | 185 | 186 | def _detect_render_type(data, input_type) -> RenderType: 187 | if (input_type == 'image' or 188 | input_type == 'label_image' or 189 | input_type == 'fixed_image'): 190 | return RenderType.IMAGE 191 | elif input_type == 'point_set': 192 | return RenderType.POINT_SET 193 | if isinstance(data, itkwasm.Image): 194 | return RenderType.IMAGE 195 | elif isinstance(data, NgffImage): 196 | return RenderType.IMAGE 197 | elif isinstance(data, Multiscales): 198 | return RenderType.IMAGE 199 | elif isinstance(data, itkwasm.PointSet): 200 | return RenderType.POINT_SET 201 | elif isinstance(data, (zarr.Array, zarr.Group)): 202 | # For now assume zarr.Group is an image 203 | # In the future, once NGFF supports point sets fully 204 | # We may need to do more introspection 205 | return RenderType.IMAGE 206 | elif isinstance(data, np.ndarray): 207 | if data.ndim == 2 and data.shape[1] < 4: 208 | return RenderType.POINT_SET 209 | else: 210 | return RenderType.IMAGE 211 | elif isinstance(data, zarr.storage.BaseStore): 212 | return RenderType.IMAGE 213 | elif HAVE_ITK: 214 | import itk 215 | if isinstance(data, itk.Image): 216 | return RenderType.IMAGE 217 | elif isinstance(data, itk.VectorImage): 218 | return RenderType.IMAGE 219 | elif isinstance(data, itk.PointSet): 220 | return RenderType.POINT_SET 221 | if HAVE_MULTISCALE_SPATIAL_IMAGE: 222 | from multiscale_spatial_image import MultiscaleSpatialImage 223 | if isinstance(data, MultiscaleSpatialImage): 224 | return RenderType.IMAGE 225 | if HAVE_VTK: 226 | import vtk 227 | if isinstance(data, vtk.vtkImageData): 228 | return RenderType.IMAGE 229 | elif isinstance(data, vtk.vtkPolyData): 230 | return RenderType.POINT_SET 231 | if isinstance(data, dask.array.core.Array): 232 | if data.ndim ==2 and data.shape[1] < 4: 233 | return RenderType.POINT_SET 234 | else: 235 | return RenderType.IMAGE 236 | if HAVE_TORCH: 237 | import torch 238 | if isinstance(data, torch.Tensor): 239 | if data.dim == 2 and data.shape[1] < 4: 240 | return RenderType.POINT_SET 241 | else: 242 | return RenderType.IMAGE 243 | if HAVE_XARRAY: 244 | import xarray as xr 245 | if isinstance(data, xr.DataArray): 246 | if data.dims == 2 and data.shape[1] < 4: 247 | return RenderType.POINT_SET 248 | else: 249 | return RenderType.IMAGE 250 | if isinstance(data, xr.Dataset): 251 | if data.dims == 2 and data.shape[1] < 4: 252 | return RenderType.POINT_SET 253 | else: 254 | return RenderType.IMAGE 255 | -------------------------------------------------------------------------------- /itkwidgets/integrations/environment.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from importlib import import_module 3 | from packaging import version 4 | import importlib_metadata 5 | import sys 6 | 7 | 8 | class Env(Enum): 9 | JUPYTER = 'jupyter' 10 | JUPYTERLITE = 'lite' 11 | SAGEMAKER = 'sagemaker' 12 | HYPHA = 'hypha' 13 | COLAB = 'colab' 14 | 15 | 16 | def find_env(): 17 | try: 18 | from google.colab import files 19 | return Env.COLAB 20 | except: 21 | try: 22 | from IPython import get_ipython 23 | parent_header = get_ipython().parent_header 24 | username = parent_header['header']['username'] 25 | if username == '': 26 | return Env.JUPYTER 27 | else: 28 | return Env.SAGEMAKER 29 | except: 30 | if sys.platform == 'emscripten': 31 | return Env.JUPYTERLITE 32 | return Env.HYPHA 33 | 34 | 35 | ENVIRONMENT = find_env() 36 | 37 | if ENVIRONMENT is not Env.JUPYTERLITE and ENVIRONMENT is not Env.HYPHA: 38 | if ENVIRONMENT is not Env.COLAB: 39 | if ENVIRONMENT is Env.JUPYTER: 40 | try: 41 | notebook_version = importlib_metadata.version('notebook') 42 | if version.parse(notebook_version) < version.parse('7'): 43 | raise RuntimeError('itkwidgets 1.0a51 and newer requires Jupyter notebook>=7.') 44 | except importlib_metadata.PackageNotFoundError: 45 | # notebook may not be available 46 | pass 47 | try: 48 | lab_version = importlib_metadata.version('jupyterlab') 49 | if version.parse(lab_version) < version.parse('4'): 50 | raise RuntimeError('itkwidgets 1.0a51 and newer requires jupyterlab>=4.') 51 | except importlib_metadata.PackageNotFoundError: 52 | # jupyterlab may not be available 53 | pass 54 | try: 55 | import_module("imjoy_jupyterlab_extension") 56 | except ModuleNotFoundError: 57 | if ENVIRONMENT is Env.JUPYTERLITE: 58 | raise RuntimeError('imjoy-jupyterlab-extension is required. Install the package and refresh page.') 59 | elif sys.version_info.minor > 7: 60 | raise RuntimeError('imjoy-jupyterlab-extension is required. `pip install itkwidgets[lab]` and refresh page.') 61 | 62 | try: 63 | import imjoy_elfinder 64 | except: 65 | if ENVIRONMENT is Env.JUPYTERLITE: 66 | raise RuntimeError('imjoy-elfinder is required. Install the package and refresh page.') 67 | elif sys.version_info.minor > 7: 68 | raise RuntimeError('imjoy-elfinder is required. `pip install imjoy-elfinder` and refresh page.') 69 | -------------------------------------------------------------------------------- /itkwidgets/integrations/imageio.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/imageio.py -------------------------------------------------------------------------------- /itkwidgets/integrations/itk.py: -------------------------------------------------------------------------------- 1 | import itkwasm 2 | 3 | from packaging import version 4 | import importlib_metadata 5 | HAVE_ITK = False 6 | try: 7 | itk_version = importlib_metadata.version('itk-core') 8 | if version.parse(itk_version) < version.parse('5.3.0'): 9 | raise RuntimeError('itk 5.3 or newer is required. `pip install itk>=5.3.0`') 10 | HAVE_ITK = True 11 | except importlib_metadata.PackageNotFoundError: 12 | pass 13 | 14 | 15 | if HAVE_ITK: 16 | def itk_group_spatial_object_to_wasm_point_set(point_set): 17 | import itk 18 | point_set_dict = itk.dict_from_pointset(point_set) 19 | wasm_point_set = itkwasm.PointSet(**point_set_dict) 20 | return wasm_point_set 21 | 22 | else: 23 | def itk_group_spatial_object_to_wasm_point_set(point_set): 24 | raise RuntimeError('itk 5.3 or newer is required. `pip install itk>=5.3.0`') 25 | -------------------------------------------------------------------------------- /itkwidgets/integrations/meshio.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/meshio.py -------------------------------------------------------------------------------- /itkwidgets/integrations/monai.py: -------------------------------------------------------------------------------- 1 | import importlib_metadata 2 | 3 | HAVE_MONAI = False 4 | try: 5 | importlib_metadata.metadata("monai") 6 | HAVE_MONAI = True 7 | except importlib_metadata.PackageNotFoundError: 8 | pass 9 | -------------------------------------------------------------------------------- /itkwidgets/integrations/numpy.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/numpy.py -------------------------------------------------------------------------------- /itkwidgets/integrations/pyimagej.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/pyimagej.py -------------------------------------------------------------------------------- /itkwidgets/integrations/pytorch.py: -------------------------------------------------------------------------------- 1 | import importlib_metadata 2 | 3 | HAVE_TORCH = False 4 | try: 5 | importlib_metadata.metadata("torch") 6 | HAVE_TORCH = True 7 | except importlib_metadata.PackageNotFoundError: 8 | pass 9 | -------------------------------------------------------------------------------- /itkwidgets/integrations/pyvista.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/pyvista.py -------------------------------------------------------------------------------- /itkwidgets/integrations/skan.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/skan.py -------------------------------------------------------------------------------- /itkwidgets/integrations/vedo.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/vedo.py -------------------------------------------------------------------------------- /itkwidgets/integrations/vtk.py: -------------------------------------------------------------------------------- 1 | import importlib_metadata 2 | 3 | HAVE_VTK = False 4 | try: 5 | importlib_metadata.metadata("vtk") 6 | HAVE_VTK = True 7 | except importlib_metadata.PackageNotFoundError: 8 | pass 9 | 10 | from ngff_zarr import to_ngff_image 11 | 12 | 13 | def vtk_image_to_ngff_image(image): 14 | from vtk.util.numpy_support import vtk_to_numpy 15 | array = vtk_to_numpy(image.GetPointData().GetScalars()) 16 | dimensions = list(image.GetDimensions()) 17 | array.shape = dimensions[::-1] 18 | 19 | origin = image.GetOrigin() 20 | translation = { 'x': origin[0], 'y': origin[1], 'z': origin[2] } 21 | 22 | spacing = image.GetSpacing() 23 | scale = { 'x': spacing[0], 'y': spacing[1], 'z': spacing[2] } 24 | 25 | ngff_image = to_ngff_image(array, scale=scale, translation=translation) 26 | 27 | return ngff_image 28 | 29 | def vtk_polydata_to_vtkjs(point_set): 30 | from vtk.util.numpy_support import vtk_to_numpy 31 | array = vtk_to_numpy(point_set.GetPoints().GetData()) 32 | return array 33 | -------------------------------------------------------------------------------- /itkwidgets/integrations/xarray.py: -------------------------------------------------------------------------------- 1 | import importlib_metadata 2 | 3 | HAVE_XARRAY = False 4 | try: 5 | importlib_metadata.metadata("xarray") 6 | HAVE_XARRAY = True 7 | except importlib_metadata.PackageNotFoundError: 8 | pass 9 | 10 | HAVE_MULTISCALE_SPATIAL_IMAGE = False 11 | try: 12 | importlib_metadata.metadata("multiscale-spatial-image") 13 | HAVE_MULTISCALE_SPATIAL_IMAGE = True 14 | except importlib_metadata.PackageNotFoundError: 15 | pass 16 | 17 | def xarray_data_array_to_numpy(data_array): 18 | return data_array.to_numpy() 19 | 20 | def xarray_data_set_to_numpy(data_set): 21 | return xarray_data_array_to_numpy(data_set.to_array(name='Dataset')) 22 | -------------------------------------------------------------------------------- /itkwidgets/integrations/zarr.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/integrations/zarr.py -------------------------------------------------------------------------------- /itkwidgets/render_types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class RenderType(Enum): 4 | """Rendered data types""" 5 | IMAGE = "image" 6 | LABELIMAGE = "labelImage" 7 | GEOMETRY = "geometry" 8 | POINT_SET = "pointSets" 9 | FIXEDIMAGE = "fixedImage" 10 | -------------------------------------------------------------------------------- /itkwidgets/standalone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InsightSoftwareConsortium/itkwidgets/d4bd7c425944bbf90d008405bfda37ff4556b979/itkwidgets/standalone/__init__.py -------------------------------------------------------------------------------- /itkwidgets/standalone/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | SERVER_PORT = 37480 4 | SERVER_HOST = "127.0.0.1" 5 | 6 | VIEWER_HTML = str(Path(__file__).parent.resolve().absolute() / "index.html") 7 | -------------------------------------------------------------------------------- /itkwidgets/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /itkwidgets/standalone_server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import functools 3 | import logging 4 | import socket 5 | import code 6 | import threading 7 | import uuid 8 | import os 9 | import subprocess 10 | import sys 11 | import time 12 | from pathlib import Path 13 | from base64 import b64decode 14 | import webbrowser 15 | 16 | import imjoy_rpc 17 | 18 | from imjoy_rpc.hypha import connect_to_server_sync 19 | from itkwidgets.standalone.config import SERVER_HOST, SERVER_PORT, VIEWER_HTML 20 | from itkwidgets.imjoy import register_itkwasm_imjoy_codecs_cli 21 | from itkwidgets._initialization_params import ( 22 | build_config, 23 | build_init_data, 24 | init_params_dict, 25 | DATA_OPTIONS, 26 | ) 27 | from itkwidgets.viewer import view 28 | from ngff_zarr import detect_cli_io_backend, cli_input_to_ngff_image, ConversionBackend 29 | from pathlib import Path 30 | from urllib.parse import parse_qs, urlencode, urlparse 31 | from .integrations.environment import ENVIRONMENT, Env 32 | # not available in pyodide by default 33 | if ENVIRONMENT is not Env.JUPYTERLITE: 34 | from urllib3 import PoolManager, exceptions 35 | 36 | logging.getLogger("urllib3").setLevel(logging.ERROR) 37 | logging.getLogger("websocket-client").setLevel(logging.ERROR) 38 | 39 | 40 | def find_port(port=SERVER_PORT): 41 | # Find first available port starting at SERVER_PORT 42 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 43 | if s.connect_ex((SERVER_HOST, port)) == 0: 44 | # Port in use, try again 45 | return find_port(port=port + 1) 46 | else: 47 | return port 48 | 49 | 50 | VIEWER = None 51 | BROWSER = None 52 | 53 | 54 | def standalone_viewer(url): 55 | parsed = urlparse(url) 56 | query = parse_qs(parsed.query) 57 | server_url = f"http://{SERVER_HOST}:{parsed.port}" 58 | workspace = query.get("workspace", [""])[0] 59 | token = query.get("token", [""])[0] 60 | 61 | server = connect_to_server_sync( 62 | {"server_url": server_url, "workspace": workspace, "token": token} 63 | ) 64 | imjoy_rpc.api.update(server.server) 65 | register_itkwasm_imjoy_codecs_cli(server.server) 66 | 67 | svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer") 68 | return view(itk_viewer=svc.viewer()) 69 | 70 | 71 | def input_dict(viewer_options): 72 | user_input = read_files(viewer_options) 73 | data = build_init_data(user_input, {}) 74 | ui = user_input.get("ui", "reference") 75 | data["config"] = build_config(ui) 76 | 77 | if data["view_mode"] is not None: 78 | vm = data["view_mode"] 79 | if vm == "x": 80 | data["view_mode"] = "XPlane" 81 | elif vm == "y": 82 | data["view_mode"] = "YPlane" 83 | elif vm == "z": 84 | data["view_mode"] = "ZPlane" 85 | elif vm == "v": 86 | data["view_mode"] = "Volume" 87 | 88 | return {"data": data} 89 | 90 | 91 | def read_files(viewer_options): 92 | user_input = vars(viewer_options) 93 | reader = user_input.get("reader", None) 94 | for param in DATA_OPTIONS: 95 | input = user_input.get(param, None) 96 | if input: 97 | if reader: 98 | reader = ConversionBackend(reader) 99 | else: 100 | reader = detect_cli_io_backend([input]) 101 | if not input.find('://') == -1 and not Path(input).exists(): 102 | sys.stderr.write(f"File not found: {input}\n") 103 | # hack 104 | raise KeyboardInterrupt 105 | ngff_image = cli_input_to_ngff_image(reader, [input]) 106 | user_input[param] = ngff_image 107 | return user_input 108 | 109 | 110 | class ViewerReady: 111 | def __init__(self, viewer_options, init_params_dict): 112 | self.init_viewer_kwargs = vars(viewer_options) 113 | self.init_params_dict = init_params_dict 114 | self.event = threading.Event() 115 | 116 | async def on_ready(self, itk_viewer): 117 | settings = self.init_params_dict(itk_viewer) 118 | for key, value in self.init_viewer_kwargs.items(): 119 | if key in settings.keys() and value is not None: 120 | settings[key](value) 121 | 122 | self.event.set() 123 | 124 | def wait(self): 125 | self.event.wait() 126 | 127 | 128 | def set_label_or_image(server, type): 129 | workspace = server.config.workspace 130 | svc = server.get_service(f"{workspace}/itkwidgets-client:set-label-or-image") 131 | getattr(svc, f"set_{type}")() 132 | 133 | 134 | def fetch_zarr_store(store_type): 135 | return getattr(VIEWER, store_type, None) 136 | 137 | 138 | def start_viewer(server_url, viewer_options): 139 | server = connect_to_server_sync( 140 | { 141 | "client_id": "itkwidgets-server", 142 | "name": "itkwidgets_server", 143 | "server_url": server_url, 144 | } 145 | ) 146 | register_itkwasm_imjoy_codecs_cli(server) 147 | 148 | input_obj = input_dict(viewer_options) 149 | viewer_ready = ViewerReady(viewer_options, init_params_dict) 150 | server.register_service( 151 | { 152 | "name": "parsed_data", 153 | "id": "parsed-data", 154 | "description": "Provide parsed data to the client.", 155 | "config": { 156 | "visibility": "protected", 157 | "require_context": False, 158 | "run_in_executor": True, 159 | }, 160 | "inputObject": lambda: input_obj, 161 | "viewerReady": viewer_ready.on_ready, 162 | "fetchZarrStore": fetch_zarr_store, 163 | } 164 | ) 165 | 166 | server.register_service( 167 | { 168 | "name": "data_set", 169 | "id": "data-set", 170 | "description": "Save the image data set via REPL session.", 171 | "config": { 172 | "visibility": "protected", 173 | "require_context": False, 174 | "run_in_executor": True, 175 | }, 176 | "set_label_or_image": functools.partial(set_label_or_image, server), 177 | } 178 | ) 179 | 180 | return server, input_obj, viewer_ready 181 | 182 | 183 | def main(viewer_options): 184 | global VIEWER 185 | JWT_SECRET = str(uuid.uuid4()) 186 | os.environ["JWT_SECRET"] = JWT_SECRET 187 | hypha_server_env = os.environ.copy() 188 | 189 | port = find_port() 190 | server_url = f"http://{SERVER_HOST}:{port}" 191 | viewer_mount_dir = str(Path(VIEWER_HTML).parent) 192 | 193 | out = None if viewer_options.verbose else subprocess.DEVNULL 194 | err = None if viewer_options.verbose else subprocess.STDOUT 195 | with subprocess.Popen( 196 | [ 197 | sys.executable, 198 | "-m", 199 | "hypha.server", 200 | f"--host={SERVER_HOST}", 201 | f"--port={port}", 202 | "--static-mounts", 203 | f"/itkwidgets:{viewer_mount_dir}", 204 | ], 205 | env=hypha_server_env, 206 | stdout=out, 207 | stderr=err, 208 | ): 209 | timeout = 10 210 | while timeout > 0: 211 | try: 212 | http = PoolManager() 213 | response = http.request("GET", f"{server_url}/health/liveness") 214 | if response.status == 200: 215 | break 216 | except exceptions.MaxRetryError: 217 | pass 218 | timeout -= 0.1 219 | time.sleep(0.1) 220 | 221 | server, input_obj, viewer_ready = start_viewer(server_url, viewer_options) 222 | workspace = server.config.workspace 223 | token = server.generate_token() 224 | params = urlencode({"workspace": workspace, "token": token}) 225 | url = f"{server_url}/itkwidgets/index.html?{params}" 226 | 227 | # Updates for resolution progression 228 | rate = 1.0 229 | fast_rate = 0.05 230 | if viewer_options.rotate: 231 | rate = fast_rate 232 | 233 | if viewer_options.browser: 234 | sys.stdout.write(f"Viewer url:\n\n {url}\n\n") 235 | webbrowser.open_new_tab(f"{server_url}/itkwidgets/index.html?{params}") 236 | else: 237 | from playwright.sync_api import sync_playwright 238 | playwright = sync_playwright().start() 239 | args = [ 240 | "--enable-unsafe-webgpu", 241 | ] 242 | browser = playwright.chromium.launch(args=args) 243 | BROWSER = browser 244 | page = browser.new_page() 245 | 246 | terminal_size = os.get_terminal_size() 247 | width = terminal_size.columns * 10 248 | is_tmux = 'TMUX' in os.environ and 'tmux' in os.environ['TMUX'] 249 | # https://github.com/tmux/tmux/issues/1502 250 | if is_tmux: 251 | if viewer_options.use2D: 252 | width = min(width, 320) 253 | else: 254 | width = min(width, 420) 255 | else: 256 | width = min(width, 768) 257 | height = width 258 | page.set_viewport_size({"width": width, "height": height}) 259 | 260 | response = page.goto(url, timeout=0, wait_until="load") 261 | assert response.status == 200, ( 262 | "Failed to start browser app instance, " 263 | f"status: {response.status}, url: {url}" 264 | ) 265 | 266 | input_data = input_obj["data"] 267 | if not input_data["use2D"]: 268 | if input_data["x_slice"] is None and input_data["view_mode"] == "XPlane": 269 | page.locator('label[itk-vtk-tooltip-content="X plane play scroll"]').click() 270 | rate = fast_rate 271 | elif input_data["y_slice"] is None and input_data["view_mode"] == "YPlane": 272 | page.locator('label[itk-vtk-tooltip-content="Y plane play scroll"]').click() 273 | rate = fast_rate 274 | elif input_data["z_slice"] is None and input_data["view_mode"] == "ZPlane": 275 | page.locator('label[itk-vtk-tooltip-content="Z plane play scroll"]').click() 276 | rate = fast_rate 277 | 278 | viewer_ready.wait() # Wait until viewer is created before launching REPL 279 | workspace = server.config.workspace 280 | svc = server.get_service(f"{workspace}/itkwidgets-client:itk-vtk-viewer") 281 | VIEWER = view(itk_viewer=svc.viewer(), server=server) 282 | if not viewer_options.browser: 283 | from imgcat import imgcat 284 | terminal_height = min(terminal_size.lines - 1, terminal_size.columns // 3) 285 | 286 | 287 | while True: 288 | png_bin = b64decode(svc.capture_screenshot()[22:]) 289 | imgcat(png_bin, height=terminal_height) 290 | time.sleep(rate) 291 | CSI = b'\033[' 292 | sys.stdout.buffer.write(CSI + str(terminal_height).encode() + b"F") 293 | 294 | if viewer_options.repl: 295 | banner = f""" 296 | Welcome to the itkwidgets command line tool! Press CTRL+D or 297 | run `exit()` to terminate the REPL session. Use the `viewer` 298 | object to manipulate the viewer. 299 | """ 300 | exitmsg = "Exiting REPL. Press CTRL+C to exit the viewer." 301 | code.interact(banner=banner, local={"viewer": VIEWER, "svc": svc, "server": server}, exitmsg=exitmsg) 302 | 303 | 304 | def cli_entrypoint(): 305 | parser = argparse.ArgumentParser() 306 | 307 | parser.add_argument("data", nargs="?", type=str, help="Path to a data file.") 308 | parser.add_argument("-i", "--image", dest="image", type=str, help="Path to an image data file.") 309 | parser.add_argument( 310 | "-l", "--label-image", dest="label_image", type=str, help="Path to a label image data file." 311 | ) 312 | parser.add_argument("-p", "--point-set", dest="point_set", type=str, help="Path to a point set data file.") 313 | parser.add_argument( 314 | "--use2D", dest="use2D", action="store_true", default=False, help="Image is 2D." 315 | ) 316 | parser.add_argument( 317 | "--reader", 318 | type=str, 319 | choices=["ngff_zarr", "zarr", "itk", "tifffile", "imageio"], 320 | help="Backend to use to read the data file(s). Optional.", 321 | ) 322 | parser.add_argument( 323 | "--verbose", 324 | dest="verbose", 325 | action="store_true", 326 | default=False, 327 | help="Print all log messages to stdout.", 328 | ) 329 | parser.add_argument( 330 | "-b", "--browser", 331 | dest="browser", 332 | action="store_true", 333 | default=False, 334 | help="Render to a browser tab instead of the terminal.", 335 | ) 336 | parser.add_argument( 337 | "--repl", 338 | dest="repl", 339 | action="store_true", 340 | default=False, 341 | help="Start interactive REPL after launching viewer.", 342 | ) 343 | # General Interface 344 | parser.add_argument( 345 | "-r", "--rotate", 346 | dest="rotate", 347 | action="store_true", 348 | default=False, 349 | help="Continuously rotate the camera around the scene in volume rendering mode.", 350 | ) 351 | parser.add_argument( 352 | "--ui", 353 | type=str, 354 | choices=["reference", "pydata-sphinx"], 355 | default="reference", 356 | help="Which UI to use", 357 | ) 358 | parser.add_argument( 359 | "--ui-collapsed", 360 | dest="ui_collapsed", 361 | action="store_true", 362 | default=False, 363 | help="Collapse the native widget user interface.", 364 | ) 365 | parser.add_argument( 366 | "--no-annotations", 367 | dest="annotations", 368 | action="store_false", 369 | default=True, 370 | help="Display annotations describing orientation and the value of a mouse-position-based data probe.", 371 | ) 372 | parser.add_argument( 373 | "--axes", dest="axes", action="store_true", default=False, help="Display axes." 374 | ) 375 | parser.add_argument( 376 | "--bg-color", 377 | type=tuple, 378 | nargs="+", 379 | default=(0.0, 0.0, 0.0), 380 | help="Background color: (red, green, blue) tuple, components from 0.0 to 1.0.", 381 | ) 382 | # Images 383 | parser.add_argument( 384 | "--label-blend", 385 | type=float, 386 | help="Label map blend with intensity image, from 0.0 to 1.0.", 387 | ) 388 | parser.add_argument( 389 | "--label-names", 390 | type=list, 391 | nargs="+", 392 | help="String names associated with the integer label values. List of (label_value, label_name).", 393 | ) 394 | parser.add_argument( 395 | "--label-lut", 396 | type=str, 397 | help="Lookup table for the label map.", 398 | ) 399 | parser.add_argument( 400 | "--label-weights", 401 | type=float, 402 | help="The rendering weight assigned to current label. Values range from 0.0 to 1.0.", 403 | ) 404 | parser.add_argument( 405 | "--color-range", 406 | type=list, 407 | nargs="+", 408 | help="The [min, max] range of intensity values mapped to colors for the given image component identified by name.", 409 | ) 410 | parser.add_argument( 411 | "--color-bounds", 412 | type=list, 413 | nargs="+", 414 | help="The [min, max] range of intensity values for color maps that provide a bounds for user inputs.", 415 | ) 416 | parser.add_argument( 417 | "--cmap", 418 | type=str, 419 | help="The color map for the current component/channel.", 420 | ) 421 | parser.add_argument( 422 | "--x-slice", 423 | type=float, 424 | help="The position in world space of the X slicing plane.", 425 | ) 426 | parser.add_argument( 427 | "--y-slice", 428 | type=float, 429 | help="The position in world space of the Y slicing plane.", 430 | ) 431 | parser.add_argument( 432 | "--z-slice", 433 | type=float, 434 | help="The position in world space of the Z slicing plane.", 435 | ) 436 | parser.add_argument( 437 | "--no-interpolation", 438 | dest="interpolation", 439 | action="store_false", 440 | default=True, 441 | help="Linear as opposed to nearest neighbor interpolation for image slices.", 442 | ) 443 | parser.add_argument( 444 | "--gradient-opacity", 445 | type=float, 446 | help="Gradient opacity for composite volume rendering, in the range (0.0, 1.0].", 447 | ) 448 | parser.add_argument( 449 | "--gradient-opacity-scale", 450 | type=float, 451 | help="Gradient opacity scale for composite volume rendering, in the range (0.0, 1.0].", 452 | ) 453 | parser.add_argument( 454 | "--blend-mode", 455 | type=str, 456 | help='Volume rendering blend mode. Supported modes: "Composite", "Maximum", "Minimum", "Average".', 457 | ) 458 | parser.add_argument( 459 | "--component-hidden", 460 | dest="component_visible", 461 | action="store_false", 462 | default=True, 463 | help="Whether to used gradient-based shadows in the volume rendering.", 464 | ) 465 | parser.add_argument( 466 | "--shadow-disabled", 467 | dest="shadow_enabled", 468 | action="store_false", 469 | default=True, 470 | help="Whether to used gradient-based shadows in the volume rendering.", 471 | ) 472 | parser.add_argument( 473 | "-m", "--view-mode", 474 | type=str, 475 | choices=["x", "y", "z", "v"], 476 | help="Only relevant for 3D scenes.", 477 | ) 478 | parser.add_argument( 479 | "--layer", 480 | type=str, 481 | help="Select the layer identified by `name` in the user interface.", 482 | ) 483 | parser.add_argument( 484 | "--layer-hidden", 485 | dest="layer_visible", 486 | action="store_false", 487 | default=True, 488 | help="Whether the current layer is visible.", 489 | ) 490 | # Other Parameters 491 | parser.add_argument( 492 | "--sample-distance", 493 | type=float, 494 | help="Sampling distance for volume rendering, normalized from 0.0 to 1.0. Lower values result in a higher quality rendering. High values improve the framerate.", 495 | ) 496 | parser.add_argument("--units", type=str, help="Units to display in the scale bar.") 497 | 498 | viewer_options = parser.parse_args() 499 | 500 | try: 501 | main(viewer_options) 502 | except KeyboardInterrupt: 503 | if BROWSER: 504 | BROWSER.close() 505 | if not viewer_options.browser: 506 | # Clear `^C%` 507 | CSI = b'\033[' 508 | sys.stdout.buffer.write(CSI + b"1K") 509 | sys.stdout.buffer.write(b"\n") 510 | sys.exit(0) 511 | 512 | 513 | if __name__ == "__main__": 514 | cli_entrypoint() 515 | -------------------------------------------------------------------------------- /itkwidgets/viewer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import functools 5 | import queue 6 | import threading 7 | import numpy as np 8 | from imjoy_rpc import api 9 | from inspect import isawaitable 10 | from typing import Callable, Dict, List, Union, Tuple 11 | from IPython.display import display, HTML 12 | from IPython.lib import backgroundjobs as bg 13 | from ngff_zarr import from_ngff_zarr, to_ngff_image, Multiscales, NgffImage 14 | import uuid 15 | 16 | from ._method_types import deferred_methods 17 | from ._type_aliases import Style, Image, PointSet, CroppingPlanes, Points2d 18 | from ._initialization_params import ( 19 | init_params_dict, 20 | build_config, 21 | parse_input_data, 22 | build_init_data, 23 | defer_for_data_render, 24 | ) 25 | from .imjoy import register_itkwasm_imjoy_codecs 26 | from .integrations import _detect_render_type, _get_viewer_image, _get_viewer_point_set 27 | from .integrations.environment import ENVIRONMENT, Env 28 | from .render_types import RenderType 29 | from .viewer_config import ITK_VIEWER_SRC 30 | from imjoy_rpc import register_default_codecs 31 | 32 | __all__ = [ 33 | "Viewer", 34 | "view", 35 | ] 36 | 37 | _viewer_count = 1 38 | _codecs_registered = False 39 | _cell_watcher = None 40 | if not ENVIRONMENT in (Env.HYPHA, Env.JUPYTERLITE): 41 | from .cell_watcher import CellWatcher 42 | _cell_watcher = CellWatcher() # Instantiate the singleton class right away 43 | 44 | 45 | class ViewerRPC: 46 | """Viewer remote procedure interface.""" 47 | 48 | def __init__( 49 | self, 50 | ui_collapsed: bool = True, 51 | rotate: bool = False, 52 | ui: str = "pydata-sphinx", 53 | init_data: dict = None, 54 | parent: str = None, 55 | **add_data_kwargs, 56 | ) -> None: 57 | global _codecs_registered, _cell_watcher 58 | """Create a viewer.""" 59 | # Register codecs if they haven't been already 60 | if not _codecs_registered and ENVIRONMENT is not Env.HYPHA: 61 | register_default_codecs() 62 | register_itkwasm_imjoy_codecs() 63 | _codecs_registered = True 64 | 65 | self._init_viewer_kwargs = dict(ui_collapsed=ui_collapsed, rotate=rotate, ui=ui) 66 | self._init_viewer_kwargs.update(**add_data_kwargs) 67 | self.init_data = init_data 68 | self.img = display(HTML(f'
'), display_id=str(uuid.uuid4())) 69 | self.wid = None 70 | self.parent = parent 71 | if ENVIRONMENT is not Env.JUPYTERLITE: 72 | _cell_watcher and _cell_watcher.add_viewer(self.parent) 73 | if ENVIRONMENT is not Env.HYPHA: 74 | self.viewer_event = threading.Event() 75 | self.data_event = threading.Event() 76 | 77 | async def setup(self) -> None: 78 | pass 79 | 80 | async def run(self, ctx: dict) -> None: 81 | """ImJoy plugin setup function.""" 82 | global _viewer_count, _cell_watcher 83 | ui = self._init_viewer_kwargs.get("ui", None) 84 | config = build_config(ui) 85 | 86 | if ENVIRONMENT is not Env.HYPHA: 87 | itk_viewer = await api.createWindow( 88 | name=f"itkwidgets viewer {_viewer_count}", 89 | type="itk-vtk-viewer", 90 | src=ITK_VIEWER_SRC, 91 | fullscreen=True, 92 | data=self.init_data, 93 | # config should be a python data dictionary and can't be a string e.g. 'pydata-sphinx', 94 | config=config, 95 | ) 96 | _viewer_count += 1 97 | 98 | self.set_default_ui_values(itk_viewer) 99 | self.itk_viewer = itk_viewer 100 | self.wid = self.itk_viewer.config.window_id 101 | 102 | if ENVIRONMENT is not Env.JUPYTERLITE: 103 | # Create the initial screenshot 104 | await self.create_screenshot() 105 | itk_viewer.registerEventListener( 106 | 'renderedImageAssigned', self.set_event 107 | ) 108 | if not defer_for_data_render(self.init_data): 109 | # Once the viewer has been created any queued requests can be run 110 | _cell_watcher.update_viewer_status(self.parent, True) 111 | asyncio.get_running_loop().call_soon_threadsafe(self.viewer_event.set) 112 | 113 | # Wait and then update the screenshot in case rendered level changed 114 | await asyncio.sleep(10) 115 | await self.create_screenshot() 116 | # Set up an event listener so that the embedded 117 | # screenshot is updated when the user requests 118 | itk_viewer.registerEventListener('screenshotTaken', self.update_screenshot) 119 | 120 | def set_default_ui_values(self, itk_viewer: dict) -> None: 121 | """Set any UI values passed in on initialization. 122 | 123 | :param itk_viewer: The ImJoy plugin API to use 124 | :type itk_viewer: dict 125 | """ 126 | settings = init_params_dict(itk_viewer) 127 | for key, value in self._init_viewer_kwargs.items(): 128 | if key in settings.keys(): 129 | settings[key](value) 130 | 131 | async def create_screenshot(self) -> None: 132 | """Grab a screenshot of the current Viewer and embed it in the 133 | notebook cell. 134 | """ 135 | base64_image = await self.itk_viewer.captureImage() 136 | self.update_screenshot(base64_image) 137 | 138 | def update_screenshot(self, base64_image: str) -> None: 139 | """Embed an image in the current notebook cell. 140 | 141 | :param base64_image: An encoded image to be embedded 142 | :type base64_image: bstring 143 | """ 144 | html = HTML( 145 | f''' 146 | 147 | 154 | ''') 155 | self.img.display(html) 156 | 157 | def update_viewer_status(self): 158 | """Update the CellWatcher class to indicate that the Viewer is ready""" 159 | global _cell_watcher 160 | if not _cell_watcher.viewer_ready(self.parent): 161 | _cell_watcher.update_viewer_status(self.parent, True) 162 | 163 | def set_event(self, event_data: str) -> None: 164 | """Set the event in the background thread to indicate that the plugin 165 | API is available so that queued setter requests are processed. 166 | 167 | :param event_data: The name of the image that has been rendered 168 | :type event_data: string 169 | """ 170 | if not self.data_event.is_set(): 171 | # Once the data has been set the deferred queue requests can be run 172 | asyncio.get_running_loop().call_soon_threadsafe(self.data_event.set) 173 | if ENVIRONMENT is not Env.HYPHA: 174 | self.update_viewer_status() 175 | 176 | 177 | class Viewer: 178 | """Pythonic Viewer class.""" 179 | 180 | def __init__( 181 | self, 182 | ui_collapsed: bool = True, 183 | rotate: bool = False, 184 | ui: bool = "pydata-sphinx", 185 | **add_data_kwargs, 186 | ) -> None: 187 | """Create a viewer.""" 188 | self.stores = {} 189 | self.name = self.__str__() 190 | input_data = parse_input_data(add_data_kwargs) 191 | data = build_init_data(input_data, self.stores) 192 | if compare := input_data.get('compare'): 193 | data['compare'] = compare 194 | if ENVIRONMENT is not Env.HYPHA: 195 | self.viewer_rpc = ViewerRPC( 196 | ui_collapsed=ui_collapsed, rotate=rotate, ui=ui, init_data=data, parent=self.name, **add_data_kwargs 197 | ) 198 | if ENVIRONMENT is not Env.JUPYTERLITE: 199 | self._setup_queueing() 200 | api.export(self.viewer_rpc) 201 | else: 202 | self._itk_viewer = add_data_kwargs.get('itk_viewer', None) 203 | self.server = add_data_kwargs.get('server', None) 204 | self.workspace = self.server.config.workspace 205 | 206 | def _setup_queueing(self) -> None: 207 | """Create a background thread and two queues of requests: one will hold 208 | requests that can be run as soon as the plugin API is available, the 209 | deferred queue will hold requests that need the data to be rendered 210 | before they are applied. Background requests will not return any 211 | results. 212 | """ 213 | self.bg_jobs = bg.BackgroundJobManager() 214 | self.queue = queue.Queue() 215 | self.deferred_queue = queue.Queue() 216 | self.bg_thread = self.bg_jobs.new(self.queue_worker) 217 | 218 | @property 219 | def loop(self) -> asyncio.BaseEventLoop: 220 | """Return the running event loop in the current OS thread. 221 | 222 | :return: Current running event loop 223 | :rtype: asyncio.BaseEventLoop 224 | """ 225 | return asyncio.get_running_loop() 226 | 227 | @property 228 | def has_viewer(self) -> bool: 229 | """Whether or not the plugin API is available to call. 230 | 231 | :return: Availability of API 232 | :rtype: bool 233 | """ 234 | if hasattr(self, "viewer_rpc"): 235 | return hasattr(self.viewer_rpc, "itk_viewer") 236 | return self.itk_viewer is not None 237 | 238 | @property 239 | def itk_viewer(self) -> dict | None: 240 | """Return the plugin API if it is available. 241 | 242 | :return: The plugin API if available, else None 243 | :rtype: dict | None 244 | """ 245 | if hasattr(self, "viewer_rpc"): 246 | return self.viewer_rpc.itk_viewer 247 | return self._itk_viewer 248 | 249 | async def run_queued_requests(self) -> None: 250 | """Once the plugin API is available and the viewer_event is set, run 251 | all requests queued for the background thread that do not require the 252 | data to be available. 253 | Once the data has been rendered process any deferred requests. 254 | """ 255 | def _run_queued_requests(queue): 256 | method_name, args, kwargs = queue.get() 257 | fn = getattr(self.itk_viewer, method_name) 258 | self.loop.call_soon_threadsafe(asyncio.ensure_future, fn(*args, **kwargs)) 259 | 260 | # Wait for the viewer to be created 261 | self.viewer_rpc.viewer_event.wait() 262 | while self.queue.qsize(): 263 | _run_queued_requests(self.queue) 264 | # Wait for the data to be set 265 | self.viewer_rpc.data_event.wait() 266 | while self.deferred_queue.qsize(): 267 | _run_queued_requests(self.deferred_queue) 268 | 269 | def queue_worker(self) -> None: 270 | """Create a new event loop in the background thread and run until all 271 | queued tasks are complete. 272 | """ 273 | loop = asyncio.new_event_loop() 274 | asyncio.set_event_loop(loop) 275 | task = loop.create_task(self.run_queued_requests()) 276 | loop.run_until_complete(task) 277 | 278 | def call_getter(self, future: asyncio.Future) -> None: 279 | """Create a future for requests that expect a response and set the 280 | callback to update the CellWatcher once resolved. 281 | 282 | :param future: A future for the awaitable request that we are waiting 283 | to resolve. 284 | :type future: asyncio.Future 285 | """ 286 | global _cell_watcher 287 | name = uuid.uuid4() 288 | _cell_watcher.results[name] = future 289 | future.add_done_callback(functools.partial(_cell_watcher._callback, name)) 290 | 291 | def queue_request(self, method: Callable, *args, **kwargs) -> None: 292 | """Determine if a request should be run immeditately, queued to run 293 | once the plugin API is avaialable, or queued to run once the data has 294 | been rendered. 295 | 296 | :param method: Function to either call or queue 297 | :type method: Callable 298 | """ 299 | if ( 300 | ENVIRONMENT is Env.JUPYTERLITE or ENVIRONMENT is Env.HYPHA 301 | ) or self.has_viewer: 302 | fn = getattr(self.itk_viewer, method) 303 | fn(*args, **kwargs) 304 | elif method in deferred_methods(): 305 | self.deferred_queue.put((method, args, kwargs)) 306 | else: 307 | self.queue.put((method, args, kwargs)) 308 | 309 | def fetch_value(func: Callable) -> Callable: 310 | """Decorator function that wraps the decorated function and returns the 311 | wrapper. In this case we decorate our API wrapper functions in order to 312 | determine if it needs to be managed by the CellWatcher class. 313 | 314 | :param func: Plugin API wrapper 315 | :type func: Callable 316 | :return: wrapper function 317 | :rtype: Callable 318 | """ 319 | @functools.wraps(func) 320 | def _fetch_value(self, *args, **kwargs): 321 | result = func(self, *args, **kwargs) 322 | global _cell_watcher 323 | if isawaitable(result) and _cell_watcher: 324 | future = asyncio.ensure_future(result) 325 | self.call_getter(future) 326 | return future 327 | return result 328 | return _fetch_value 329 | 330 | @fetch_value 331 | def set_annotations_enabled(self, enabled: bool) -> None: 332 | """Set whether or not the annotations should be displayed. Queue the 333 | function to be run in the background thread once the plugin API is 334 | available. 335 | 336 | :param enabled: Should annotations be enabled 337 | :type enabled: bool 338 | """ 339 | self.queue_request('setAnnotationsEnabled', enabled) 340 | @fetch_value 341 | async def get_annotations_enabled(self) -> asyncio.Future | bool: 342 | """Determine if annotations are enabled. 343 | 344 | :return: The future for the coroutine, to be updated with the 345 | annotations visibility status. 346 | :rtype: asyncio.Future | bool 347 | """ 348 | return await self.viewer_rpc.itk_viewer.getAnnotationsEnabled() 349 | 350 | @fetch_value 351 | def set_axes_enabled(self, enabled: bool) -> None: 352 | """Set whether or not the axes should be displayed. Queue the function 353 | to be run in the background thread once the plugin API is available. 354 | 355 | :param enabled: If axes should be enabled 356 | :type enabled: bool 357 | """ 358 | self.queue_request('setAxesEnabled', enabled) 359 | @fetch_value 360 | async def get_axes_enabled(self) -> asyncio.Future | bool: 361 | """Determine if the axes are enabled. 362 | 363 | :return: The future for the coroutine, to be updated with the axes 364 | visibility status. 365 | :rtype: asyncio.Future | bool 366 | """ 367 | return await self.viewer_rpc.itk_viewer.getAxesEnabled() 368 | 369 | @fetch_value 370 | def set_background_color(self, bg_color: List[float]) -> None: 371 | """Set the background color for the viewer. Queue the function to be 372 | run in the background thread once the plugin API is available. 373 | 374 | :param bg_color: A list of floats [r, g, b, a] 375 | :type bg_color: List[float] 376 | """ 377 | self.queue_request('setBackgroundColor', bg_color) 378 | @fetch_value 379 | async def get_background_color(self) -> asyncio.Future | List[float]: 380 | """Get the current background color. 381 | 382 | :return: The future for the coroutine, to be updated with a list of 383 | floats representing the current color [r, g, b, a]. 384 | :rtype: asyncio.Future | List[float] 385 | """ 386 | return await self.viewer_rpc.itk_viewer.getBackgroundColor() 387 | 388 | @fetch_value 389 | def set_cropping_planes(self, cropping_planes: CroppingPlanes) -> None: 390 | """Set the origins and normals for the current cropping planes. Queue 391 | the function to be run in the background thread once the plugin API is 392 | available. 393 | 394 | :param cropping_planes: A list of 6 dicts representing the 6 cropping 395 | planes. Each dict should contain an 'origin' key with the origin with a 396 | list of three floats and a 'normal' key with a list of three ints. 397 | :type cropping_planes: CroppingPlanes 398 | """ 399 | self.queue_request('setCroppingPlanes', cropping_planes) 400 | @fetch_value 401 | async def get_cropping_planes(self) -> asyncio.Future | CroppingPlanes: 402 | """Get the origins and normals for the current cropping planes. 403 | 404 | :return: The future for the coroutine, to be updated with a list of 6 405 | dicts representing the 6 cropping planes. Each dict should contain an 406 | 'origin' key with the origin with a list of three floats and a 'normal' 407 | key with a list of three ints. 408 | :rtype: asyncio.Future | CroppingPlanes 409 | """ 410 | return await self.viewer_rpc.itk_viewer.getCroppingPlanes() 411 | 412 | @fetch_value 413 | def set_image(self, image: Image, name: str = 'Image') -> None: 414 | """Set the image to be rendered. Queue the function to be run in the 415 | background thread once the plugin API is available. 416 | 417 | :param image: Image data to render 418 | :type image: Image 419 | :param name: Image name, defaults to 'Image' 420 | :type name: str, optional 421 | """ 422 | global _cell_watcher 423 | render_type = _detect_render_type(image, 'image') 424 | if render_type is RenderType.IMAGE: 425 | image = _get_viewer_image(image, label=False) 426 | # Keep a reference to stores that we create 427 | self.stores[name] = image 428 | if ENVIRONMENT is Env.HYPHA: 429 | self.image = image 430 | svc_name = f'{self.workspace}/itkwidgets-server:data-set' 431 | svc = self.server.get_service(svc_name) 432 | svc.set_label_or_image('image') 433 | else: 434 | self.queue_request('setImage', image, name) 435 | # Make sure future getters are deferred until render 436 | _cell_watcher and _cell_watcher.update_viewer_status(self.name, False) 437 | elif render_type is RenderType.POINT_SET: 438 | image = _get_viewer_point_set(image) 439 | self.queue_request('setPointSets', image) 440 | @fetch_value 441 | async def get_image(self, name: str = 'Image') -> NgffImage: 442 | """Get the full, highest resolution image. 443 | 444 | :param name: Name of the loaded image data to use. 'Image', the 445 | default, selects the first loaded image. 446 | :type name: str 447 | 448 | :return: image 449 | :rtype: NgffImage 450 | """ 451 | if store := self.stores.get(name): 452 | multiscales = from_ngff_zarr(store) 453 | loaded_image = multiscales.images[0] 454 | roi_data = loaded_image.data 455 | return to_ngff_image( 456 | roi_data, 457 | dims=loaded_image.dims, 458 | scale=loaded_image.scale, 459 | name=name, 460 | axes_units=loaded_image.axes_units 461 | ) 462 | raise ValueError(f'No image data found for {name}.') 463 | 464 | @fetch_value 465 | def set_image_blend_mode(self, mode: str) -> None: 466 | """Set the volume rendering blend mode. Queue the function to be run in 467 | the background thread once the plugin API is available. 468 | 469 | :param mode: Volume blend mode. Supported modes: 'Composite', 470 | 'Maximum', 'Minimum', 'Average'. default: 'Composite'. 471 | :type mode: str 472 | """ 473 | self.queue_request('setImageBlendMode', mode) 474 | @fetch_value 475 | async def get_image_blend_mode(self) -> asyncio.Future | str: 476 | """Get the current volume rendering blend mode. 477 | 478 | :return: The future for the coroutine, to be updated with the current 479 | blend mode. 480 | :rtype: asyncio.Future | str 481 | """ 482 | return await self.viewer_rpc.itk_viewer.getImageBlendMode() 483 | 484 | @property 485 | @fetch_value 486 | async def color_map(self) -> asyncio.Future | str: 487 | """Get the color map for the current component/channel. 488 | 489 | :return: The future for the coroutine, to be updated with the current 490 | color map. 491 | :rtype: asyncio.Future | str 492 | """ 493 | return await self.viewer_rpc.itk_viewer.getImageColorMap() 494 | @color_map.setter 495 | @fetch_value 496 | async def color_map(self, color_map: str) -> None: 497 | """Set the color map for the current component/channel. Queue the 498 | function to be run in the background thread once the plugin API is 499 | available. 500 | 501 | :param color_map: Color map for the current image. default: 'Grayscale' 502 | :type color_map: str 503 | """ 504 | self.queue_request('setImageColorMap', color_map) 505 | 506 | @fetch_value 507 | def set_image_color_map(self, color_map: str) -> None: 508 | """Set the color map for the current component/channel. Queue the 509 | function to be run in the background thread once the plugin API is 510 | available. 511 | 512 | :param color_map: Color map for the current image. default: 'Grayscale' 513 | :type color_map: str 514 | """ 515 | self.queue_request('setImageColorMap', color_map) 516 | @fetch_value 517 | async def get_image_color_map(self) -> asyncio.Future | str: 518 | """Get the color map for the current component/channel. 519 | 520 | :return: The future for the coroutine, to be updated with the current 521 | color map. 522 | :rtype: asyncio.Future | str 523 | """ 524 | return await self.viewer_rpc.itk_viewer.getImageColorMap() 525 | 526 | @property 527 | @fetch_value 528 | async def color_range(self) -> asyncio.Future | List[float]: 529 | """Get the range of the data values mapped to colors for the given 530 | image. 531 | 532 | :return: _description_ 533 | :rtype: asyncio.Future | List[float] 534 | """ 535 | return await self.viewer_rpc.itk_viewer.getImageColorRange() 536 | @color_range.setter 537 | @fetch_value 538 | async def color_range(self, range: List[float]) -> None: 539 | """The range of the data values mapped to colors for the given image. 540 | Queue the function to be run in the background thread once the plugin 541 | API is available. 542 | 543 | :param range: The [min, max] range of the data values 544 | :type range: List[float] 545 | """ 546 | self.queue_request('setImageColorRange', range) 547 | 548 | @fetch_value 549 | def set_image_color_range(self, range: List[float]) -> None: 550 | """The range of the data values mapped to colors for the given image. 551 | Queue the function to be run in the background thread once the plugin 552 | API is available. 553 | 554 | :param range: The [min, max] range of the data values 555 | :type range: List[float] 556 | """ 557 | self.queue_request('setImageColorRange', range) 558 | @fetch_value 559 | async def get_image_color_range(self) -> asyncio.Future | List[float]: 560 | """Get the range of the data values mapped to colors for the given 561 | image. 562 | 563 | :return: The future for the coroutine, to be updated with the 564 | [min, max] range of the data values. 565 | :rtype: asyncio.Future | List[float] 566 | """ 567 | return await self.viewer_rpc.itk_viewer.getImageColorRange() 568 | 569 | @property 570 | @fetch_value 571 | async def vmin(self) -> asyncio.Future | float: 572 | """Get the minimum data value mapped to colors for the current image. 573 | 574 | :return: The future for the coroutine, to be updated with the minimum 575 | value mapped to the color map. 576 | :rtype: asyncio.Future | float 577 | """ 578 | range = await self.get_image_color_range() 579 | return range[0] 580 | @vmin.setter 581 | @fetch_value 582 | async def vmin(self, vmin: float) -> None: 583 | """Set the minimum data value mapped to colors for the current image. 584 | Queue the function to be run in the background thread once the plugin 585 | API is available. 586 | 587 | :param vmin: The minimum value mapped to the color map. 588 | :type vmin: float 589 | """ 590 | self.queue_request('setImageColorRangeMin', vmin) 591 | 592 | @property 593 | @fetch_value 594 | async def vmax(self) -> asyncio.Future | float: 595 | """Get the maximum data value mapped to colors for the current image. 596 | 597 | :return: The future for the coroutine, to be updated with the maximum 598 | value mapped to the color map. 599 | :rtype: asyncio.Future | float 600 | """ 601 | range = await self.get_image_color_range() 602 | return range[1] 603 | @vmax.setter 604 | @fetch_value 605 | async def vmax(self, vmax: float) -> None: 606 | """Set the maximum data value mapped to colors for the current image. 607 | Queue the function to be run in the background thread once the plugin 608 | API is available. 609 | 610 | :param vmax: The maximum value mapped to the color map. 611 | :type vmax: float 612 | """ 613 | self.queue_request('setImageColorRangeMax', vmax) 614 | 615 | @property 616 | @fetch_value 617 | async def color_bounds(self) -> asyncio.Future | List[float]: 618 | """Get the range of the data values for color maps. 619 | 620 | :return: The future for the coroutine, to be updated with the 621 | [min, max] range of the data values. 622 | :rtype: asyncio.Future | List[float] 623 | """ 624 | return await self.viewer_rpc.itk_viewer.getImageColorRangeBounds() 625 | @color_bounds.setter 626 | @fetch_value 627 | async def color_bounds(self, range: List[float]) -> None: 628 | """Set the range of the data values for color maps. Queue the function 629 | to be run in the background thread once the plugin API is available. 630 | 631 | :param range: The [min, max] range of the data values. 632 | :type range: List[float] 633 | """ 634 | self.queue_request('setImageColorRangeBounds', range) 635 | 636 | @fetch_value 637 | def set_image_color_range_bounds(self, range: List[float]) -> None: 638 | """Set the range of the data values for color maps. Queue the function 639 | to be run in the background thread once the plugin API is available. 640 | 641 | :param range: The [min, max] range of the data values. 642 | :type range: List[float] 643 | """ 644 | self.queue_request('setImageColorRangeBounds', range) 645 | @fetch_value 646 | async def get_image_color_range_bounds(self) -> asyncio.Future | List[float]: 647 | """Get the range of the data values for color maps. 648 | 649 | :return: The future for the coroutine, to be updated with the 650 | [min, max] range of the data values. 651 | :rtype: asyncio.Future | List[float] 652 | """ 653 | return await self.viewer_rpc.itk_viewer.getImageColorRangeBounds() 654 | 655 | @fetch_value 656 | def set_image_component_visibility(self, visibility: bool, component: int) -> None: 657 | """Set the given image intensity component index's visibility. Queue 658 | the function to be run in the background thread once the plugin API is 659 | available. 660 | 661 | :param visibility: Whether or not the component should be visible. 662 | :type visibility: bool 663 | :param component: The component to set the visibility for. 664 | :type component: int 665 | """ 666 | self.queue_request('setImageComponentVisibility', visibility, component) 667 | @fetch_value 668 | async def get_image_component_visibility( 669 | self, component: int 670 | ) -> asyncio.Future | int: 671 | """Get the given image intensity component index's visibility. 672 | 673 | :param component: The component to set the visibility for. 674 | :type component: int 675 | :return: The future for the coroutine, to be updated with the 676 | component's visibility. 677 | :rtype: asyncio.Future | int 678 | """ 679 | return await self.viewer_rpc.itk_viewer.getImageComponentVisibility(component) 680 | 681 | @property 682 | @fetch_value 683 | async def gradient_opacity(self) -> asyncio.Future | float: 684 | """Get the gradient opacity for composite volume rendering. 685 | 686 | :return: The future for the coroutine, to be updated with the gradient 687 | opacity. 688 | :rtype: asyncio.Future | float 689 | """ 690 | return await self.viewer_rpc.itk_viewer.getImageGradientOpacity() 691 | @gradient_opacity.setter 692 | @fetch_value 693 | async def gradient_opacity(self, opacity: float) -> None: 694 | """Set the gradient opacity for composite volume rendering. Queue 695 | the function to be run in the background thread once the plugin API is 696 | available. 697 | 698 | :param opacity: Gradient opacity in the range (0.0, 1.0]. default: 0.5 699 | :type opacity: float 700 | """ 701 | self.queue_request('setImageGradientOpacity', opacity) 702 | 703 | @fetch_value 704 | def set_image_gradient_opacity(self, opacity: float) -> None: 705 | """Set the gradient opacity for composite volume rendering. Queue 706 | the function to be run in the background thread once the plugin API is 707 | available. 708 | 709 | :param opacity: Gradient opacity in the range (0.0, 1.0]. default: 0.5 710 | :type opacity: float 711 | """ 712 | self.queue_request('setImageGradientOpacity', opacity) 713 | @fetch_value 714 | async def get_image_gradient_opacity(self) -> asyncio.Future | float: 715 | """Get the gradient opacity for composite volume rendering. 716 | 717 | :return: The future for the coroutine, to be updated with the gradient 718 | opacity. 719 | :rtype: asyncio.Future | float 720 | """ 721 | return await self.viewer_rpc.itk_viewer.getImageGradientOpacity() 722 | 723 | @property 724 | @fetch_value 725 | async def gradient_opacity_scale(self) -> asyncio.Future | float: 726 | """Get the gradient opacity scale for composite volume rendering. 727 | 728 | :return: The future for the coroutine, to be updated with the current 729 | gradient opacity scale. 730 | :rtype: asyncio.Future | float 731 | """ 732 | return await self.viewer_rpc.itk_viewer.getImageGradientOpacityScale() 733 | @gradient_opacity_scale.setter 734 | @fetch_value 735 | async def gradient_opacity_scale(self, min: float) -> None: 736 | """Set the gradient opacity scale for composite volume rendering. Queue 737 | the function to be run in the background thread once the plugin API is 738 | available. 739 | 740 | :param min: Gradient opacity scale in the range (0.0, 1.0] default: 0.5 741 | :type min: float 742 | """ 743 | self.queue_request('setImageGradientOpacityScale', min) 744 | 745 | @fetch_value 746 | def set_image_gradient_opacity_scale(self, min: float) -> None: 747 | """Set the gradient opacity scale for composite volume rendering. Queue 748 | the function to be run in the background thread once the plugin API is 749 | available. 750 | 751 | :param min: Gradient opacity scale in the range (0.0, 1.0] default: 0.5 752 | :type min: float 753 | """ 754 | self.queue_request('setImageGradientOpacityScale', min) 755 | @fetch_value 756 | async def get_image_gradient_opacity_scale(self) -> asyncio.Future | float: 757 | """Get the gradient opacity scale for composite volume rendering. 758 | 759 | :return: The future for the coroutine, to be updated with the current 760 | gradient opacity scale. 761 | :rtype: asyncio.Future | float 762 | """ 763 | return await self.viewer_rpc.itk_viewer.getImageGradientOpacityScale() 764 | 765 | @fetch_value 766 | def set_image_interpolation_enabled(self, enabled: bool) -> None: 767 | """Set whether to use linear as opposed to nearest neighbor 768 | interpolation for image slices. Queue the function to be run in the 769 | background thread once the plugin API is available. 770 | 771 | :param enabled: Use linear interpolation. default: True 772 | :type enabled: bool 773 | """ 774 | self.queue_request('setImageInterpolationEnabled', enabled) 775 | @fetch_value 776 | async def get_image_interpolation_enabled(self) -> asyncio.Future | bool: 777 | """Get whether to use linear as opposed to nearest neighbor 778 | interpolation for image slices. 779 | 780 | :return: The future for the coroutine, to be updated with whether 781 | linear interpolation is used. 782 | :rtype: asyncio.Future | bool 783 | """ 784 | return await self.viewer_rpc.itk_viewer.getImageInterpolationEnabled() 785 | 786 | @fetch_value 787 | def set_image_piecewise_function_points(self, points: Points2d) -> None: 788 | """Set the volume rendering opacity transfer function points. 789 | Queue the function to be run in the background thread once 790 | the plugin API is available. 791 | 792 | :param points: Opacity piecewise transfer function points. Example args: [[.2, .1], [.8, .9]] 793 | :type points2d: Points2d 794 | """ 795 | self.queue_request('setImagePiecewiseFunctionPoints', points) 796 | @fetch_value 797 | async def get_image_piecewise_function_points( 798 | self, 799 | ) -> asyncio.Future | Points2d: 800 | """Get the volume rendering opacity transfer function points. 801 | 802 | :return: The future for the coroutine, to be updated with the opacity 803 | transfer function points. 804 | :rtype: asyncio.Future | Points2d 805 | """ 806 | return await self.viewer_rpc.itk_viewer.getImagePiecewiseFunctionPoints() 807 | 808 | @fetch_value 809 | def set_image_shadow_enabled(self, enabled: bool) -> None: 810 | """Set whether to used gradient-based shadows in the volume rendering. 811 | Queue the function to be run in the background thread once the plugin 812 | API is available. 813 | 814 | :param enabled: Apply shadows. default: True 815 | :type enabled: bool 816 | """ 817 | self.queue_request('setImageShadowEnabled', enabled) 818 | @fetch_value 819 | async def get_image_shadow_enabled(self) -> asyncio.Future | bool: 820 | """Get whether gradient-based shadows are used in the volume rendering. 821 | 822 | :return: The future for the coroutine, to be updated with whether 823 | gradient-based shadows are used. 824 | :rtype: asyncio.Future | bool 825 | """ 826 | return await self.viewer_rpc.itk_viewer.getImageShadowEnabled() 827 | 828 | @fetch_value 829 | def set_image_volume_sample_distance(self, distance: float) -> None: 830 | """Set the sampling distance for volume rendering, normalized from 831 | 0.0 to 1.0. Lower values result in a higher quality rendering. High 832 | values improve the framerate. Queue the function to be run in the 833 | background thread once the plugin API is available. 834 | 835 | :param distance: Sampling distance for volume rendering. default: 0.2 836 | :type distance: float 837 | """ 838 | self.queue_request('setImageVolumeSampleDistance', distance) 839 | @fetch_value 840 | async def get_image_volume_sample_distance(self) -> asyncio.Future | float: 841 | """Get the normalized sampling distance for volume rendering. 842 | 843 | :return: The future for the coroutine, to be updated with the 844 | normalized sampling distance. 845 | :rtype: asyncio.Future | float 846 | """ 847 | return await self.viewer_rpc.itk_viewer.getImageVolumeSampleDistance() 848 | 849 | @fetch_value 850 | def set_image_volume_scattering_blend(self, scattering_blend: float) -> None: 851 | """Set the volumetric scattering blend. Queue the function to be run in 852 | the background thread once the plugin API is available. 853 | 854 | :param scattering_blend: Volumetric scattering blend in the range [0, 1] 855 | :type scattering_blend: float 856 | """ 857 | self.queue_request('setImageVolumeScatteringBlend', scattering_blend) 858 | @fetch_value 859 | async def get_image_volume_scattering_blend(self) -> asyncio.Future | float: 860 | """Get the volumetric scattering blend. 861 | 862 | :return: The future for the coroutine, to be updated with the current 863 | volumetric scattering blend. 864 | :rtype: asyncio.Future | float 865 | """ 866 | return await self.viewer_rpc.itk_viewer.getImageVolumeScatteringBlend() 867 | 868 | @fetch_value 869 | async def get_current_scale(self) -> asyncio.Future | int: 870 | """Get the current resolution scale of the primary image. 871 | 872 | 0 is the highest resolution. Values increase for lower resolutions. 873 | 874 | :return: scale 875 | :rtype: asyncio.Future | int 876 | """ 877 | return await self.viewer_rpc.itk_viewer.getLoadedScale() 878 | 879 | @fetch_value 880 | async def get_roi_image(self, scale: int = -1, name: str = 'Image') -> NgffImage: 881 | """Get the image for the current ROI. 882 | 883 | :param scale: scale of the primary image to get the slices for the 884 | current roi. -1, the default, uses the current scale. 885 | :type scale: int 886 | :param name: Name of the loaded image data to use. 'Image', the 887 | default, selects the first loaded image. 888 | :type name: str 889 | 890 | :return: roi_image 891 | :rtype: NgffImage 892 | """ 893 | if scale == -1: 894 | scale = await self.get_current_scale() 895 | roi_slices = await self.get_roi_slice(scale) 896 | roi_region = await self.get_roi_region() 897 | if store := self.stores.get(name): 898 | multiscales = from_ngff_zarr(store) 899 | loaded_image = multiscales.images[scale] 900 | roi_data = loaded_image.data[roi_slices] 901 | roi_data = roi_data.rechunk(loaded_image.data.chunksize) 902 | return to_ngff_image( 903 | roi_data, 904 | dims=loaded_image.dims, 905 | scale=loaded_image.scale, 906 | translation=roi_region[0], 907 | name=name, 908 | axes_units=loaded_image.axes_units 909 | ) 910 | raise ValueError(f'No image data found for {name}.') 911 | 912 | @fetch_value 913 | async def get_roi_multiscale(self, name: str = 'Image') -> Multiscales: 914 | """Build and return a new Multiscales NgffImage for the ROI. 915 | 916 | :param name: Name of the loaded image data to use. 'Image', the 917 | default, selects the first loaded image. 918 | :type name: str 919 | CellWatcher().update_viewer_status(self.name, False) 920 | 921 | :return: roi_multiscales 922 | :rtype: Multiscales NgffImage 923 | """ 924 | if store := self.stores.get(name): 925 | multiscales = from_ngff_zarr(store) 926 | scales = range(len(multiscales.images)) 927 | images = [await self.get_roi_image(s) for s in scales] 928 | return Multiscales( 929 | images=images, 930 | metadata=multiscales.metadata, 931 | scale_factors=multiscales.scale_factors, 932 | method=multiscales.method, 933 | chunks=multiscales.chunks 934 | ) 935 | raise ValueError(f'No image data found for {name}.') 936 | 937 | @fetch_value 938 | async def get_roi_region(self) -> asyncio.Future | List[Dict[str, float]]: 939 | """Get the current region of interest in world / physical space. 940 | 941 | Returns [lower_bounds, upper_bounds] in the form: 942 | 943 | [{ 'x': x0, 'y': y0, 'z': z0 }, { 'x': x1, 'y': y1, 'z': z1 }] 944 | 945 | :return: roi_region 946 | :rtype: asyncio.Future | List[Dict[str, float]] 947 | """ 948 | bounds = await self.viewer_rpc.itk_viewer.getCroppedImageWorldBounds() 949 | x0, x1, y0, y1, z0, z1 = bounds 950 | return [{ 'x': x0, 'y': y0, 'z': z0 }, { 'x': x1, 'y': y1, 'z': z1 }] 951 | 952 | @fetch_value 953 | async def get_roi_slice(self, scale: int = -1): 954 | """Get the current region of interest as Python slice objects for the 955 | current resolution of the primary image. The result is in the order: 956 | 957 | [z_slice, y_slice, x_slice] 958 | 959 | Not that for standard C-order NumPy arrays, this result can be used to 960 | directly extract the region of interest from the array. For example, 961 | 962 | roi_array = image.data[roi_slice] 963 | 964 | :param scale: scale of the primary image to get the slices for the 965 | current roi. -1, the default, uses the current scale. 966 | :type scale: int 967 | 968 | :return: roi_slice 969 | :rtype: List[slice] 970 | """ 971 | if scale == -1: 972 | scale = await self.get_current_scale() 973 | idxs = await self.viewer_rpc.itk_viewer.getCroppedIndexBounds(scale) 974 | x0, x1 = idxs['x'] 975 | y0, y1 = idxs['y'] 976 | z0, z1 = idxs['z'] 977 | return np.index_exp[int(z0):int(z1+1), int(y0):int(y1+1), int(x0):int(x1+1)] 978 | 979 | @fetch_value 980 | def compare_images( 981 | self, 982 | fixed_image: Union[str, Image], 983 | moving_image: Union[str, Image], 984 | method: str = None, 985 | image_mix: float = None, 986 | checkerboard: bool = None, 987 | pattern: Union[Tuple[int, int], Tuple[int, int, int]] = None, 988 | swap_image_order: bool = None, 989 | ) -> None: 990 | """Fuse 2 images with a checkerboard filter or as a 2 component image. 991 | The moving image is re-sampled to the fixed image space. Set a keyword 992 | argument to None to use defaults based on method. Queue the function to 993 | be run in the background thread once the plugin API is available. 994 | 995 | :param fixed_image: Static image the moving image is re-sampled to. For 996 | non-checkerboard methods ('blend', 'green-magenta', etc.), the fixed 997 | image is on the first component. 998 | :type fixed_image: array_like, itk.Image, or vtk.vtkImageData 999 | :param moving_image: Image is re-sampled to the fixed_image. For 1000 | non-checkerboard methods ('blend', 'green-magenta', etc.), the moving 1001 | image is on the second component. 1002 | :type moving_image: array_like, itk.Image, or vtk.vtkImageData 1003 | :param method: The checkerboard method picks pixels from the fixed and 1004 | moving image to create a checkerboard pattern. Setting the method to 1005 | checkerboard turns on the checkerboard flag. The non-checkerboard 1006 | methods ('blend', 'green-magenta', etc.) put the fixed image on 1007 | component 0, moving image on component 1. The 'green-magenta' and 1008 | 'red-cyan' change the color maps so matching images are grayish white. 1009 | The 'cyan-magenta' color maps produce a purple if the images match. 1010 | :type method: string, default: None, possible values: 'green-magenta', 1011 | 'cyan-red', 'cyan-magenta', 'blend', 'checkerboard', 'disabled' 1012 | :param image_mix: Changes the percent contribution the fixed vs moving 1013 | image makes to the render by modifying the opacity transfer function. 1014 | Value of 1 means max opacity for moving image, 0 for fixed image. If 1015 | value is None and the method is not checkerboard, the image_mix is set 1016 | to 0.5. If the method is "checkerboard", the image_mix is set to 0. 1017 | :type image_mix: float, default: None 1018 | :param checkerboard: Forces the checkerboard mixing of fixed and moving 1019 | images for the cyan-magenta and blend methods. The rendered image has 2 1020 | components, each component reverses which image is sampled for each 1021 | checkerboard box. 1022 | :type checkerboard: bool, default: None 1023 | :param pattern: The number of checkerboard boxes for each dimension. 1024 | :type pattern: tuple, default: None 1025 | :param swap_image_order: Reverses which image is sampled for each 1026 | checkerboard box. This simply toggles image_mix between 0 and 1. 1027 | :type swap_image_order: bool, default: None 1028 | """ 1029 | global _cell_watcher 1030 | # image args may be image name or image object 1031 | fixed_name = 'Fixed' 1032 | if isinstance(fixed_image, str): 1033 | fixed_name = fixed_image 1034 | else: 1035 | self.set_image(fixed_image, fixed_name) 1036 | moving_name = 'Moving' 1037 | if isinstance(moving_image, str): 1038 | moving_name = moving_image 1039 | else: 1040 | self.set_image(moving_image, moving_name) 1041 | options = {} 1042 | # if None let viewer use defaults or last value. 1043 | if method is not None: 1044 | options['method'] = method 1045 | if image_mix is not None: 1046 | options['imageMix'] = image_mix 1047 | if checkerboard is not None: 1048 | options['checkerboard'] = checkerboard 1049 | if pattern is not None: 1050 | options['pattern'] = pattern 1051 | if swap_image_order is not None: 1052 | options['swapImageOrder'] = swap_image_order 1053 | self.queue_request('compareImages', fixed_name, moving_name, options) 1054 | _cell_watcher and _cell_watcher.update_viewer_status(self.name, False) 1055 | 1056 | @fetch_value 1057 | def set_label_image(self, label_image: Image) -> None: 1058 | """Set the label image to be rendered. Queue the function to be run in 1059 | the background thread once the plugin API is available. 1060 | 1061 | :param label_image: The label map to visualize 1062 | :type label_image: Image 1063 | """ 1064 | global _cell_watcher 1065 | render_type = _detect_render_type(label_image, 'image') 1066 | if render_type is RenderType.IMAGE: 1067 | label_image = _get_viewer_image(label_image, label=True) 1068 | self.stores['LabelImage'] = label_image 1069 | if ENVIRONMENT is Env.HYPHA: 1070 | self.label_image = label_image 1071 | svc_name = f"{self.workspace}/itkwidgets-server:data-set" 1072 | svc = self.server.get_service(svc_name) 1073 | svc.set_label_or_image('label_image') 1074 | else: 1075 | self.queue_request('setLabelImage', label_image) 1076 | _cell_watcher and _cell_watcher.update_viewer_status(self.name, False) 1077 | elif render_type is RenderType.POINT_SET: 1078 | label_image = _get_viewer_point_set(label_image) 1079 | self.queue_request('setPointSets', label_image) 1080 | @fetch_value 1081 | async def get_label_image(self) -> NgffImage: 1082 | """Get the full, highest resolution label image. 1083 | 1084 | :return: label_image 1085 | :rtype: NgffImage 1086 | """ 1087 | if store := self.stores.get('LabelImage'): 1088 | multiscales = from_ngff_zarr(store) 1089 | loaded_image = multiscales.images[0] 1090 | roi_data = loaded_image.data 1091 | return to_ngff_image( 1092 | roi_data, 1093 | dims=loaded_image.dims, 1094 | scale=loaded_image.scale, 1095 | name='LabelImage', 1096 | axes_units=loaded_image.axes_units 1097 | ) 1098 | raise ValueError(f'No label image data found.') 1099 | 1100 | @fetch_value 1101 | def set_label_image_blend(self, blend: float) -> None: 1102 | """Set the label map blend with intensity image. Queue the function to 1103 | be run in the background thread once the plugin API is available. 1104 | 1105 | :param blend: Blend with intensity image, from 0.0 to 1.0. default: 0.5 1106 | :type blend: float 1107 | """ 1108 | self.queue_request('setLabelImageBlend', blend) 1109 | @fetch_value 1110 | async def get_label_image_blend(self) -> asyncio.Future | float: 1111 | """Get the label map blend with intensity image. 1112 | 1113 | :return: The future for the coroutine, to be updated with the blend 1114 | with the intensity image. 1115 | :rtype: asyncio.Future | float 1116 | """ 1117 | return await self.viewer_rpc.itk_viewer.getLabelImageBlend() 1118 | 1119 | @fetch_value 1120 | def set_label_image_label_names(self, names: List[str]) -> None: 1121 | """Set the string names associated with the integer label values. Queue 1122 | the function to be run in the background thread once the plugin API is 1123 | available. 1124 | 1125 | :param names: A list of names for each label map. 1126 | :type names: List[str] 1127 | """ 1128 | self.queue_request('setLabelImageLabelNames', names) 1129 | @fetch_value 1130 | async def get_label_image_label_names(self) -> asyncio.Future | List[str]: 1131 | """Get the string names associated with the integer label values. 1132 | 1133 | :return: The future for the coroutine, to be updated with the list of 1134 | names for each label map. 1135 | :rtype: asyncio.Future | List[str] 1136 | """ 1137 | return await self.viewer_rpc.itk_viewer.getLabelImageLabelNames() 1138 | 1139 | @fetch_value 1140 | def set_label_image_lookup_table(self, lookup_table: str) -> None: 1141 | """Set the lookup table for the label map. Queue the function to be run 1142 | in the background thread once the plugin API is available. 1143 | 1144 | :param lookup_table: Label map lookup table. default: 'glasbey' 1145 | :type lookup_table: str 1146 | """ 1147 | self.queue_request('setLabelImageLookupTable', lookup_table) 1148 | @fetch_value 1149 | async def get_label_image_lookup_table(self) -> asyncio.Future | str: 1150 | """Get the lookup table for the label map. 1151 | 1152 | :return: The future for the coroutine, to be updated with the current 1153 | label map lookup table. 1154 | :rtype: asyncio.Future | str 1155 | """ 1156 | return await self.viewer_rpc.itk_viewer.getLabelImageLookupTable() 1157 | 1158 | @fetch_value 1159 | def set_label_image_weights(self, weights: float) -> None: 1160 | """Set the rendering weight assigned to current label. Queue the 1161 | function to be run in the background thread once the plugin API is 1162 | available. 1163 | 1164 | :param weights: Assign the current label rendering weight between 1165 | [0.0, 1.0]. 1166 | :type weights: float 1167 | """ 1168 | self.queue_request('setLabelImageWeights', weights) 1169 | @fetch_value 1170 | async def get_label_image_weights(self) -> asyncio.Future | float: 1171 | """Get the rendering weight assigned to current label. 1172 | 1173 | :return: The future for the coroutine, to be updated with the current 1174 | label rendering weight. 1175 | :rtype: asyncio.Future | float 1176 | """ 1177 | return await self.viewer_rpc.itk_viewer.getLabelImageWeights() 1178 | 1179 | @fetch_value 1180 | def select_layer(self, name: str) -> None: 1181 | """Set the layer identified by `name` as the current layer. Queue the 1182 | function to be run in the background thread once the plugin API is 1183 | available. 1184 | 1185 | :param name: The name of thelayer to select. 1186 | :type name: str 1187 | """ 1188 | self.queue_request('selectLayer', name) 1189 | @fetch_value 1190 | async def get_layer_names(self) -> asyncio.Future | List[str]: 1191 | """Get the list of all layer names. 1192 | 1193 | :return: The future for the coroutine, to be updated with the list of 1194 | layer names. 1195 | :rtype: asyncio.Future | List[str] 1196 | """ 1197 | return await self.viewer_rpc.itk_viewer.getLayerNames() 1198 | 1199 | @fetch_value 1200 | def set_layer_visibility(self, visible: bool, name: str) -> None: 1201 | """Set whether the layer is visible. Queue the function to be run in 1202 | the background thread once the plugin API is available. 1203 | 1204 | :param visible: Layer visibility. default: True 1205 | :type visible: bool 1206 | :param name: The name of the layer. 1207 | :type name: str 1208 | """ 1209 | self.queue_request('setLayerVisibility', visible, name) 1210 | @fetch_value 1211 | async def get_layer_visibility(self, name: str) -> asyncio.Future | bool: 1212 | """Get whether the layer is visible. 1213 | 1214 | :param name: The name of the layer to fetch the visibility for. 1215 | :type name: str 1216 | :return: The future for the coroutine, to be updated with the layer 1217 | visibility. 1218 | :rtype: asyncio.Future | bool 1219 | """ 1220 | return await self.viewer_rpc.itk_viewer.getLayerVisibility(name) 1221 | 1222 | @fetch_value 1223 | def get_loaded_image_names(self) -> List[str]: 1224 | """Get the list of loaded image names. 1225 | 1226 | :return: List of loaded images. 1227 | :rtype: List[str] 1228 | """ 1229 | return list(self.stores.keys()) 1230 | 1231 | @fetch_value 1232 | def add_point_set(self, point_set: PointSet) -> None: 1233 | """Add a point set to the visualization. Queue the function to be run 1234 | in the background thread once the plugin API is available. 1235 | 1236 | :param point_set: An array of points to visualize. 1237 | :type point_set: PointSet 1238 | """ 1239 | point_set = _get_viewer_point_set(point_set) 1240 | self.queue_request('addPointSet', point_set) 1241 | @fetch_value 1242 | def set_point_set(self, point_set: PointSet) -> None: 1243 | """Set the point set to the visualization. Queue the function to be run 1244 | in the background thread once the plugin API is available. 1245 | 1246 | :param point_set: An array of points to visualize. 1247 | :type point_set: PointSet 1248 | """ 1249 | point_set = _get_viewer_point_set(point_set) 1250 | self.queue_request('setPointSets', point_set) 1251 | 1252 | @fetch_value 1253 | def set_rendering_view_container_style(self, container_style: Style) -> None: 1254 | """Set the CSS style for the rendering view `div`'s. Queue the function 1255 | to be run in the background thread once the plugin API is available. 1256 | 1257 | :param container_style: A dict of string keys and sting values 1258 | representing the desired CSS styling. 1259 | :type container_style: Style 1260 | """ 1261 | self.queue_request('setRenderingViewContainerStyle', container_style) 1262 | @fetch_value 1263 | async def get_rendering_view_container_style(self) -> Style: 1264 | """Get the CSS style for the rendering view `div`'s. 1265 | 1266 | :return: The future for the coroutine, to be updated with a dict of 1267 | string keys and sting values representing the desired CSS styling. 1268 | :rtype: Style 1269 | """ 1270 | return await self.viewer_rpc.itk_viewer.getRenderingViewStyle() 1271 | 1272 | @fetch_value 1273 | def set_rotate(self, enabled: bool) -> None: 1274 | """Set whether the camera should continuously rotate around the scene 1275 | in volume rendering mode. Queue the function to be run in the 1276 | background thread once the plugin API is available. 1277 | 1278 | :param enabled: Rotate the camera. default: False 1279 | :type enabled: bool 1280 | """ 1281 | self.queue_request('setRotateEnabled', enabled) 1282 | @fetch_value 1283 | async def get_rotate(self) -> bool: 1284 | """Get whether the camera is rotating. 1285 | 1286 | :return: The future for the coroutine, to be updated with the boolean 1287 | status. 1288 | :rtype: bool 1289 | """ 1290 | return await self.viewer_rpc.itk_viewer.getRotateEnabled() 1291 | 1292 | @fetch_value 1293 | def set_ui_collapsed(self, collapsed: bool) -> None: 1294 | """Collapse the native widget user interface. Queue the function to be 1295 | run in the background thread once the plugin API is available. 1296 | 1297 | :param collapsed: If the UI interface should be collapsed. default: True 1298 | :type collapsed: bool 1299 | """ 1300 | self.queue_request('setUICollapsed', collapsed) 1301 | @fetch_value 1302 | async def get_ui_collapsed(self) -> bool: 1303 | """Get the collapsed status of the UI interface. 1304 | 1305 | :return: The future for the coroutine, to be updated with the collapsed 1306 | state of the UI interface. 1307 | :rtype: bool 1308 | """ 1309 | return await self.viewer_rpc.itk_viewer.getUICollapsed() 1310 | 1311 | @fetch_value 1312 | def set_units(self, units: str) -> None: 1313 | """Set the units to display in the scale bar. Queue the function to be 1314 | run in the background thread once the plugin API is available. 1315 | 1316 | :param units: Units to use. 1317 | :type units: str 1318 | """ 1319 | self.queue_request('setUnits', units) 1320 | @fetch_value 1321 | async def get_units(self) -> str: 1322 | """Get the units to display in the scale bar. 1323 | 1324 | :return: The future for the coroutine, to be updated with the units 1325 | used in the scale bar. 1326 | :rtype: str 1327 | """ 1328 | return await self.viewer_rpc.itk_viewer.getUnits() 1329 | 1330 | @fetch_value 1331 | def set_view_mode(self, mode: str) -> None: 1332 | """Set the viewing mode. Queue the function to be run in the background 1333 | thread once the plugin API is available. 1334 | 1335 | :param mode: View mode. One of the following: 'XPlane', 'YPlane', 1336 | 'ZPlane', or 'Volume'. default: 'Volume' 1337 | :type mode: str 1338 | """ 1339 | self.queue_request('setViewMode', mode) 1340 | @fetch_value 1341 | async def get_view_mode(self) -> str: 1342 | """Get the current view mode. 1343 | 1344 | :return: The future for the coroutine, to be updated with the view mode. 1345 | :rtype: str 1346 | """ 1347 | return await self.viewer_rpc.itk_viewer.getViewMode() 1348 | 1349 | @fetch_value 1350 | def set_x_slice(self, position: float) -> None: 1351 | """Set the position in world space of the X slicing plane. Queue the 1352 | function to be run in the background thread once the plugin API is 1353 | available. 1354 | 1355 | :param position: Position in world space. 1356 | :type position: float 1357 | """ 1358 | self.queue_request('setXSlice', position) 1359 | @fetch_value 1360 | async def get_x_slice(self) -> float: 1361 | """Get the position in world space of the X slicing plane. 1362 | 1363 | :return: The future for the coroutine, to be updated with the position 1364 | in world space. 1365 | :rtype: float 1366 | """ 1367 | return await self.viewer_rpc.itk_viewer.getXSlice() 1368 | 1369 | @fetch_value 1370 | def set_y_slice(self, position: float) -> None: 1371 | """Set the position in world space of the Y slicing plane. Queue the 1372 | function to be run in the background thread once the plugin API is 1373 | available. 1374 | 1375 | :param position: Position in world space. 1376 | :type position: float 1377 | """ 1378 | self.queue_request('setYSlice', position) 1379 | @fetch_value 1380 | async def get_y_slice(self) -> float: 1381 | """Get the position in world space of the Y slicing plane. 1382 | 1383 | :return: The future for the coroutine, to be updated with the position 1384 | in world space. 1385 | :rtype: float 1386 | """ 1387 | return await self.viewer_rpc.itk_viewer.getYSlice() 1388 | 1389 | @fetch_value 1390 | def set_z_slice(self, position: float) -> None: 1391 | """Set the position in world space of the Z slicing plane. Queue the 1392 | function to be run in the background thread once the plugin API is 1393 | available. 1394 | 1395 | :param position: Position in world space. 1396 | :type position: float 1397 | """ 1398 | self.queue_request('setZSlice', position) 1399 | @fetch_value 1400 | async def get_z_slice(self) -> float: 1401 | """Get the position in world space of the Z slicing plane. 1402 | 1403 | :return: The future for the coroutine, to be updated with the position 1404 | in world space. 1405 | :rtype: float 1406 | """ 1407 | return await self.viewer_rpc.itk_viewer.getZSlice() 1408 | 1409 | 1410 | def view(data=None, **kwargs): 1411 | """View the image and/or point set. 1412 | 1413 | Creates and returns an ImJoy plugin ipywidget to visualize an image, and/or 1414 | point set. 1415 | 1416 | The image can be 2D or 3D. The type of the image can be an numpy.array, 1417 | itkwasm.Image, itk.Image, additional NumPy-arraylike's, such as a dask.Array, 1418 | or vtk.vtkImageData. 1419 | 1420 | A point set can be visualized. The type of the point set can be an 1421 | numpy.array (Nx3 array of point positions). 1422 | 1423 | Parameters 1424 | ---------- 1425 | 1426 | ### General Interface 1427 | 1428 | :param ui_collapsed: Collapse the native widget user interface. default: True 1429 | :type ui_collapsed: bool 1430 | 1431 | :param rotate: Continuously rotate the camera around the scene in volume rendering mode. default: False 1432 | :type rotate: bool 1433 | 1434 | :param annotations: Display annotations describing orientation and the value of a mouse-position-based data probe. default: True 1435 | :type annotations: bool 1436 | 1437 | :param axes: Display axes. default: False 1438 | :type axes: bool 1439 | 1440 | :param bg_color: Background color. default: based on the current Jupyter theme 1441 | :type bg_color: (red, green, blue) tuple, components from 0.0 to 1.0 1442 | 1443 | :param container_style: The CSS style for the rendering view `div`'s. 1444 | :type container_style: dict 1445 | 1446 | ### Images 1447 | 1448 | :param image: The image to visualize. 1449 | :type image: array_like, itk.Image, or vtk.vtkImageData 1450 | 1451 | :param label_image: The label map to visualize. If an image is also provided, the label map must have the same size. 1452 | :type label_image: array_like, itk.Image, or vtk.vtkImageData 1453 | 1454 | :param label_blend: Label map blend with intensity image, from 0.0 to 1.0. default: 0.5 1455 | :type label_blend: float 1456 | 1457 | :param label_names: String names associated with the integer label values. 1458 | :type label_names: list of (label_value, label_name) 1459 | 1460 | :param label_lut: Lookup table for the label map. default: 'glasbey' 1461 | :type label_lut: string 1462 | 1463 | :param label_weights: The rendering weight assigned to current label. Values range from 0.0 to 1.0. 1464 | :type label_weights: float 1465 | 1466 | :param color_range: The [min, max] range of the data values mapped to colors for the given image component identified by name. 1467 | :type color_range: list, default: The [min, max] range of the data values 1468 | 1469 | :param vmin: Data values below vmin take the bottom color of the color map. 1470 | :type vmin: float 1471 | 1472 | :param vmax: Data values above vmax take the top color of the color map. 1473 | :type vmax: float 1474 | 1475 | :param color_bounds: The [min, max] range of the data values for color maps that provide a bounds for user inputs. 1476 | :type color_bounds: list, default: The [min, max] range of the data values 1477 | 1478 | :param cmap: The color map for the current component/channel. default: 'Grayscale' 1479 | :type cmap: string 1480 | 1481 | :param x_slice: The position in world space of the X slicing plane. 1482 | :type x_slice: float 1483 | 1484 | :param y_slice: The position in world space of the Y slicing plane. 1485 | :type y_slice: float 1486 | 1487 | :param z_slice: The position in world space of the Z slicing plane. 1488 | :type z_slice: float 1489 | 1490 | :param interpolation: Linear as opposed to nearest neighbor interpolation for image slices. Note: Interpolation is not currently supported with label maps. default: True 1491 | :type interpolation: bool 1492 | 1493 | :param gradient_opacity: Gradient opacity for composite volume rendering, in the range (0.0, 1.0]. default: 0.5 1494 | :type gradient_opacity: float 1495 | 1496 | :param gradient_opacity_scale: Gradient opacity scale for composite volume rendering, in the range (0.0, 1.0]. default: 0.5 1497 | :type gradient_opacity_scale: float 1498 | 1499 | :param piecewise_function_points: Volume rendering opacity transfer function parameters. Example points arg: [[.2, .1], [.8, .9]] 1500 | :type piecewise_function_points: list 1501 | 1502 | :param blend_mode: Volume rendering blend mode. Supported modes: 'Composite', 'Maximum', 'Minimum', 'Average'. default: 'Composite' 1503 | :type blend_mode: string 1504 | 1505 | :param component_visible: The given image intensity component index's visibility. default: True 1506 | :type component_visible: bool 1507 | 1508 | :param shadow_enabled: Whether to used gradient-based shadows in the volume rendering. default: True 1509 | :type shadow_enabled: bool 1510 | 1511 | :param view_mode: Only relevant for 3D scenes. Viewing mode: 'XPlane', 'YPlane', 'ZPlane', or 'Volume'. default: 'Volume' 1512 | :type view_mode: 'XPlane', 'YPlane', 'ZPlane', or 'Volume' 1513 | 1514 | :param layer: Select the layer identified by `name` in the user interface. 1515 | :type layer: string 1516 | 1517 | :param layer_visible: Whether the current layer is visible. default: True 1518 | :type layer_visible: bool 1519 | 1520 | ### Point Set 1521 | 1522 | :param point_set: The point set to visualize. 1523 | :type point_set: array_like 1524 | 1525 | Other Parameters 1526 | ---------------- 1527 | 1528 | :param sample_distance: Sampling distance for volume rendering, normalized from 0.0 to 1.0. Lower values result in a higher quality rendering. High values improve the framerate. default: 0.2 1529 | :type sample_distance: float 1530 | 1531 | :param units: Units to display in the scale bar. 1532 | :type units: string 1533 | 1534 | Returns 1535 | ------- 1536 | 1537 | :return: viewer, display by placing at the end of a Jupyter or Colab cell. Query or set properties on the object to change the visualization. 1538 | :rtype: Viewer 1539 | """ 1540 | viewer = Viewer(data=data, **kwargs) 1541 | 1542 | return viewer 1543 | 1544 | 1545 | def compare_images( 1546 | fixed_image: Union[str, Image], 1547 | moving_image: Union[str, Image], 1548 | method: str = None, 1549 | image_mix: float = None, 1550 | checkerboard: bool = None, 1551 | pattern: Union[Tuple[int, int], Tuple[int, int, int]] = None, 1552 | swap_image_order: bool = None, 1553 | **kwargs, 1554 | ): 1555 | """Fuse 2 images with a checkerboard filter or as a 2 component image. 1556 | 1557 | The moving image is re-sampled to the fixed image space. Set a keyword argument to None to use defaults based on method. 1558 | 1559 | :param fixed_image: Static image the moving image is re-sampled to. For non-checkerboard methods ('blend', 'green-magenta', etc.), the fixed image is on the first component. 1560 | :type fixed_image: array_like, itk.Image, or vtk.vtkImageData 1561 | 1562 | :param moving_image: Image is re-sampled to the fixed_image. For non-checkerboard methods ('blend', 'green-magenta', etc.), the moving image is on the second component. 1563 | :type moving_image: array_like, itk.Image, or vtk.vtkImageData 1564 | 1565 | :param method: The checkerboard method picks pixels from the fixed and moving image to create a checkerboard pattern. Setting the method to checkerboard turns on the checkerboard flag. The non-checkerboard methods ('blend', 'green-magenta', etc.) put the fixed image on component 0, moving image on component 1. The 'green-magenta' and 'red-cyan' change the color maps so matching images are grayish white. The 'cyan-magenta' color maps produce a purple if the images match. 1566 | :type method: string, default: None, possible values: 'green-magenta', 'cyan-red', 'cyan-magenta', 'blend', 'checkerboard', 'disabled' 1567 | 1568 | :param image_mix: Changes the percent contribution the fixed vs moving image makes to the render by modifying the opacity transfer function. Value of 1 means max opacity for moving image, 0 for fixed image. If value is None and the method is not checkerboard, the image_mix is set to 0.5. If the method is "checkerboard", the image_mix is set to 0. 1569 | :type image_mix: float, default: None 1570 | 1571 | :param checkerboard: Forces the checkerboard mixing of fixed and moving images for the cyan-magenta and blend methods. The rendered image has 2 components, each component reverses which image is sampled for each checkerboard box. 1572 | :type checkerboard: bool, default: None 1573 | 1574 | :param pattern: The number of checkerboard boxes for each dimension. 1575 | :type pattern: tuple, default: None 1576 | 1577 | :param swap_image_order: Reverses which image is sampled for each checkerboard box. This simply toggles image_mix between 0 and 1. 1578 | :type swap_image_order: bool, default: None 1579 | 1580 | :return: viewer, display by placing at the end of a Jupyter or Colab cell. Query or set properties on the object to change the visualization. 1581 | :rtype: Viewer 1582 | """ 1583 | options = {} 1584 | # if None let viewer use defaults or last value. 1585 | if method is not None: 1586 | options['method'] = method 1587 | if image_mix is not None: 1588 | options['imageMix'] = image_mix 1589 | if checkerboard is not None: 1590 | options['checkerboard'] = checkerboard 1591 | if pattern is not None: 1592 | options['pattern'] = pattern 1593 | if swap_image_order is not None: 1594 | options['swapImageOrder'] = swap_image_order 1595 | 1596 | viewer = Viewer(data=None, image=moving_image, fixed_image=fixed_image, compare=options, **kwargs) 1597 | return viewer 1598 | 1599 | -------------------------------------------------------------------------------- /itkwidgets/viewer_config.py: -------------------------------------------------------------------------------- 1 | ITK_VIEWER_SRC = ( 2 | "https://bafybeic2mehlnsf3s44outegrue6zhfeywc6wajpbqfhw442yc42jsmmwy.ipfs.dweb.link/" 3 | ) 4 | PYDATA_SPHINX_HREF = "https://cdn.jsdelivr.net/npm/itk-viewer-bootstrap-ui@0.30.0/dist/bootstrapUIMachineOptions.js.es.js" 5 | MUI_HREF = "https://cdn.jsdelivr.net/npm/itk-viewer-material-ui@0.3.0/dist/materialUIMachineOptions.js.es.js" 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "itkwidgets" 7 | authors = [{name = "Matt McCormick", email = "matt.mccormick@kitware.com"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | dynamic = ["version",] 11 | description = "An elegant Python interface for visualization on the web platform to interactively generate insights into multidimensional images, point sets, and geometry." 12 | classifiers = [ 13 | "License :: OSI Approved :: Apache Software License", 14 | "Programming Language :: Python", 15 | 'Development Status :: 3 - Alpha', 16 | 'Framework :: IPython', 17 | 'Intended Audience :: Developers', 18 | 'Intended Audience :: Science/Research', 19 | 'Topic :: Multimedia :: Graphics', 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.8', 22 | 'Programming Language :: Python :: 3.9', 23 | 'Programming Language :: Python :: 3.10', 24 | 'Programming Language :: Python :: 3.11', 25 | ] 26 | keywords = [ 27 | "jupyter", 28 | "jupyterlab-extension", 29 | "widgets", 30 | "itk", 31 | "imaging", 32 | "visualization", 33 | "webgl", 34 | "webgpu", 35 | ] 36 | 37 | requires-python = ">=3.8" 38 | dependencies = [ 39 | "itkwasm >= 1.0b.178", 40 | "imjoy-elfinder", 41 | "imjoy-rpc >= 0.5.59", 42 | "imjoy-utils >= 0.1.2", 43 | "importlib_metadata", 44 | "ngff-zarr >= 0.8.7; sys_platform != \"emscripten\"", 45 | "ngff-zarr[dask-image] >= 0.8.7; sys_platform == \"emscripten\"", 46 | "numcodecs", 47 | "zarr < 3", 48 | ] 49 | 50 | [tool.hatch.version] 51 | source = "vcs" 52 | 53 | [tool.hatch.build] 54 | exclude = [ 55 | "/js/node_modules", 56 | "/examples", 57 | ] 58 | 59 | [project.urls] 60 | Home = "https://itkwidgets.readthedocs.io/en/latest/" 61 | Documentation = "https://itkwidgets.readthedocs.io/en/latest/" 62 | Source = "https://github.com/InsightSoftwareConsortium/itkwidgets" 63 | 64 | [project.optional-dependencies] 65 | all = [ 66 | "imjoy-jupyterlab-extension", 67 | "imjoy-elfinder[jupyter]", 68 | "aiohttp <4.0" 69 | ] 70 | lab = [ 71 | "imjoy-jupyterlab-extension", 72 | "imjoy-elfinder[jupyter]", 73 | "aiohttp <4.0" 74 | ] 75 | cli = [ 76 | "hypha >= 0.15.28", 77 | "imgcat", 78 | "IPython >= 8.4.0", 79 | "itk-io >= 5.3.0", 80 | "ngff-zarr[cli]", 81 | "playwright", 82 | ] 83 | 84 | notebook = [ 85 | "imjoy-jupyterlab-extension", 86 | "imjoy-elfinder[jupyter]", 87 | "notebook >= 7" 88 | ] 89 | test = [ 90 | "pytest >=2.7.3", 91 | "nbmake", 92 | "pip", 93 | ] 94 | doc = ["sphinx"] 95 | 96 | [project.scripts] 97 | itkwidgets = "itkwidgets.standalone_server:cli_entrypoint" 98 | 99 | [tool.pixi.project] 100 | channels = ["conda-forge"] 101 | platforms = ["linux-64"] 102 | 103 | [tool.pixi.pypi-dependencies] 104 | itkwidgets = { path = ".", editable = true } 105 | 106 | [tool.pixi.environments] 107 | default = { solve-group = "default" } 108 | all = { features = ["all"], solve-group = "default" } 109 | cli = { features = ["cli"], solve-group = "default" } 110 | doc = { features = ["doc"], solve-group = "default" } 111 | lab = { features = ["lab"], solve-group = "default" } 112 | notebook = { features = ["notebook"], solve-group = "default" } 113 | test = { features = ["test", "all", "cli", "lab", "notebook"], solve-group = "default" } 114 | 115 | [tool.pixi.tasks] 116 | 117 | [tool.pixi.feature.test.tasks] 118 | start = "jupyter lab examples" 119 | -------------------------------------------------------------------------------- /utilities/release-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | 6 | if len(sys.argv) < 2: 7 | print('Usage: ' + sys.argv[0] + ' ') 8 | sys.exit(1) 9 | output_file = sys.argv[1] 10 | 11 | with open(output_file, 'w') as fp: 12 | tags = subprocess.check_output(['git', 'tag', '--sort=creatordate']).decode('utf-8') 13 | recent_tags = tags.split()[-2:] 14 | previous_tag = recent_tags[0] 15 | current_tag = recent_tags[1] 16 | 17 | version = current_tag[1:] 18 | fp.write('# itkwidgets {0}\n\n'.format(version)) 19 | 20 | fp.write('`itkwidgets` provides an elegant Python interface for visualization on the web platform to interactively generate insights into multidimensional images, point sets, and geometry.\n\n') 21 | 22 | fp.write('## Installation\n\n') 23 | fp.write('```\n') 24 | fp.write('pip install itkwidgets\n') 25 | fp.write('```\n') 26 | fp.write('or\n') 27 | fp.write('```\n') 28 | fp.write('conda install -c conda-forge itkwidgets\n') 29 | fp.write('```\n') 30 | fp.write('\n') 31 | 32 | fp.write('## Changes from {0} to {1}\n'.format(previous_tag, current_tag)) 33 | subjects = subprocess.check_output(['git', 'log', '--pretty=%s:%h', 34 | '--no-merges', '{0}..{1}'.format(previous_tag, current_tag)]).decode('utf-8') 35 | 36 | bug_fixes = [] 37 | platform_fixes = [] 38 | doc_updates = [] 39 | enhancements = [] 40 | performance_improvements = [] 41 | style_changes = [] 42 | for subject in subjects.split('\n'): 43 | prefix = subject.split(':')[0] 44 | commit = subject.split(':')[-1] 45 | if prefix == 'BUG': 46 | description = subject.split(':')[1] 47 | bug_fixes.append((description, commit)) 48 | elif prefix == 'COMP': 49 | description = subject.split(':')[1] 50 | platform_fixes.append((description, commit)) 51 | elif prefix == 'DOC': 52 | description = subject.split(':')[1] 53 | doc_updates.append((description, commit)) 54 | elif prefix == 'ENH': 55 | description = subject.split(':')[1] 56 | enhancements.append((description, commit)) 57 | elif prefix == 'PERF': 58 | description = subject.split(':')[1] 59 | performance_improvements.append((description, commit)) 60 | elif prefix == 'STYLE': 61 | description = subject.split(':')[1] 62 | style_changes.append((description, commit)) 63 | 64 | commit_link_prefix = 'https://github.com/InsightSoftwareConsortium/itkwidgets/commit/' 65 | 66 | if enhancements: 67 | fp.write('\n### Enhancements\n\n') 68 | for subject, commit in enhancements: 69 | if subject.find('Bump itkwidgets version for development') != -1: 70 | continue 71 | fp.write('- {0}'.format(subject)) 72 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 73 | 74 | if performance_improvements: 75 | fp.write('\n### Performance Improvements\n\n') 76 | for subject, commit in performance_improvements: 77 | fp.write('- {0}'.format(subject)) 78 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 79 | 80 | if doc_updates: 81 | fp.write('\n### Documentation Updates\n\n') 82 | for subject, commit in doc_updates: 83 | fp.write('- {0}'.format(subject)) 84 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 85 | 86 | if platform_fixes: 87 | fp.write('\n### Platform Fixes\n\n') 88 | for subject, commit in platform_fixes: 89 | fp.write('- {0}'.format(subject)) 90 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 91 | 92 | if bug_fixes: 93 | fp.write('\n### Bug Fixes\n\n') 94 | for subject, commit in bug_fixes: 95 | fp.write('- {0}'.format(subject)) 96 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 97 | 98 | if style_changes: 99 | fp.write('\n### Style Changes\n\n') 100 | for subject, commit in style_changes: 101 | fp.write('- {0}'.format(subject)) 102 | fp.write(' ([{0}]({1}{0}))\n'.format(commit, commit_link_prefix)) 103 | --------------------------------------------------------------------------------