├── .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 | [](https://itkwidgets.readthedocs.io/en/latest/?badge=latest)
4 | [](https://github.com/InsightSoftwareConsortium/itkwidgets/actions/workflows/notebook-test.yml)
5 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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: [](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: [](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 | 
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 | 
17 |
18 | 
19 |
20 |
21 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
196 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | "[](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/HEAD?labpath=examples%2FEnvironmentCheck.ipynb)\n",
11 | "[](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 | "[](https://mybinder.org/v2/gh/InsightSoftwareConsortium/itkwidgets/HEAD?labpath=examples%2Fintegrations%2FPyImageJ%2FImageJImgLib2.ipynb)\n",
13 | "[](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 |
--------------------------------------------------------------------------------