├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── sphinx.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── biome.json ├── build.js ├── build_assets ├── $itksnap.json └── $slicer3d.json ├── docs ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── architecture.rst │ ├── conf.py │ ├── contributing.rst │ ├── enums.rst │ ├── index.rst │ ├── install.rst │ ├── options.rst │ └── traits.rst ├── environment.yml ├── examples ├── add_from_frontend.ipynb ├── additive_voxels.ipynb ├── afni.ipynb ├── alphathreshold.ipynb ├── atlas.ipynb ├── atlas.sparse.ipynb ├── basic_multiplanar.ipynb ├── colormaps.ipynb ├── colormaps.mesh.ipynb ├── complex.ipynb ├── denoise.ipynb ├── document.load.ipynb ├── draw.ui.ipynb ├── draw2.ipynb ├── example_4d.ipynb ├── example_sideview.ipynb ├── mask.ipynb ├── mesh.4d.ipynb ├── mesh.matcap.ipynb ├── mesh_and_volume.ipynb ├── mesh_layers.ipynb ├── modulatescalar.ipynb ├── mosaic.ipynb ├── mosaics1.ipynb ├── mosaics2.ipynb ├── on_event.ipynb ├── prototypes │ ├── meshes_(GIfTI, FreeSurfer, MZ3, OBJ, STL, legacy VTK).ipynb │ ├── torso_regions.ipynb │ └── trajectory.ipynb ├── saving.ipynb ├── segment.ipynb ├── sync.bidirectional.ipynb ├── test_images.ipynb ├── tracts.ipynb ├── vox.tradeoffs.ipynb ├── widgets.ipynb ├── worldspace.ipynb └── worldspace2.ipynb ├── js ├── lib.ts ├── mesh.ts ├── types.ts ├── types │ ├── niivue-fix.d.ts │ └── niivue-min.d.ts ├── volume.ts └── widget.ts ├── original_gallery.md ├── package.json ├── pyproject.toml ├── scripts └── generate_config_options.py ├── src └── ipyniivue │ ├── __init__.py │ ├── config_options.py │ ├── constants.py │ ├── download_dataset.py │ ├── serializers.py │ ├── traits.py │ ├── utils.py │ └── widget.py ├── tests └── test_ipyniivue.py └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | # Maintain dependencies for npm 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | 15 | # Maintain dependencies for Composer 16 | - package-ecosystem: "pip" 17 | directory: "/" 18 | schedule: 19 | interval: "monthly" 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Lint: 13 | runs-on: macos-14 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: actions/setup-python@v6 17 | with: 18 | python-version: "3.12" 19 | # Run Ruff 20 | - name: Run Ruff 21 | run: | 22 | pip install uv 23 | uv pip install --system ruff 24 | ruff check 25 | # Run Codespell 26 | - name: Run Codespell 27 | run: | 28 | pip install codespell 29 | codespell codespell src/**/*.py 30 | 31 | Test: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 36 | os: 37 | - ubuntu-latest 38 | - macos-latest # arm64 (Apple Silicon) 39 | - macos-13 # latest Intel release 40 | - windows-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v5 44 | # requires npm because JS needs to be built 45 | - uses: actions/setup-node@v5 46 | with: 47 | node-version: "18.x" 48 | - uses: actions/setup-python@v6 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | - run: | 52 | pip install pytest-notebook 53 | pip install pre-commit 54 | pre-commit run --all 55 | pre-commit install 56 | # pipx install hatch 57 | # hatch run test --cover --nb-test-files 58 | - name: Install IPyNiiVue & Deps 59 | run: pip install -e . 60 | - name: Test import 61 | run: | 62 | python -c "import ipyniivue" 63 | - name: Test IPyNiiVue 64 | run: | 65 | pip install pytest-cov 66 | pip install --upgrade pip ipython ipykernel 67 | ipython kernel install --name "python3" --user 68 | mkdir -p ./reports 69 | pytest --cov --cov-report=json:./reports/coverage.json --cov-report=xml:./reports/coverage.xml 70 | - name: Upload ipyniivue test coverage report 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: coverage_ipyniivue-${{ matrix.os }}-py${{ matrix.python-version }} 74 | path: ./reports/coverage.json 75 | - name: Generate Markdown summary of code coverage 76 | uses: irongut/CodeCoverageSummary@v1.3.0 77 | with: 78 | filename: ./reports/coverage.xml 79 | format: markdown 80 | output: both 81 | thresholds: "10 10" 82 | - name: Add test results to job summary 83 | run: | 84 | cat ./code-coverage-results.md >> $GITHUB_STEP_SUMMARY 85 | - name: Test example notebooks 86 | run: | 87 | pip install nbmake 88 | rm -rf ./reports 89 | mkdir -p ./reports 90 | pytest --nbmake examples/*ipynb --cov=src --cov-report=json:./reports/coverage.json --cov-report=xml:./reports/coverage.xml 91 | - name: Upload ipyniivue example notbooks coverage report 92 | uses: actions/upload-artifact@v4 93 | with: 94 | name: coverage_ipyniivue_notebooks-${{ matrix.os }}-py${{ matrix.python-version }} 95 | path: ./reports/coverage.json 96 | - name: Generate Markdown summary of code coverage 97 | uses: irongut/CodeCoverageSummary@v1.3.0 98 | with: 99 | filename: ./reports/coverage.xml 100 | format: markdown 101 | output: both 102 | thresholds: "10 10" 103 | - name: Add test results to job summary 104 | run: | 105 | cat ./code-coverage-results.md >> $GITHUB_STEP_SUMMARY 106 | # coverage run -m pytest --nb-test-files 107 | # nbt-test-files check for changes in the output of the notebook. 108 | # This does not work for our purpose since we wipe the output 109 | # before committing changes to facilitate integration with git. 110 | # - name: Upload coverage to codecov (Only do this for the ubuntu-latest job) 111 | # if: matrix.os == 'ubuntu-latest' 112 | # uses: codecov/codecov-action@v5 113 | # with: 114 | # token: ${{secrets.CODECOV_TOKEN}} 115 | # 116 | LintJavaScript: 117 | name: JavaScript / Lint 118 | runs-on: macos-14 119 | steps: 120 | - uses: actions/checkout@v5 121 | - uses: biomejs/setup-biome@v2 122 | with: 123 | version: 1.9.4 124 | - run: biome ci . 125 | 126 | TypecheckJavaScript: 127 | name: JavaScript / Typecheck 128 | runs-on: macos-14 129 | steps: 130 | - uses: actions/checkout@v5 131 | - uses: actions/setup-node@v5 132 | with: 133 | node-version: "22.x" 134 | - run: | 135 | npm install 136 | npm run typecheck 137 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | 10 | Release: 11 | runs-on: ubuntu-latest 12 | environment: 13 | name: pypi 14 | url: https://pypi.org/p/ipyniivue 15 | permissions: 16 | # IMPORTANT: this permission is mandatory for trusted publishing 17 | id-token: write 18 | # For generating GitHub Releases + Release notes 19 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 20 | contents: write 21 | steps: 22 | - uses: actions/checkout@v5 23 | with: 24 | fetch-depth: 0 25 | 26 | # requires Node to build JS 27 | - uses: actions/setup-node@v5 28 | with: 29 | node-version: "18.x" 30 | 31 | - uses: actions/setup-python@v6 32 | with: 33 | python-version: "3.x" 34 | 35 | - run: | 36 | pipx install hatch 37 | hatch build 38 | 39 | - name: Publish distribution 📦 to PyPI 40 | uses: pypa/gh-action-pypi-publish@release/v1 41 | 42 | - run: npx changelogithub@0.12 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/sphinx.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v5 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v6 19 | with: 20 | python-version: '3.12' 21 | 22 | - name: Install Hatch 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install hatch 26 | 27 | - name: Set up Hatch environment and install dependencies 28 | run: | 29 | hatch env create 30 | 31 | - name: Build documentation with Hatch 32 | run: | 33 | hatch run docs 34 | 35 | - name: Deploy to GitHub Pages 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: docs/build/html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .venv 3 | dist 4 | images 5 | docs/build 6 | package-lock.json 7 | 8 | # Python 9 | __pycache__ 10 | .ipynb_checkpoints 11 | 12 | src/ipyniivue/static 13 | 14 | launch.json 15 | 16 | # OS Specific 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/srstevenson/nb-clean 3 | rev: 4.0.1 4 | hooks: 5 | - id: nb-clean 6 | args: 7 | - --remove-empty-cells 8 | - -- 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2025, Niivue 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipyniivue 2 | 3 | [![PyPI Version](https://badge.fury.io/py/ipyniivue.svg)](https://badge.fury.io/py/ipyniivue) 4 | [![License](https://img.shields.io/github/license/niivue/ipyniivue)](https://opensource.org/license/bsd-2-clause) 5 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/niivue/ipyniivue/main?urlpath=lab%2Ftree%2Fexamples) 6 | 7 | **A Jupyter Widget for [Niivue](https://github.com/niivue/niivue) based on [anywidget](https://github.com/manzt/anywidget).** 8 | 9 | --- 10 | 11 | 12 | ## Installation 13 | 14 | Install ipyniivue using `pip`: 15 | 16 | ```sh 17 | pip install ipyniivue 18 | ``` 19 | 20 | --- 21 | 22 | ## Usage 23 | 24 | In a Jupyter environment: 25 | 26 | ```python 27 | from ipyniivue import NiiVue 28 | 29 | nv = NiiVue() 30 | nv.load_volumes([{"path": "images/mni152.nii.gz"}]) 31 | nv 32 | ``` 33 | 34 | This will render an interactive Niivue widget within your notebook. 35 | 36 | **See the [basic demo](./examples/basic_multiplanar.ipynb) to learn more.** 37 | 38 | --- 39 | 40 | ## Documentation 41 | 42 | See the [Documentation](https://niivue.github.io/ipyniivue) for usage. 43 | 44 | 45 | --- 46 | 47 | ## Development 48 | 49 | ipyniivue uses the recommended [`hatchling`](https://packaging.python.org/en/latest/flow/#using-hatch) build system, which is convenient to use via the [`hatch` CLI](https://hatch.pypa.io/latest/). We recommend installing `hatch` globally (e.g., via `pipx`) and running the various commands defined within `pyproject.toml`. `hatch` will take care of creating and synchronizing a virtual environment with all dependencies defined in `pyproject.toml`. 50 | 51 | ### Command Cheat Sheet 52 | 53 | Run these commands from the root of the project: 54 | 55 | | Command | Description | 56 | |------------------------|----------------------------------------------------------------------| 57 | | `hatch run format` | Format the project with `ruff format .` and apply linting with `ruff --fix .` | 58 | | `hatch run lint` | Lint the project with `ruff check .` | 59 | | `hatch run test` | Run unit tests with `pytest` | 60 | | `hatch run docs` | Build docs with `Sphinx` | 61 | 62 | Alternatively, you can manually create a virtual environment and manage installation and dependencies with `pip`: 63 | 64 | ```sh 65 | python3 -m venv .venv && source .venv/bin/activate 66 | pip install -e ".[dev]" 67 | ``` 68 | 69 | ### Making Changes to the JavaScript Code 70 | 71 | This is an [anywidget](https://github.com/manzt/anywidget) project, meaning the codebase is a hybrid of Python and JavaScript. The JavaScript code resides under the `js/` directory and uses [esbuild](https://esbuild.github.io/) for bundling. Whenever you make changes to the JavaScript code, you need to rebuild the files under `src/ipyniivue/static`. 72 | 73 | You have two options: 74 | 75 | 1. **Build Once**: Build the JavaScript code one time: 76 | 77 | ```sh 78 | npm run build 79 | ``` 80 | 81 | 2. **Start Development Server**: Start a development server that automatically rebuilds the code as you make changes: 82 | 83 | ```sh 84 | npm run dev 85 | ``` 86 | 87 | We recommend this approach for a smoother development experience. 88 | 89 | **Working with Jupyter** 90 | 91 | Once the development server is running, you can start JupyterLab or Visual Studio Code to develop the widget. When you're finished, stop the development server with `Ctrl+C`. 92 | 93 | > **Note:** To have `anywidget` automatically apply changes as you work, set the environment variable `ANYWIDGET_HMR=1`. You can set this directly in a notebook cell: 94 | > 95 | > ```python 96 | > %env ANYWIDGET_HMR=1 97 | > ``` 98 | > or in the shell: 99 | > ```sh 100 | > export ANYWIDGET_HMR=1 101 | > ``` 102 | 103 | --- 104 | 105 | ## Release Process 106 | 107 | Releases are automated using GitHub Actions via the [`release.yml`](.github/workflows/release.yml) workflow. 108 | 109 | ### Steps to Create a New Release 110 | 111 | 1. **Commit Changes**: Ensure all your changes are committed. 112 | 113 | 2. **Create a Tag**: Create a new tag matching the pattern `v*`: 114 | 115 | ```sh 116 | git tag -a vX.X.X -m "vX.X.X" 117 | git push --follow-tags 118 | ``` 119 | 120 | 3. **Workflow Actions**: When triggered, the workflow will: 121 | 122 | - Publish the package to PyPI with the tag version. 123 | - Generate a changelog based on conventional commits. 124 | - Create a GitHub Release with the changelog. 125 | 126 | ### Changelog Generation 127 | 128 | - We generate a changelog for GitHub releases with [`antfu/changelogithub`](https://github.com/antfu/changelogithub). 129 | - Each changelog entry is grouped and rendered based on conventional commits. 130 | - It's recommended to follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. 131 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true 10 | } 11 | }, 12 | "vcs": { 13 | "enabled": true, 14 | "clientKind": "git", 15 | "useIgnoreFile": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import * as niivue from "@niivue/niivue"; 5 | import esbuild from "esbuild"; 6 | 7 | global.window = {}; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const generateColormapsPlugin = { 13 | name: "generate-colormaps", 14 | setup(build) { 15 | build.onEnd((result) => { 16 | if (result?.errors?.length === 0) { 17 | const nv = new niivue.Niivue(); 18 | const colormapNames = nv.colormaps(); 19 | 20 | const outputDir = path.join( 21 | __dirname, 22 | "src", 23 | "ipyniivue", 24 | "static", 25 | "colormaps", 26 | ); 27 | 28 | if (fs.existsSync(outputDir)) { 29 | fs.rmSync(outputDir, { recursive: true, force: true }); 30 | } 31 | 32 | fs.mkdirSync(outputDir, { recursive: true }); 33 | 34 | for (const name of colormapNames) { 35 | const data = nv.colormapFromKey(name); 36 | const jsonContent = JSON.stringify(data); 37 | const filePath = path.join(outputDir, `${name}.json`); 38 | 39 | fs.writeFileSync(filePath, jsonContent, "utf8"); 40 | } 41 | 42 | // colormaps for drawing 43 | fs.copyFileSync( 44 | path.join(__dirname, "build_assets", "$itksnap.json"), 45 | path.join(outputDir, "$itksnap.json"), 46 | ); 47 | fs.copyFileSync( 48 | path.join(__dirname, "build_assets", "$slicer3d.json"), 49 | path.join(outputDir, "$slicer3d.json"), 50 | ); 51 | 52 | console.log("Successfully generated colormaps."); 53 | } else { 54 | console.error("Build failed, colormaps not generated."); 55 | } 56 | }); 57 | }, 58 | }; 59 | 60 | const generateShaderNamesPlugin = { 61 | name: "generate-shader-names", 62 | setup(build) { 63 | build.onEnd((result) => { 64 | if (result?.errors?.length === 0) { 65 | const nv = new niivue.Niivue(); 66 | const shaderNames = nv.meshShaders.map((shader) => shader.Name); 67 | const shaderNamesTxtContent = shaderNames.join("\n"); 68 | 69 | const outputPath = path.join( 70 | __dirname, 71 | "src", 72 | "ipyniivue", 73 | "static", 74 | "meshShaderNames.txt", 75 | ); 76 | 77 | fs.writeFileSync(outputPath, shaderNamesTxtContent, "utf8"); 78 | console.log("Successfully generated meshShaderNames.txt."); 79 | } else { 80 | console.error("Build failed, meshShaderNames.txt not generated."); 81 | } 82 | }); 83 | }, 84 | }; 85 | 86 | async function build(isWatchMode) { 87 | const buildOptions = { 88 | entryPoints: ["js/widget.ts"], 89 | bundle: true, 90 | outfile: "src/ipyniivue/static/widget.js", 91 | plugins: [generateColormapsPlugin, generateShaderNamesPlugin], 92 | format: "esm", 93 | minify: true, 94 | }; 95 | 96 | if (isWatchMode) { 97 | const ctx = await esbuild.context(buildOptions); 98 | await ctx.watch(); 99 | console.log("Watching for changes..."); 100 | } else { 101 | await esbuild.build(buildOptions).catch(() => process.exit(1)); 102 | } 103 | } 104 | 105 | const isWatchMode = process.argv.includes("--watch"); 106 | 107 | build(isWatchMode).catch((e) => { 108 | console.error(e); 109 | process.exit(1); 110 | }); 111 | -------------------------------------------------------------------------------- /build_assets/$itksnap.json: -------------------------------------------------------------------------------- 1 | { 2 | "R": [ 3 | 0, 255, 0, 0, 255, 0, 255, 255, 0, 205, 210, 102, 0, 0, 46, 255, 106, 221, 4 | 233, 165, 255, 147, 218, 75, 255, 60, 255, 255, 218, 0, 188, 255, 255, 222, 5 | 127, 139, 124, 255, 70, 0, 238, 238, 240, 245, 184, 32, 255, 25, 112, 34, 6 | 248, 245, 255, 144, 173, 65, 255, 250, 128, 50, 244, 255, 123, 255, 173, 7 | 255, 127, 255, 143, 220, 253, 255, 0, 0, 128, 255, 250, 148, 178, 255, 135, 8 | 100, 240, 250, 255, 107, 135, 0, 139, 245, 186, 255, 255, 0, 210, 255, 47, 9 | 72, 175, 128, 176, 255, 139, 240, 255, 216, 119, 219, 72, 255, 199, 154, 10 | 189, 240, 230, 0, 85, 64, 153, 205, 250, 95, 0, 255, 224, 176, 138, 30, 240, 11 | 152, 160 12 | ], 13 | "G": [ 14 | 0, 0, 255, 0, 255, 255, 0, 239, 0, 133, 180, 205, 0, 139, 139, 228, 90, 160, 15 | 150, 42, 250, 112, 112, 0, 182, 179, 235, 228, 165, 128, 143, 105, 218, 184, 16 | 255, 69, 252, 255, 130, 100, 130, 232, 255, 222, 134, 178, 20, 25, 128, 139, 17 | 248, 255, 160, 238, 255, 105, 99, 240, 0, 205, 164, 255, 104, 165, 216, 192, 18 | 255, 140, 188, 20, 245, 250, 206, 255, 0, 250, 128, 0, 34, 127, 206, 149, 19 | 230, 235, 245, 142, 206, 0, 0, 245, 85, 228, 222, 191, 105, 248, 79, 61, 20 | 238, 128, 224, 240, 0, 255, 215, 191, 136, 112, 209, 0, 21, 205, 183, 248, 21 | 230, 250, 107, 224, 50, 92, 250, 158, 128, 69, 255, 196, 43, 144, 128, 251, 22 | 82 23 | ], 24 | "B": [ 25 | 0, 0, 0, 255, 0, 255, 255, 213, 205, 63, 140, 170, 128, 139, 87, 225, 205, 26 | 221, 122, 42, 250, 219, 214, 130, 193, 113, 205, 196, 32, 128, 143, 180, 27 | 185, 135, 0, 19, 0, 224, 180, 0, 238, 170, 240, 179, 11, 170, 147, 112, 144, 28 | 34, 255, 250, 122, 144, 47, 225, 71, 230, 0, 50, 96, 240, 238, 0, 230, 203, 29 | 212, 0, 143, 60, 230, 240, 209, 127, 128, 205, 114, 211, 34, 80, 235, 237, 30 | 140, 215, 238, 35, 250, 139, 139, 220, 211, 181, 173, 255, 30, 220, 79, 139, 31 | 238, 0, 230, 245, 0, 255, 0, 216, 153, 147, 204, 255, 133, 50, 107, 255, 32 | 250, 154, 47, 208, 204, 92, 210, 160, 0, 0, 255, 222, 226, 255, 128, 152, 45 33 | ], 34 | "A": [ 35 | 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 36 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 37 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 38 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 39 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 40 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 41 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 42 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 43 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 44 | ], 45 | "I": [ 46 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 47 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 48 | 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 49 | 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 50 | 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 51 | 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 52 | 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 53 | 128, 129, 130 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /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 = source 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 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /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=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 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/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. autoclass:: ipyniivue.NiiVue 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | .. autoclass:: ipyniivue.Volume 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | 14 | .. autoclass:: ipyniivue.Mesh 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | 19 | .. autoclass:: ipyniivue.MeshLayer 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/architecture.rst: -------------------------------------------------------------------------------- 1 | Architecture 2 | ============ 3 | 4 | IPyNiiVue is a widget that bridges WebGL-powered JavaScript visualization (Niivue) with Python in notebooks. This document contains an overview of how JavaScript interacts with Python in this library. 5 | 6 | System Overview 7 | --------------- 8 | 9 | - **Python Side**: Users interact with the ``NiiVue`` class to load and manipulate neuroimaging data 10 | - **JavaScript Side**: Handles WebGL rendering and user interactions in the browser 11 | - **Communication Layer**: Traitlets synchronize state between Python and JavaScript via WebSocket (JupyterLab) or HTTP (Marimo) 12 | 13 | Architecture Layers 14 | ------------------- 15 | 16 | 1. Python Backend (Kernel) 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | Runs in the notebook kernel process and contains: 20 | 21 | - ``NiiVue``, ``Volume``, ``Mesh``, and ``MeshLayer`` classes 22 | - State management through traitlets 23 | - Data processing and computational logic 24 | - Chunked data handling for large data from the frontend (to overcome Tornado's 10MB limit) 25 | 26 | 2. Widget Bridge Layer 27 | ~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | Provided by anywidget framework: 30 | 31 | - Maintains synchronized widget models between kernel and browser 32 | - Handles state synchronization via traitlets 33 | - Manages WebSocket/HTTP communication 34 | - Serializes/deserializes data between Python and JavaScript 35 | 36 | 3. JavaScript Frontend (Browser) 37 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 38 | 39 | Runs in the browser and includes: 40 | 41 | - WebGL rendering via Niivue.js library 42 | - UI event handling (mouse, keyboard interactions) 43 | - Visual output in notebook cells 44 | 45 | Data Flow 46 | --------- 47 | 48 | .. mermaid:: 49 | 50 | flowchart LR 51 | subgraph "Kernel" 52 | A[Python Backend
NiiVue class] 53 | end 54 | 55 | subgraph "Communication Layer" 56 | B[Widget Bridge
Traitlets sync
WebSocket/HTTP] 57 | end 58 | 59 | subgraph "Browser" 60 | C[JavaScript Frontend
WebGL rendering
User interactions] 61 | D[Notebook Output Cell
Visual display] 62 | end 63 | 64 | A <--> B 65 | B <--> C 66 | C <--> D 67 | 68 | Communication Patterns 69 | ~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | 1. **State Updates**: Property changes (opacity, colormap, etc.) sync automatically via traitlets 72 | 2. **Large Data Transfer**: Volume/mesh data transmitted in chunks to handle size limitations 73 | 3. **Efficient Updates**: Only array differences (indices + values) sent for existing data 74 | 4. **Event Handling**: User interactions in JS trigger Python callbacks via custom messages 75 | 76 | Some Implementation Details 77 | --------------------------- 78 | 79 | **JavaScript Bundle**: 80 | 81 | - Source: ``js/`` directory 82 | - Build: esbuild bundles to ``src/ipyniivue/static/widget.js`` 83 | - Main components: ``widget.ts``, ``volume.ts``, ``mesh.ts``, ``lib.ts`` 84 | 85 | **Python Components**: 86 | 87 | - ``widget.py``: Core NiiVue widget and data models 88 | - ``serializers.py``: Data conversion between Python and JavaScript 89 | - ``config_options.py``: Configuration management 90 | - ``traits.py``: Custom trait types for specialized data 91 | 92 | **Build System**: 93 | 94 | - ``build.js``: Generates colormaps and shader names during build 95 | - ``pyproject.toml``: Python packaging configuration -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder.""" 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.abspath("../../src")) 7 | 8 | # -- Project information ----------------------------------------------------- 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 10 | 11 | project = "IPyNiiVue" 12 | author = "Jan-Hendrik Müller, Trevor Manz, Bradley Alford, Anthony Androulakis, " 13 | author += "Taylor Hanayik, Christian O'Reilly" 14 | project_copyright = "2025, " + author 15 | release = "2.1.0" 16 | 17 | # -- General configuration --------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 19 | 20 | extensions = [ 21 | "sphinx.ext.autodoc", 22 | "sphinx.ext.viewcode", 23 | "sphinxcontrib.mermaid", 24 | "myst_parser", 25 | ] 26 | 27 | templates_path = ["_templates"] 28 | exclude_patterns = [] 29 | 30 | # Autodoc options 31 | autodoc_default_options = { 32 | "members": True, 33 | "undoc-members": False, 34 | "inherited-members": False, 35 | "special-members": "__init__", 36 | "show-inheritance": True, 37 | } 38 | # autodoc_member_order = "bysource" 39 | autoclass_content = "class" 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 43 | 44 | html_theme = "furo" 45 | # html_static_path = ["_static"] 46 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We are glad you are here! Contributions to this package are always welcome. 5 | Read on to learn more about the contribution process and package design. 6 | 7 | ipyniivue uses `the recommended `__ hatchling build-system, which is convenient to use via the `hatch CLI `__. We recommend installing hatch globally (e.g., via pipx) and running the various commands defined within pyproject.toml. hatch will take care of creating and synchronizing a virtual environment with all dependencies defined in pyproject.toml. 8 | 9 | Install Pre-Commit hooks 10 | ^^^^^^^^^^^^^^^^^^^^^^^^ 11 | We use `pre-commit `__ to run code checks and clear 12 | notebook outputs before committing changes. To install the pre-commit hooks, 13 | run the following command: 14 | 15 | .. code-block:: console 16 | 17 | $ pre-commit install 18 | 19 | As a result, the hooks will automatically check and fix some issues before 20 | pushing with git. If changes are made after committing with git, you will need 21 | to commit again afterwards. Alternatively, you can just run the following 22 | command beforehand: 23 | 24 | .. code-block:: console 25 | 26 | $ nb-clean clean --remove-empty-cells 27 | 28 | Command Cheatsheet 29 | ^^^^^^^^^^^^^^^^^^ 30 | All commands are run from the root of the project, from a terminal: 31 | 32 | +--------------------+-----------------------------------+ 33 | | Command | Action | 34 | +====================+===================================+ 35 | | $ hatch run format | Format project with ruff format . | 36 | | | and apply linting with ruff --fix | 37 | +--------------------+-----------------------------------+ 38 | | $ hatch run lint | Lint project with ruff check . | 39 | +--------------------+-----------------------------------+ 40 | | $ hatch run test | Run unit tests with pytest | 41 | +--------------------+-----------------------------------+ 42 | | $ hatch run docs | Build docs with Sphinx | 43 | +--------------------+-----------------------------------+ 44 | 45 | Alternatively, you can develop ipyniivue by manually creating a virtual environment and managing installation and dependencies with pip. 46 | 47 | .. code-block:: console 48 | 49 | python3 -m venv .venv && source .venv/bin/activate 50 | pip install -e ".[dev]" 51 | 52 | Making Changes to the JavaScript Code 53 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 54 | 55 | This is an `anywidget `__ project, which means the code base is hybrid Python and JavaScript. The JavaScript part is developed under js/ and uses `esbuild `__ to bundle the code. Any time you make changes to the JavaScript code, you need to rebuild the files under src/ipyniivue/static. This can be done in two ways: 56 | 57 | .. code-block:: console 58 | 59 | $ npm run build 60 | 61 | which will build the JavaScript code once, or you can start a development server: 62 | 63 | .. code-block:: console 64 | 65 | $ npm run dev 66 | 67 | which will start a development server that will automatically rebuild the code as you make changes. We recommend the latter approach, as it is more convenient. 68 | 69 | Once you have the development server running, you can start the JupyterLab or VS Code to develop the widget. When finished, you can stop the development server with Ctrl+C. 70 | 71 | NOTE: In order to have anywidget automatically apply changes as you work, make sure to export ANYWIDGET_HMR=1 environment variable. This can be set directly in a notebook with %env ANYWIDGET_HMR=1 in a cell. -------------------------------------------------------------------------------- /docs/source/enums.rst: -------------------------------------------------------------------------------- 1 | Enums 2 | ===== 3 | 4 | .. currentmodule:: ipyniivue 5 | 6 | .. autoclass:: ColormapType 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | .. autoclass:: DragMode 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | 16 | .. autoclass:: MultiplanarType 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | .. autoclass:: PenType 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | 26 | .. autoclass:: ShowRender 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | .. autoclass:: SliceType 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | IPyNiiVue Documentation 2 | ======================= 3 | 4 | A Jupyter Widget for Niivue based on anywidget. 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :caption: Contents: 9 | 10 | install 11 | api 12 | options 13 | traits 14 | enums 15 | architecture 16 | contributing -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | To first install IPyNiiVue and its dependencies, simply run: 5 | 6 | .. code-block:: console 7 | 8 | $ pip install ipyniivue 9 | 10 | Usage 11 | ^^^^^^ 12 | 13 | In a Jupyter environment: 14 | 15 | .. code-block:: python 16 | 17 | from ipyniivue import NiiVue 18 | 19 | nv = NiiVue() 20 | nv.load_volumes([{"path": "images/mni152.nii.gz"}]) 21 | nv 22 | 23 | See the `basic demo `__ to learn more. -------------------------------------------------------------------------------- /docs/source/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ======= 3 | 4 | .. autoclass:: ipyniivue.config_options.ConfigOptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/traits.rst: -------------------------------------------------------------------------------- 1 | Traits 2 | ====== 3 | 4 | .. currentmodule:: ipyniivue 5 | 6 | .. autoclass:: ColorMap 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | .. autoclass:: LUT 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | 16 | .. autoclass:: Graph 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: ipyniivue 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - pip 6 | - pip: 7 | - ipyniivue==2.4.1 8 | - requests 9 | - anywidget 10 | - nibabel 11 | - ipylab -------------------------------------------------------------------------------- /examples/add_from_frontend.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3c537ca6", 6 | "metadata": {}, 7 | "source": [ 8 | "# Drag-and-Drop\n", 9 | "This example showcases adding volumes/meshes via drag-and-drop. " 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "id": "4bf34aa7", 15 | "metadata": {}, 16 | "source": [ 17 | "This first code cell creates a new nv object without any loaded volumes or meshes." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "37a1e7ad-ea43-4e2a-95e9-2eef3dc74a97", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from ipyniivue import NiiVue\n", 28 | "\n", 29 | "nv = NiiVue()\n", 30 | "nv" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "abbcb465", 36 | "metadata": {}, 37 | "source": [ 38 | "Now, try dragging and dropping a volume file, for example [mni152.nii.gz](https://niivue.com/demos/images/mni152.nii.gz). Then, run the next code cell to check that volumes have been loaded into the python backend." 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "78169ab8-781c-4488-8259-47a6be43e625", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "nv.volumes" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "id": "0844a7c1", 54 | "metadata": {}, 55 | "source": [ 56 | "You can also modify this volume's features, such as its opacity." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "06bd69d3-3a1a-40ad-9362-7cc21f264ebf", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "if len(nv.volumes) > 0:\n", 67 | " nv.volumes[0].opacity = 0.5" 68 | ] 69 | } 70 | ], 71 | "metadata": {}, 72 | "nbformat": 4, 73 | "nbformat_minor": 5 74 | } 75 | -------------------------------------------------------------------------------- /examples/additive_voxels.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from pathlib import Path\n", 10 | "\n", 11 | "import ipyniivue\n", 12 | "from ipyniivue import NiiVue, download_dataset\n", 13 | "\n", 14 | "# GitHub API URL for the base folder\n", 15 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 16 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "# Download data for example\n", 26 | "download_dataset(\n", 27 | " BASE_API_URL,\n", 28 | " DATA_FOLDER,\n", 29 | " files=[\n", 30 | " \"mni152.nii.gz\",\n", 31 | " \"narps-4965_9U7M-hypo1_unthresh.nii.gz\",\n", 32 | " \"narps-4735_50GV-hypo1_unthresh.nii.gz\",\n", 33 | " ],\n", 34 | ")" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "# based on https://niivue.github.io/niivue/features/additive.voxels.html\n", 44 | "\n", 45 | "volumes = [\n", 46 | " {\"path\": DATA_FOLDER / \"mni152.nii.gz\"},\n", 47 | " {\n", 48 | " \"path\": DATA_FOLDER / \"narps-4965_9U7M-hypo1_unthresh.nii.gz\",\n", 49 | " \"colormap\": \"red\",\n", 50 | " \"cal_min\": 2,\n", 51 | " \"cal_max\": 4,\n", 52 | " },\n", 53 | " {\n", 54 | " \"path\": DATA_FOLDER / \"narps-4735_50GV-hypo1_unthresh.nii.gz\",\n", 55 | " \"colormap\": \"green\",\n", 56 | " \"cal_min\": 2,\n", 57 | " \"cal_max\": 4,\n", 58 | " },\n", 59 | "]\n", 60 | "nv = NiiVue(\n", 61 | " back_color=(1, 1, 1, 1),\n", 62 | " show_3D_crosshair=True,\n", 63 | " is_colorbar=True,\n", 64 | ")\n", 65 | "nv.load_volumes(volumes)\n", 66 | "nv" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "import ipywidgets\n", 76 | "\n", 77 | "nv.volumes[0].colorbar_visible = False\n", 78 | "sred = ipywidgets.FloatSlider(min=0.1, max=0.4, step=0.01, value=0.2)\n", 79 | "ipywidgets.link((sred, \"value\"), (nv.volumes[1], \"cal_min\"))\n", 80 | "sgreen = ipywidgets.FloatSlider(min=0.1, max=0.4, step=0.01, value=0.2)\n", 81 | "ipywidgets.link((sgreen, \"value\"), (nv.volumes[2], \"cal_min\"))\n", 82 | "ipywidgets.HBox([sred, sgreen])" 83 | ] 84 | } 85 | ], 86 | "metadata": {}, 87 | "nbformat": 4, 88 | "nbformat_minor": 4 89 | } 90 | -------------------------------------------------------------------------------- /examples/afni.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8ac54f8d-2d2c-4e57-a8a1-a1a1463b6451", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import necessary modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "e57b7f74-2402-4580-a3c9-bd1dbb3b8cdc", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import asyncio\n", 19 | "import pathlib\n", 20 | "\n", 21 | "import ipywidgets as widgets\n", 22 | "from IPython.display import display\n", 23 | "\n", 24 | "import ipyniivue\n", 25 | "from ipyniivue import NiiVue, ShowRender, SliceType, download_dataset" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "id": "b000ac63-b5b8-4258-b1be-e83868675c25", 31 | "metadata": {}, 32 | "source": [ 33 | "# Download required data" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "8f176e83", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 44 | "\n", 45 | "download_dataset(\n", 46 | " api_url=\"https://niivue.com/demos/images/\",\n", 47 | " dest_folder=DATA_FOLDER,\n", 48 | " files=[\n", 49 | " \"example4d+orig.HEAD\",\n", 50 | " \"example4d+orig.BRIK.gz\",\n", 51 | " ],\n", 52 | ")" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "id": "a3678fb5-65b1-4528-9a74-d7f27c62c8df", 58 | "metadata": {}, 59 | "source": [ 60 | "# Create the NiiVue widget" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "a873dc99-8c09-4518-a053-c6a4720aff8d", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "nv = NiiVue()\n", 71 | "\n", 72 | "nv.set_radiological_convention(False)\n", 73 | "nv.set_slice_type(SliceType.MULTIPLANAR)\n", 74 | "nv.opts.multiplanar_show_render = ShowRender.ALWAYS\n", 75 | "\n", 76 | "# Configure graph values\n", 77 | "nv.graph.auto_size_multiplanar = True\n", 78 | "nv.graph.normalize_values = False\n", 79 | "nv.graph.opacity = 1.0\n", 80 | "\n", 81 | "# Load 4D volume with paired HEAD and BRIK files\n", 82 | "nv.load_volumes(\n", 83 | " [\n", 84 | " {\n", 85 | " \"path\": DATA_FOLDER / \"example4d+orig.HEAD\",\n", 86 | " \"paired_img_path\": DATA_FOLDER / \"example4d+orig.BRIK.gz\",\n", 87 | " \"colormap\": \"gray\",\n", 88 | " \"opacity\": 1.0,\n", 89 | " \"visible\": True,\n", 90 | " },\n", 91 | " ]\n", 92 | ")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "dd0e9aee-0bdb-4eba-829c-e6e5acd68835", 98 | "metadata": {}, 99 | "source": [ 100 | "# Create other buttons/checkboxes" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "id": "3fd88e96-ca86-4b36-92eb-1e1f2d80f8ed", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "display_frame = widgets.Label(value=\"Volume: 0\")\n", 111 | "\n", 112 | "normalize_checkbox = widgets.Checkbox(\n", 113 | " value=False,\n", 114 | " description=\"Normalize Graph\",\n", 115 | ")\n", 116 | "\n", 117 | "prev_button = widgets.Button(description=\"Back\")\n", 118 | "next_button = widgets.Button(description=\"Forward\")" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "id": "111a97d2-714e-47ae-81b8-80c44f6726a9", 124 | "metadata": {}, 125 | "source": [ 126 | "# Implement the callbacks" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "id": "167128f8-a16a-43bd-803f-da10f08c0d4d", 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "def on_normalize_change(change):\n", 137 | " \"\"\"Normalize graph.\"\"\"\n", 138 | " nv.graph.normalize_values = change[\"new\"]\n", 139 | "\n", 140 | "\n", 141 | "normalize_checkbox.observe(on_normalize_change, names=\"value\")\n", 142 | "\n", 143 | "\n", 144 | "def on_prev_button_clicked(b):\n", 145 | " \"\"\"Decrement the frame index.\"\"\"\n", 146 | " if nv.volumes:\n", 147 | " current_frame = nv.volumes[0].frame_4d\n", 148 | " new_frame = max(current_frame - 1, 0)\n", 149 | " nv.volumes[0].frame_4d = new_frame\n", 150 | " display_frame.value = f\"Volume: {new_frame}\"\n", 151 | "\n", 152 | "\n", 153 | "def on_next_button_clicked(b):\n", 154 | " \"\"\"Increment the frame index.\"\"\"\n", 155 | " if nv.volumes:\n", 156 | " current_frame = nv.volumes[0].frame_4d\n", 157 | " n_frames = nv.volumes[0].n_frame_4d\n", 158 | " new_frame = min(current_frame + 1, n_frames - 1)\n", 159 | " nv.volumes[0].frame_4d = new_frame\n", 160 | " display_frame.value = f\"Volume: {new_frame}\"\n", 161 | "\n", 162 | "\n", 163 | "prev_button.on_click(on_prev_button_clicked)\n", 164 | "next_button.on_click(on_next_button_clicked)" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "id": "b310d908-6f11-4d66-83fa-117af002799e", 170 | "metadata": {}, 171 | "source": [ 172 | "# Create animate button" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "id": "24726cb0-0d09-4e98-a16c-630d3b746cd3", 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "animate_button = widgets.Button(description=\"Animate\")\n", 183 | "\n", 184 | "animation_running = False\n", 185 | "animation_task = None\n", 186 | "\n", 187 | "\n", 188 | "async def animate_frames():\n", 189 | " \"\"\"Animation loop.\"\"\"\n", 190 | " global animation_running\n", 191 | " if not nv.volumes:\n", 192 | " return\n", 193 | " n_frames = nv.volumes[0].n_frame_4d\n", 194 | " try:\n", 195 | " while animation_running:\n", 196 | " current_frame = nv.volumes[0].frame_4d\n", 197 | " current_frame = (current_frame + 1) % n_frames\n", 198 | " nv.volumes[0].frame_4d = current_frame\n", 199 | " display_frame.value = f\"Volume: {current_frame}\"\n", 200 | " await asyncio.sleep(0.1)\n", 201 | " except asyncio.CancelledError:\n", 202 | " pass\n", 203 | "\n", 204 | "\n", 205 | "def on_animate_button_clicked(b):\n", 206 | " \"\"\"Define 'Animate' button click handler.\"\"\"\n", 207 | " global animation_running, animation_task\n", 208 | " if not animation_running:\n", 209 | " # Start animation\n", 210 | " animation_running = True\n", 211 | " animate_button.description = \"Stop\"\n", 212 | " # Schedule the animation coroutine and store the future\n", 213 | " animation_task = asyncio.ensure_future(animate_frames())\n", 214 | " else:\n", 215 | " # Stop animation\n", 216 | " animation_running = False\n", 217 | " animate_button.description = \"Animate\"\n", 218 | " # Cancel the running task if it's active\n", 219 | " if animation_task is not None:\n", 220 | " animation_task.cancel()\n", 221 | " animation_task = None\n", 222 | "\n", 223 | "\n", 224 | "animate_button.on_click(on_animate_button_clicked)" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "id": "5f6f5a15-6b36-4312-b735-10e185900c19", 230 | "metadata": {}, 231 | "source": [ 232 | "# Reset frame index on image loaded" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "id": "71ba69a0-6fe0-48a7-a03b-29822e24d676", 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "@nv.on_image_loaded\n", 243 | "def update_number_of_frames(volume):\n", 244 | " \"\"\"Reset to first frame.\"\"\"\n", 245 | " nv.volumes[0].frame_4d = 0\n", 246 | " display_frame.value = \"Volume: 0\"" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "id": "906aee0a-cb57-46b6-9808-a8ac5443a408", 252 | "metadata": {}, 253 | "source": [ 254 | "# Display all" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "id": "44999f74-851a-40c7-aee1-84a65aeb69b0", 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "controls = widgets.HBox(\n", 265 | " [\n", 266 | " normalize_checkbox,\n", 267 | " prev_button,\n", 268 | " next_button,\n", 269 | " animate_button,\n", 270 | " ]\n", 271 | ")\n", 272 | "\n", 273 | "display(widgets.VBox([controls, display_frame, nv]))" 274 | ] 275 | } 276 | ], 277 | "metadata": {}, 278 | "nbformat": 4, 279 | "nbformat_minor": 5 280 | } 281 | -------------------------------------------------------------------------------- /examples/atlas.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "496e0b0c-4465-49ac-b45b-90840e8584cc", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import Necessary Modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "db0d1707-8667-4fb5-bd05-7f9c72e114bd", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import json\n", 19 | "import pathlib\n", 20 | "\n", 21 | "import ipywidgets as widgets\n", 22 | "from IPython.display import display\n", 23 | "\n", 24 | "import ipyniivue\n", 25 | "from ipyniivue import NiiVue, ShowRender, download_dataset" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "id": "6f7c902e-74ba-43ef-8abc-4e71e635452b", 31 | "metadata": {}, 32 | "source": [ 33 | "# Download Required Data" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "4e7b6d15-fadc-4775-b7b5-4768a2e6f184", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "# Define the data folder where the images will be stored\n", 44 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 45 | "\n", 46 | "# Download the required data files\n", 47 | "download_dataset(\n", 48 | " \"https://niivue.com/demos/images/\",\n", 49 | " dest_folder=DATA_FOLDER,\n", 50 | " files=[\n", 51 | " \"mni152.nii.gz\",\n", 52 | " \"aal.nii.gz\",\n", 53 | " \"aal.json\",\n", 54 | " ],\n", 55 | ")" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "426ab05d-8568-48ad-b50c-559af3327e75", 61 | "metadata": {}, 62 | "source": [ 63 | "# Create NiiVue Instance" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "a262366c-aa16-4dde-927b-04dc5133e885", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "nv = NiiVue(\n", 74 | " show_3d_crosshair=True,\n", 75 | " back_color=(0.5, 0.5, 0.5, 1),\n", 76 | ")\n", 77 | "\n", 78 | "nv.set_interpolation(True)\n", 79 | "nv.opts.crosshair_gap = 12\n", 80 | "nv.opts.multiplanar_show_render = ShowRender.ALWAYS\n", 81 | "nv.opts.drag_mode = \"PAN\"\n", 82 | "nv.opts.yoke_3d_to_2d_zoom = True\n", 83 | "\n", 84 | "nv.load_volumes(\n", 85 | " [\n", 86 | " {\"path\": DATA_FOLDER / \"mni152.nii.gz\"},\n", 87 | " {\"path\": DATA_FOLDER / \"aal.nii.gz\"},\n", 88 | " ]\n", 89 | ")" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "ef9c6c82-c66c-405a-8af5-431f9cf0d14b", 95 | "metadata": {}, 96 | "source": [ 97 | "# Load colormap label" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "c974ffd9-b6ad-4ff5-bfde-634c3ec0a61e", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "with open(DATA_FOLDER / \"aal.json\") as f:\n", 108 | " cmap = json.load(f)\n", 109 | "\n", 110 | "nv.volumes[1].set_colormap_label(cmap)\n", 111 | "\n", 112 | "clut = nv.volumes[1].colormap_label.lut.copy()\n", 113 | "\n", 114 | "# Make all regions translucent by setting alpha values to 96\n", 115 | "for i in range(3, len(clut), 4):\n", 116 | " clut[i] = 96\n", 117 | "\n", 118 | "# Update the colormap label with the modified lut\n", 119 | "nv.volumes[1].colormap_label.lut = clut" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "9e56f814-6c98-4450-aa36-9f4c6b6930f6", 125 | "metadata": {}, 126 | "source": [ 127 | "# Add other widgets" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "id": "64ccf253-e201-412d-99e9-f5566cbb7e94", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "interp_checkbox = widgets.Checkbox(\n", 138 | " value=True,\n", 139 | " description=\"Jagged\",\n", 140 | ")\n", 141 | "\n", 142 | "outline_slider = widgets.IntSlider(\n", 143 | " min=0,\n", 144 | " max=255,\n", 145 | " value=1,\n", 146 | " description=\"Outline\",\n", 147 | ")\n", 148 | "\n", 149 | "alpha_slider = widgets.IntSlider(\n", 150 | " min=1,\n", 151 | " max=255,\n", 152 | " value=150,\n", 153 | " description=\"Opacity\",\n", 154 | ")\n", 155 | "\n", 156 | "pad_slider = widgets.IntSlider(\n", 157 | " min=0,\n", 158 | " max=10,\n", 159 | " value=5,\n", 160 | " description=\"Padding\",\n", 161 | ")\n", 162 | "\n", 163 | "gap_slider = widgets.IntSlider(\n", 164 | " min=0,\n", 165 | " max=36,\n", 166 | " value=12,\n", 167 | " description=\"Crosshair Gap\",\n", 168 | ")" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "id": "f05fd288-5f27-49f5-ba92-8b8cd26aa954", 174 | "metadata": {}, 175 | "source": [ 176 | "# Setup observer functions" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "id": "2e86879a-71c5-4048-8d9f-6bca0ed9d9fd", 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "def on_outline_change(change):\n", 187 | " \"\"\"Handle changes in the outline slider.\"\"\"\n", 188 | " nv.set_atlas_outline(change[\"new\"] / 255)\n", 189 | "\n", 190 | "\n", 191 | "def on_alpha_change(change):\n", 192 | " \"\"\"Handle changes in the opacity slider.\"\"\"\n", 193 | " nv.volumes[1].opacity = change[\"new\"] / 255\n", 194 | "\n", 195 | "\n", 196 | "def on_pad_change(change):\n", 197 | " \"\"\"Handle changes in the padding slider.\"\"\"\n", 198 | " nv.opts.multiplanar_pad_pixels = change[\"new\"]\n", 199 | "\n", 200 | "\n", 201 | "def on_gap_change(change):\n", 202 | " \"\"\"Handle changes in the crosshair gap slider.\"\"\"\n", 203 | " nv.opts.crosshair_gap = change[\"new\"]\n", 204 | "\n", 205 | "\n", 206 | "def on_interp_change(change):\n", 207 | " \"\"\"Handle changes in the interpolation checkbox.\"\"\"\n", 208 | " nv.set_interpolation(change[\"new\"])" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "id": "2e8ec870-d0c2-4701-8eef-bf34afc9c891", 214 | "metadata": {}, 215 | "source": [ 216 | "# Observe changes" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": null, 222 | "id": "a2d4118d-2987-4bbd-af6b-f82f2a9e623c", 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "# Observe changes in widget values and call the respective functions\n", 227 | "outline_slider.observe(on_outline_change, names=\"value\")\n", 228 | "alpha_slider.observe(on_alpha_change, names=\"value\")\n", 229 | "pad_slider.observe(on_pad_change, names=\"value\")\n", 230 | "gap_slider.observe(on_gap_change, names=\"value\")\n", 231 | "interp_checkbox.observe(on_interp_change, names=\"value\")\n", 232 | "\n", 233 | "# Initialize the NiiVue instance with the current widget values\n", 234 | "on_alpha_change({\"new\": alpha_slider.value})\n", 235 | "on_outline_change({\"new\": outline_slider.value})\n", 236 | "nv.opts.multiplanar_pad_pixels = pad_slider.value\n", 237 | "nv.opts.crosshair_gap = gap_slider.value\n", 238 | "nv.set_interpolation(interp_checkbox.value)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "id": "2d1d8a71-3c2b-4449-828c-e74e9653938f", 244 | "metadata": {}, 245 | "source": [ 246 | "# Setup hover and click updates" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "id": "44980c10-573d-493a-9a4d-c002942cf563", 253 | "metadata": {}, 254 | "outputs": [], 255 | "source": [ 256 | "output = widgets.HTML(\"Hover:
Clicked:\")\n", 257 | "\n", 258 | "active_idx = -1\n", 259 | "\n", 260 | "\n", 261 | "@nv.on_hover_idx_change\n", 262 | "def on_hover_idx_change(data):\n", 263 | " \"\"\"Handle hover updates.\"\"\"\n", 264 | " global active_idx\n", 265 | " idx_values = data[\"idxValues\"]\n", 266 | " idx = idx_values[1][\"idx\"]\n", 267 | " if idx is not None and idx != active_idx:\n", 268 | " nv.opts.atlas_active_index = idx\n", 269 | " label = cmap[\"labels\"][idx] if idx < len(cmap[\"labels\"]) else \"\"\n", 270 | " clicked_line = output.value.split(\"Clicked:\")[1]\n", 271 | " output.value = f\"Hover: {label}
Clicked:{clicked_line}\"\n", 272 | "\n", 273 | "\n", 274 | "@nv.on_location_change\n", 275 | "def handle_location_change(location):\n", 276 | " \"\"\"Handle mouse clicks.\"\"\"\n", 277 | " hover_line = output.value.split(\"Hover:\")[1].split(\"
\")[0]\n", 278 | " output.value = f\"Hover:{hover_line}
Clicked: {location['string']}\"" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "id": "20ac0527-b46a-4203-af39-512902327917", 284 | "metadata": {}, 285 | "source": [ 286 | "# Display all" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": null, 292 | "id": "cd2e77fd-7b6e-4026-a814-0fe155151e67", 293 | "metadata": {}, 294 | "outputs": [], 295 | "source": [ 296 | "controls = widgets.VBox(\n", 297 | " [\n", 298 | " interp_checkbox,\n", 299 | " outline_slider,\n", 300 | " alpha_slider,\n", 301 | " pad_slider,\n", 302 | " gap_slider,\n", 303 | " output,\n", 304 | " ]\n", 305 | ")\n", 306 | "\n", 307 | "display(\n", 308 | " widgets.VBox(\n", 309 | " [\n", 310 | " controls,\n", 311 | " nv,\n", 312 | " ]\n", 313 | " )\n", 314 | ")" 315 | ] 316 | } 317 | ], 318 | "metadata": {}, 319 | "nbformat": 4, 320 | "nbformat_minor": 5 321 | } 322 | -------------------------------------------------------------------------------- /examples/atlas.sparse.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "39b55e89-0de0-449c-b0f5-0d0ecb54e6f4", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import Necessary Modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "ec364cef-1bd3-498d-b571-281dfada6249", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import pathlib\n", 19 | "\n", 20 | "import ipywidgets as widgets\n", 21 | "from IPython.display import display\n", 22 | "\n", 23 | "import ipyniivue\n", 24 | "from ipyniivue import DragMode, NiiVue, ShowRender, download_dataset" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "fbd334de-9cf8-4b78-bfd3-bec98d52d50f", 30 | "metadata": {}, 31 | "source": [ 32 | "# Download Required Data" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "ef853053-a88e-423b-9e8c-096399af731f", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 43 | "\n", 44 | "download_dataset(\n", 45 | " \"https://niivue.com/demos/images/\",\n", 46 | " dest_folder=DATA_FOLDER,\n", 47 | " files=[\n", 48 | " \"inia19-t1-brain.nii.gz\",\n", 49 | " \"inia19-NeuroMaps.nii.gz\",\n", 50 | " ],\n", 51 | ")" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "bdc45f6e-9fff-44b2-bb22-dac86c37326d", 57 | "metadata": {}, 58 | "source": [ 59 | "# Setup NiiVue Instance" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "id": "bd327698-3349-4f6a-ac25-1b12b3ee18d4", 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "nv = NiiVue(\n", 70 | " back_color=(0.5, 0.5, 0.5, 1),\n", 71 | " show_3d_crosshair=True,\n", 72 | " drag_mode=DragMode.PAN,\n", 73 | " yoke_3d_to_2d_zoom=True,\n", 74 | " multiplanar_show_render=ShowRender.ALWAYS,\n", 75 | ")\n", 76 | "\n", 77 | "nv.load_volumes(\n", 78 | " [\n", 79 | " {\"path\": DATA_FOLDER / \"inia19-t1-brain.nii.gz\"},\n", 80 | " {\"path\": DATA_FOLDER / \"inia19-NeuroMaps.nii.gz\", \"opacity\": 0.5},\n", 81 | " ]\n", 82 | ")\n", 83 | "\n", 84 | "nv.volumes[1].set_colormap_label_from_url(\n", 85 | " \"https://niivue.com/demos/images/inia19-NeuroMaps.json\"\n", 86 | ")\n", 87 | "\n", 88 | "nv.volumes[1].opacity = 0.188\n", 89 | "nv.set_atlas_outline(1.0)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "b6a15481-c027-4128-8cc7-b13b0e0df1c8", 95 | "metadata": {}, 96 | "source": [ 97 | "# Create interactive widgets" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "909c02d8-5392-4f52-928e-407620fc2eea", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "# Slider for opacity\n", 108 | "opacity_slider = widgets.IntSlider(\n", 109 | " min=1,\n", 110 | " max=255,\n", 111 | " value=48,\n", 112 | " description=\"Opacity\",\n", 113 | " continuous_update=True,\n", 114 | ")\n", 115 | "\n", 116 | "# Slider for outline\n", 117 | "outline_slider = widgets.IntSlider(\n", 118 | " min=0,\n", 119 | " max=255,\n", 120 | " value=255,\n", 121 | " description=\"Outline\",\n", 122 | " continuous_update=True,\n", 123 | ")\n", 124 | "\n", 125 | "location_label = widgets.HTML(\" \")" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "id": "c22dcfc8-4ac1-443f-9c4b-dcb9073d329a", 131 | "metadata": {}, 132 | "source": [ 133 | "# Define callbacks" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "id": "36e58df0-4a17-4f6c-bcc7-f211fa48e428", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "def on_opacity_change(change):\n", 144 | " \"\"\"Update the opacity of the atlas volume.\"\"\"\n", 145 | " nv.volumes[1].opacity = change[\"new\"] / 255\n", 146 | "\n", 147 | "\n", 148 | "def on_outline_change(change):\n", 149 | " \"\"\"Update the outline thickness of the atlas.\"\"\"\n", 150 | " nv.set_atlas_outline(change[\"new\"] / 255)\n", 151 | "\n", 152 | "\n", 153 | "def handle_location_change(location):\n", 154 | " \"\"\"Update the location label with current coordinates.\"\"\"\n", 155 | " location_label.value = location[\"string\"]" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "2e03552e-e106-41fd-9a01-cf953757f800", 161 | "metadata": {}, 162 | "source": [ 163 | "# Setup observers" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "id": "350e4e11-9202-4e99-9afa-e8883d2b3bd5", 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "opacity_slider.observe(on_opacity_change, names=\"value\")\n", 174 | "outline_slider.observe(on_outline_change, names=\"value\")\n", 175 | "\n", 176 | "nv.on_location_change(handle_location_change)\n", 177 | "\n", 178 | "on_opacity_change({\"new\": opacity_slider.value})\n", 179 | "on_outline_change({\"new\": outline_slider.value})" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "id": "84c861a3-040a-4597-9a4d-40974c8501e5", 185 | "metadata": {}, 186 | "source": [ 187 | "# Display all" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "id": "94acebe5-edd8-4b92-875b-391a8f11bd57", 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "controls = widgets.HBox([opacity_slider, outline_slider])\n", 198 | "\n", 199 | "display(\n", 200 | " widgets.VBox(\n", 201 | " [\n", 202 | " controls,\n", 203 | " nv,\n", 204 | " location_label,\n", 205 | " ]\n", 206 | " )\n", 207 | ")" 208 | ] 209 | } 210 | ], 211 | "metadata": {}, 212 | "nbformat": 4, 213 | "nbformat_minor": 5 214 | } 215 | -------------------------------------------------------------------------------- /examples/basic_multiplanar.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from pathlib import Path\n", 10 | "\n", 11 | "import ipyniivue\n", 12 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 13 | "\n", 14 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 15 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# Download data for example\n", 25 | "download_dataset(\n", 26 | " BASE_API_URL,\n", 27 | " DATA_FOLDER,\n", 28 | " files=[\n", 29 | " \"mni152.nii.gz\",\n", 30 | " \"hippo.nii.gz\",\n", 31 | " ],\n", 32 | ")" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "# based on https://niivue.github.io/niivue/features/basic.multiplanar.html\n", 42 | "\n", 43 | "volumes = [\n", 44 | " {\n", 45 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 46 | " \"colormap\": \"gray\",\n", 47 | " \"visible\": True,\n", 48 | " \"opacity\": 1.0,\n", 49 | " },\n", 50 | " {\n", 51 | " \"path\": DATA_FOLDER / \"hippo.nii.gz\",\n", 52 | " \"colormap\": \"red\",\n", 53 | " \"visible\": True,\n", 54 | " \"opacity\": 1,\n", 55 | " },\n", 56 | "]\n", 57 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 58 | "nv.load_volumes(volumes)\n", 59 | "nv" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "nv.volumes[0].opacity = 0.3\n", 69 | "nv.volumes[1].colormap = \"blue\"" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "nv.opts.show_3d_crosshair = True\n", 79 | "nv.opts.crosshair_color = [0, 1, 1, 1]\n", 80 | "nv.opts.back_color = [1, 1, 1, 1]\n", 81 | "nv.opts.clip_plane_color = [0, 1, 1, 1]\n", 82 | "nv.opts.is_colorbar = True\n", 83 | "nv.opts.view_mode_hot_key = \"KeyN\"" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "nv" 93 | ] 94 | } 95 | ], 96 | "metadata": {}, 97 | "nbformat": 4, 98 | "nbformat_minor": 4 99 | } 100 | -------------------------------------------------------------------------------- /examples/colormaps.mesh.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3f68cd5d-4213-44e2-b1db-fae4bebb7a5d", 6 | "metadata": {}, 7 | "source": [ 8 | "Prepare images" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "c36ea7c1-438a-462e-b796-92c586424af8", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from pathlib import Path\n", 19 | "\n", 20 | "import ipyniivue\n", 21 | "\n", 22 | "# GitHub API URL for the base folder\n", 23 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 24 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "e1149f33-ef1e-4249-821f-e41d0644319e", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "# Download data for example\n", 35 | "ipyniivue.download_dataset(\n", 36 | " BASE_API_URL,\n", 37 | " DATA_FOLDER,\n", 38 | " files=[\n", 39 | " \"BrainMesh_ICBM152.lh.curv\",\n", 40 | " \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 41 | " \"BrainMesh_ICBM152.lh.mz3\",\n", 42 | " ],\n", 43 | ")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "1d522f34-35e0-452c-808d-2d45a9ee9c37", 49 | "metadata": {}, 50 | "source": [ 51 | "Create instance of niivue" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "id": "b4444b81-5bfe-46cc-af63-f6044d04fb80", 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "from ipyniivue import NiiVue\n", 62 | "\n", 63 | "nv = NiiVue(back_color=(0.3, 0.3, 0.3, 1))\n", 64 | "\n", 65 | "meshLHLayersList1 = [\n", 66 | " {\n", 67 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.curv\",\n", 68 | " \"colormap\": \"gray\",\n", 69 | " \"cal_min\": 0.3,\n", 70 | " \"cal_max\": 0.5,\n", 71 | " \"opacity\": 0.7,\n", 72 | " },\n", 73 | " {\n", 74 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 75 | " \"cal_min\": 1.64,\n", 76 | " \"cal_max\": 5,\n", 77 | " \"colormap\": \"warm\",\n", 78 | " \"colormap_negative\": \"winter\",\n", 79 | " \"use_negative_cmap\": True,\n", 80 | " \"opacity\": 0.7,\n", 81 | " },\n", 82 | "]\n", 83 | "\n", 84 | "nv.load_meshes(\n", 85 | " [\n", 86 | " {\n", 87 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.mz3\",\n", 88 | " \"layers\": meshLHLayersList1,\n", 89 | " }\n", 90 | " ]\n", 91 | ")\n", 92 | "\n", 93 | "nv.opts.is_colorbar = True\n", 94 | "\n", 95 | "nv.meshes[0].layers[0].colorbar_visible = False" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "55820127-9917-4e54-aa48-bf1e169ee4c6", 101 | "metadata": {}, 102 | "source": [ 103 | "Create interactive widgets" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "id": "19ea4d11-94cc-4d7a-ba4d-21e407c18939", 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "import ipywidgets as widgets\n", 114 | "from IPython.display import display\n", 115 | "\n", 116 | "# Invert Checkbox\n", 117 | "invert_check = widgets.Checkbox(\n", 118 | " value=False,\n", 119 | " description=\"Invert\",\n", 120 | ")\n", 121 | "\n", 122 | "\n", 123 | "def on_invert_change(change):\n", 124 | " \"\"\"Set mesh layer 1 colormap invert.\"\"\"\n", 125 | " nv.set_mesh_layer_property(nv.meshes[0].id, 1, \"colormap_invert\", change[\"new\"])\n", 126 | "\n", 127 | "\n", 128 | "invert_check.observe(on_invert_change, names=\"value\")\n", 129 | "\n", 130 | "# Curvature Slider\n", 131 | "curve_slider = widgets.IntSlider(\n", 132 | " value=7,\n", 133 | " min=0,\n", 134 | " max=10,\n", 135 | " step=1,\n", 136 | " description=\"Curvature\",\n", 137 | ")\n", 138 | "\n", 139 | "\n", 140 | "def on_curve_slider_change(change):\n", 141 | " \"\"\"Set mesh layer 0 opacity.\"\"\"\n", 142 | " nv.set_mesh_layer_property(nv.meshes[0].id, 0, \"opacity\", change[\"new\"] * 0.1)\n", 143 | "\n", 144 | "\n", 145 | "curve_slider.observe(on_curve_slider_change, names=\"value\")\n", 146 | "\n", 147 | "# Opacity Slider\n", 148 | "opacity_slider = widgets.IntSlider(\n", 149 | " value=7,\n", 150 | " min=0,\n", 151 | " max=10,\n", 152 | " step=1,\n", 153 | " description=\"Opacity\",\n", 154 | ")\n", 155 | "\n", 156 | "\n", 157 | "def on_opacity_slider_change(change):\n", 158 | " \"\"\"Set mesh layer 1 opacity.\"\"\"\n", 159 | " nv.set_mesh_layer_property(nv.meshes[0].id, 1, \"opacity\", change[\"new\"] * 0.1)\n", 160 | "\n", 161 | "\n", 162 | "opacity_slider.observe(on_opacity_slider_change, names=\"value\")\n", 163 | "\n", 164 | "# Save Bitmap Button\n", 165 | "save_bmp_button = widgets.Button(\n", 166 | " description=\"Save Bitmap\",\n", 167 | ")\n", 168 | "\n", 169 | "\n", 170 | "def on_save_bmp_clicked(b):\n", 171 | " \"\"\"Save scene.\"\"\"\n", 172 | " nv.save_scene(\"ScreenShot.png\")\n", 173 | " with out:\n", 174 | " from IPython.display import clear_output\n", 175 | "\n", 176 | " clear_output(wait=True)\n", 177 | " print('Scene saved as \"ScreenShot.png\"')\n", 178 | "\n", 179 | "\n", 180 | "save_bmp_button.on_click(on_save_bmp_clicked)\n", 181 | "\n", 182 | "# Output widget for messages\n", 183 | "out = widgets.Output()" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "id": "25290c1d-4992-45fb-a601-12ae69278856", 189 | "metadata": {}, 190 | "source": [ 191 | "Create the colormap header buttons" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "id": "dba89d89-9870-4b5b-943e-b87718b537e1", 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [ 201 | "import math\n", 202 | "\n", 203 | "# Get available colormaps\n", 204 | "colormaps = nv.colormaps()\n", 205 | "colormap_buttons = []\n", 206 | "\n", 207 | "\n", 208 | "def create_colormap_button(name):\n", 209 | " \"\"\"Create colormap.\"\"\"\n", 210 | " btn = widgets.Button(description=name)\n", 211 | "\n", 212 | " def on_click(b):\n", 213 | " \"\"\"Set mesh layer colormap.\"\"\"\n", 214 | " nv.set_mesh_layer_property(nv.meshes[0].id, 1, \"colormap\", name)\n", 215 | "\n", 216 | " btn.on_click(on_click)\n", 217 | " return btn\n", 218 | "\n", 219 | "\n", 220 | "colormap_buttons = [create_colormap_button(name) for name in colormaps]\n", 221 | "\n", 222 | "# Organize colormap buttons in a grid\n", 223 | "num_cols = 10\n", 224 | "num_rows = math.ceil(len(colormap_buttons) / num_cols)\n", 225 | "colormap_grid = []\n", 226 | "\n", 227 | "for i in range(num_rows):\n", 228 | " row_buttons = colormap_buttons[i * num_cols : (i + 1) * num_cols]\n", 229 | " colormap_grid.append(widgets.HBox(row_buttons))\n", 230 | "\n", 231 | "colormap_buttons_widget = widgets.VBox(colormap_grid)" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "id": "4e53f48a-867b-4195-881b-c800e17ffc90", 237 | "metadata": {}, 238 | "source": [ 239 | "Display" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "id": "0dd22c2c-0c0c-4b03-9b56-9af6020b9b60", 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "# Display header controls\n", 250 | "header_controls = widgets.HBox(\n", 251 | " [\n", 252 | " invert_check,\n", 253 | " curve_slider,\n", 254 | " opacity_slider,\n", 255 | " save_bmp_button,\n", 256 | " ]\n", 257 | ")\n", 258 | "\n", 259 | "display(header_controls)\n", 260 | "display(nv)\n", 261 | "\n", 262 | "# Display footer with colormap buttons\n", 263 | "display(colormap_buttons_widget)\n", 264 | "\n", 265 | "# Print output\n", 266 | "display(out)" 267 | ] 268 | } 269 | ], 270 | "metadata": {}, 271 | "nbformat": 4, 272 | "nbformat_minor": 5 273 | } 274 | -------------------------------------------------------------------------------- /examples/complex.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ed7fbf9d-3501-42a4-b921-5b52d8f03ed1", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import necessary modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "d3a59e62-a3df-4e47-b6d5-53fd79a9d959", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import pathlib\n", 19 | "\n", 20 | "import ipywidgets as widgets\n", 21 | "from IPython.display import display\n", 22 | "\n", 23 | "import ipyniivue\n", 24 | "from ipyniivue import NiiVue, download_dataset" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "8e9962f5-1c8f-4b23-bbec-7799dedcc36d", 30 | "metadata": {}, 31 | "source": [ 32 | "# Download required data" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "51551acf-1252-4bd9-837a-e5322f4458d5", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 43 | "\n", 44 | "download_dataset(\n", 45 | " \"https://niivue.com/demos/images/\",\n", 46 | " dest_folder=DATA_FOLDER,\n", 47 | " files=[\n", 48 | " \"complex.nii.gz\",\n", 49 | " ],\n", 50 | ")" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "id": "e41d20b2-cb78-4593-ab04-27f62d866762", 56 | "metadata": {}, 57 | "source": [ 58 | "# Setup NiiVue instance" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "id": "1fcb1f7a-fdbf-4fdc-b59d-ce18e53a8f04", 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "nv = NiiVue()\n", 69 | "\n", 70 | "intensity_output = widgets.HTML(\" \")\n", 71 | "\n", 72 | "\n", 73 | "@nv.on_location_change\n", 74 | "def handle_intensity_change(location):\n", 75 | " \"\"\"Handle location change.\"\"\"\n", 76 | " intensity_output.value = location[\"string\"]\n", 77 | "\n", 78 | "\n", 79 | "nv.load_volumes(\n", 80 | " [\n", 81 | " {\"path\": DATA_FOLDER / \"complex.nii.gz\"},\n", 82 | " ]\n", 83 | ")" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "id": "9fb4ad15-2f7c-41cd-9444-dc9792ff621a", 89 | "metadata": {}, 90 | "source": [ 91 | "# Create other widget" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "id": "a238bbe0-efaa-402b-8242-00a40b4494fa", 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "slice_type_dropdown = widgets.Dropdown(\n", 102 | " options=[\n", 103 | " (\"Axial\", 0),\n", 104 | " (\"Coronal\", 1),\n", 105 | " (\"Sagittal\", 2),\n", 106 | " (\"Render\", 4),\n", 107 | " (\"A+C+S+R\", 3),\n", 108 | " ],\n", 109 | " value=3,\n", 110 | " description=\"Slice Type:\",\n", 111 | ")\n", 112 | "\n", 113 | "\n", 114 | "def on_slice_type_change(change):\n", 115 | " \"\"\"Set slice type.\"\"\"\n", 116 | " nv.set_slice_type(change[\"new\"])\n", 117 | "\n", 118 | "\n", 119 | "slice_type_dropdown.observe(on_slice_type_change, names=\"value\")" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "a0c433b8-9e9c-4c8c-881b-2785b5c2da64", 125 | "metadata": {}, 126 | "source": [ 127 | "# Display all" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "id": "1c5c0e07-9729-41b2-ab33-9d04163a392d", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "display(widgets.VBox([slice_type_dropdown, nv, intensity_output]))" 138 | ] 139 | } 140 | ], 141 | "metadata": {}, 142 | "nbformat": 4, 143 | "nbformat_minor": 5 144 | } 145 | -------------------------------------------------------------------------------- /examples/document.load.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "65d657c7-c5a9-4a9c-b1c4-4d8e6e5290dd", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipywidgets as widgets\n", 11 | "from IPython.display import display\n", 12 | "\n", 13 | "from ipyniivue import NiiVue" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "id": "10b9f3e0-66e8-4b8d-a273-e35ce5dae539", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "nv = NiiVue()\n", 24 | "display(nv)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "d48877dc-3d23-45c6-b6f6-9cc2573fc691", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "# Document URLs to load\n", 35 | "document_urls = [\n", 36 | " \"https://niivue.com/demos/images/niivue.basic.nvd\",\n", 37 | " \"https://niivue.com/demos/images/niivue.drawing.nvd\",\n", 38 | " \"https://niivue.com/demos/images/niivue.mesh.nvd\",\n", 39 | "]\n", 40 | "\n", 41 | "\n", 42 | "# Load the first document by default\n", 43 | "@nv.on_canvas_attached\n", 44 | "def load_first_document():\n", 45 | " \"\"\"Load first document.\"\"\"\n", 46 | " nv.load_document(document_urls[0])" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "id": "e6ec2eb7-865d-4dd5-a649-03ee7fe806d7", 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "# Create preview buttons for documents\n", 57 | "def create_document_button(url, index):\n", 58 | " \"\"\"Create a button for loading a document.\"\"\"\n", 59 | " name = url.split(\"/\")[-1]\n", 60 | " button = widgets.Button(\n", 61 | " description=f\"Load {name}\", layout=widgets.Layout(width=\"auto\", margin=\"2px\")\n", 62 | " )\n", 63 | "\n", 64 | " def on_click(b):\n", 65 | " nv.load_document(url)\n", 66 | " status_output.value = f\"Loaded: {name}\"\n", 67 | "\n", 68 | " button.on_click(on_click)\n", 69 | " return button\n", 70 | "\n", 71 | "\n", 72 | "# Create buttons for each document\n", 73 | "document_buttons = [\n", 74 | " create_document_button(url, i) for i, url in enumerate(document_urls)\n", 75 | "]\n", 76 | "\n", 77 | "# Status display\n", 78 | "status_output = widgets.HTML(value=\"Loaded: niivue.basic.nvd\")" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "41e8f292-0719-4c47-a2d6-f28be679efdc", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# Track hippocampus state\n", 89 | "hippo_state = {\"is_showing\": False}\n", 90 | "hippo_url = \"https://niivue.com/demos/images/hippo.nii.gz\"\n", 91 | "\n", 92 | "\n", 93 | "def toggle_hippocampus(b):\n", 94 | " \"\"\"Toggle the hippocampus overlay.\"\"\"\n", 95 | " if hippo_state[\"is_showing\"]:\n", 96 | " nv.volumes = nv.volumes[:-1]\n", 97 | " b.description = \"Add Hippocampus\"\n", 98 | " hippo_state[\"is_showing\"] = False\n", 99 | " status_output.value = \"Removed hippocampus overlay\"\n", 100 | " else:\n", 101 | " # Add hippocampus\n", 102 | " nv.add_volume({\"url\": hippo_url, \"colormap\": \"bluegrn\", \"opacity\": 0.7})\n", 103 | " b.description = \"Remove Hippocampus\"\n", 104 | " hippo_state[\"is_showing\"] = True\n", 105 | " status_output.value = \"Added hippocampus overlay\"\n", 106 | "\n", 107 | "\n", 108 | "# Create toggle button\n", 109 | "hippo_button = widgets.Button(description=\"Add Hippocampus\", button_style=\"info\")\n", 110 | "hippo_button.on_click(toggle_hippocampus)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "2740219d-9ba6-4d72-adbb-a6acb87cb932", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "def save_document(b):\n", 121 | " \"\"\"Save the current scene as a document.\"\"\"\n", 122 | " nv.save_document(\"my_document.nvd\")\n", 123 | " status_output.value = \"Document saved as 'my_document.nvd'\"\n", 124 | "\n", 125 | "\n", 126 | "# Create save button\n", 127 | "save_button = widgets.Button(description=\"Save Document\", button_style=\"success\")\n", 128 | "save_button.on_click(save_document)" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "id": "0e85641e-6b10-4bd7-ac1b-9db365257810", 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "# Create organized layout\n", 139 | "file_controls = widgets.VBox(\n", 140 | " [\n", 141 | " widgets.HTML(\"

File Operations

\"),\n", 142 | " widgets.HBox([save_button]),\n", 143 | " widgets.HTML(\"

Load Documents

\"),\n", 144 | " widgets.VBox(document_buttons),\n", 145 | " widgets.HTML(\"

Volume Controls

\"),\n", 146 | " widgets.HBox([hippo_button]),\n", 147 | " widgets.HTML(\"

Status

\"),\n", 148 | " status_output,\n", 149 | " ]\n", 150 | ")\n", 151 | "\n", 152 | "# Display controls\n", 153 | "display(file_controls)\n", 154 | "display(nv)" 155 | ] 156 | } 157 | ], 158 | "metadata": {}, 159 | "nbformat": 4, 160 | "nbformat_minor": 5 161 | } 162 | -------------------------------------------------------------------------------- /examples/example_4d.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "094d7d78-087b-4eeb-963a-186d1653b582", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import ipywidgets as widgets\n", 11 | "\n", 12 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 13 | "from ipyniivue.download_dataset import DATA_FOLDER\n", 14 | "\n", 15 | "# GitHub API URL for the base folder\n", 16 | "BASE_API_URL = \"https://niivue.com/demos/images/\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "bca75d90", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Download data for example\n", 27 | "download_dataset(\n", 28 | " BASE_API_URL,\n", 29 | " DATA_FOLDER,\n", 30 | " files=[\n", 31 | " \"mpld_asl.nii.gz\",\n", 32 | " ],\n", 33 | ")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "id": "bf39aa6b", 39 | "metadata": {}, 40 | "source": [ 41 | "Importing nibabel. Since nibabel isn't currently in the ipyniivue requirements, you might need to install it." 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "id": "7332152b-1073-4112-a231-a04d402ae2d0", 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "try:\n", 52 | " import nibabel as nib\n", 53 | "except ModuleNotFoundError:\n", 54 | " !pip install nibabel\n", 55 | " import nibabel as nib" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "id": "66143d2b-af3a-4fcf-9d0c-b5c7ff19931d", 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "example = DATA_FOLDER / \"mpld_asl.nii.gz\"\n", 66 | "volumes = [\n", 67 | " {\n", 68 | " \"path\": example,\n", 69 | " \"colormap\": \"gray\",\n", 70 | " \"visible\": True,\n", 71 | " \"opacity\": 1.0,\n", 72 | " },\n", 73 | "]\n", 74 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 75 | "nv.load_volumes(volumes)\n", 76 | "\n", 77 | "nvols = nib.load(example).shape[-1]\n", 78 | "\n", 79 | "slider = widgets.IntSlider(min=0, max=nvols - 1, description=\"Volume\")\n", 80 | "\n", 81 | "\n", 82 | "def update_frame(*args):\n", 83 | " \"\"\"Select the frame corresponding to the slider value.\"\"\"\n", 84 | " nv.volumes[0].frame_4d = slider.value\n", 85 | "\n", 86 | "\n", 87 | "slider.observe(update_frame, \"value\")\n", 88 | "\n", 89 | "display(slider)\n", 90 | "display(nv)" 91 | ] 92 | } 93 | ], 94 | "metadata": {}, 95 | "nbformat": 4, 96 | "nbformat_minor": 5 97 | } 98 | -------------------------------------------------------------------------------- /examples/example_sideview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "52de1125", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# This is not part of IPyNiiVue dependencies so it has to be installed independently\n", 11 | "!pip install ipylab" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "b1cbb154-3c30-4d61-afea-8f7e42ee65b9", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from pathlib import Path\n", 22 | "\n", 23 | "from ipylab import JupyterFrontEnd, SplitPanel\n", 24 | "\n", 25 | "import ipyniivue\n", 26 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 27 | "\n", 28 | "# GitHub API URL for the base folder\n", 29 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 30 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "6291cd2c-0674-4ba3-a912-75ff6ca7fe78", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "# Download data for example\n", 41 | "download_dataset(\n", 42 | " BASE_API_URL,\n", 43 | " DATA_FOLDER,\n", 44 | " files=[\n", 45 | " \"mni152.nii.gz\",\n", 46 | " \"hippo.nii.gz\",\n", 47 | " ],\n", 48 | ")" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "74af1303-22e9-4933-9a13-de80b2109067", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "# this example needs to have ipylab installed and only runs in JupyterLab.\n", 59 | "volumes = [\n", 60 | " {\n", 61 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 62 | " \"colormap\": \"gray\",\n", 63 | " \"visible\": True,\n", 64 | " \"opacity\": 1.0,\n", 65 | " },\n", 66 | " {\n", 67 | " \"path\": DATA_FOLDER / \"hippo.nii.gz\",\n", 68 | " \"colormap\": \"red\",\n", 69 | " \"visible\": True,\n", 70 | " \"opacity\": 1,\n", 71 | " },\n", 72 | "]\n", 73 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 74 | "nv.load_volumes(volumes)\n", 75 | "\n", 76 | "\n", 77 | "app = JupyterFrontEnd()\n", 78 | "split_panel = SplitPanel()\n", 79 | "\n", 80 | "# add the widgets to the split panel\n", 81 | "split_panel.children = [nv]\n", 82 | "\n", 83 | "split_panel.title.label = \"NiiVue SplitPanel\"\n", 84 | "split_panel.title.icon_class = \"jp-PythonIcon\"\n", 85 | "split_panel.title.closable = True\n", 86 | "app.shell.add(split_panel, \"main\", {\"mode\": \"split-right\"})\n", 87 | "print(\"Split panel created!\")" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "id": "97c74b53-7f9f-45b4-9608-b594d564b8cd", 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "nv.volumes[0].opacity = 0.5\n", 98 | "nv.volumes[1].colormap = \"green\"" 99 | ] 100 | } 101 | ], 102 | "metadata": {}, 103 | "nbformat": 4, 104 | "nbformat_minor": 5 105 | } 106 | -------------------------------------------------------------------------------- /examples/mask.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "a7560ca5-f0bc-494d-abb0-477fb3944821", 6 | "metadata": {}, 7 | "source": [ 8 | "Prepare images" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "e1a27d89-e050-47b9-b959-1fb51e39098e", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from pathlib import Path\n", 19 | "\n", 20 | "import ipyniivue\n", 21 | "\n", 22 | "# GitHub API URL for the base folder\n", 23 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 24 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "c30f3c8d-28b5-4d0a-9883-a0d22f4f51e5", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "# Download data for example\n", 35 | "ipyniivue.download_dataset(\n", 36 | " BASE_API_URL,\n", 37 | " DATA_FOLDER,\n", 38 | " files=[\n", 39 | " \"fslmean.nii.gz\",\n", 40 | " \"fslt.nii.gz\",\n", 41 | " ],\n", 42 | ")" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "id": "f91d30c7-3cf5-48e7-97eb-f76ba165eadf", 48 | "metadata": {}, 49 | "source": [ 50 | "Imports" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "id": "67a84d95-df0a-4f24-ad0b-27fc4a724cc3", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "# Import necessary libraries\n", 61 | "import ipywidgets as widgets\n", 62 | "from IPython.display import display\n", 63 | "\n", 64 | "from ipyniivue import NiiVue, SliceType" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "id": "4348595a-6674-48e5-8ef0-04e73332e87c", 70 | "metadata": {}, 71 | "source": [ 72 | "Create niivue instance" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "1af1bf1b-9033-4fed-800c-856180de5fa4", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "# Create a NiiVue instance with specific options\n", 83 | "nv = NiiVue(show_3d_crosshair=True)\n", 84 | "\n", 85 | "# Load the volumes\n", 86 | "volumes = [\n", 87 | " {\n", 88 | " \"path\": DATA_FOLDER / \"fslmean.nii.gz\",\n", 89 | " \"colormap\": \"gray\",\n", 90 | " \"opacity\": 1.0,\n", 91 | " \"visible\": True,\n", 92 | " },\n", 93 | " {\n", 94 | " \"path\": DATA_FOLDER / \"fslt.nii.gz\",\n", 95 | " \"colormap\": \"redyell\",\n", 96 | " \"cal_min\": 0.05,\n", 97 | " \"cal_max\": 5.05,\n", 98 | " \"opacity\": 0.9,\n", 99 | " \"visible\": True,\n", 100 | " },\n", 101 | "]\n", 102 | "\n", 103 | "nv.load_volumes(volumes)\n", 104 | "\n", 105 | "# Set the slice type to render (3D view)\n", 106 | "nv.set_slice_type(SliceType.RENDER)\n", 107 | "\n", 108 | "# Set the clip plane\n", 109 | "nv.set_clip_plane(0.15, 270, 0)\n", 110 | "\n", 111 | "# Set the render azimuth and elevation\n", 112 | "nv.set_render_azimuth_elevation(45, 45)" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "id": "3257ee16-f7aa-48c4-a191-95616a44a9db", 118 | "metadata": {}, 119 | "source": [ 120 | "Create interactive checkbox" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "id": "9946de46-fff6-4b24-8de8-739f1bac8d32", 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "# Create a checkbox to toggle background masks overlays\n", 131 | "background_masks_checkbox = widgets.Checkbox(\n", 132 | " value=False,\n", 133 | " description=\"Background masks overlay\",\n", 134 | " disabled=False,\n", 135 | ")\n", 136 | "\n", 137 | "\n", 138 | "# Function to handle checkbox changes\n", 139 | "def on_background_masks_change(change):\n", 140 | " \"\"\"Set background mask overlay.\"\"\"\n", 141 | " nv.background_masks_overlays = change.new\n", 142 | "\n", 143 | "\n", 144 | "# Observe changes to the checkbox\n", 145 | "background_masks_checkbox.observe(on_background_masks_change, names=\"value\")" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "id": "930e164e-7f58-400e-8102-94502dce8076", 151 | "metadata": {}, 152 | "source": [ 153 | "Display all" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "id": "79df762c-d512-4a00-a33a-a5fd6ca22840", 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "display(background_masks_checkbox)\n", 164 | "display(nv)" 165 | ] 166 | } 167 | ], 168 | "metadata": {}, 169 | "nbformat": 4, 170 | "nbformat_minor": 5 171 | } 172 | -------------------------------------------------------------------------------- /examples/mesh.4d.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "479d3a4f-9821-494c-8784-d1cbf85e91cf", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipyniivue\n", 13 | "\n", 14 | "# GitHub API URL for the base folder\n", 15 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 16 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "119c091d-8a98-4b0b-97c6-d5b724d67f80", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Download data for example\n", 27 | "ipyniivue.download_dataset(\n", 28 | " BASE_API_URL,\n", 29 | " DATA_FOLDER,\n", 30 | " files=[\n", 31 | " \"Human.colin.Cerebral.R.VERY_INFLATED.71723.surf.gii\",\n", 32 | " \"Human.colin.R.FUNCTIONAL.71723.func.gii\",\n", 33 | " ],\n", 34 | ")" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "id": "45ba4675-32cd-4a89-ab5e-33eec2597d26", 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "import ipywidgets as widgets\n", 45 | "from IPython.display import display\n", 46 | "\n", 47 | "from ipyniivue import NiiVue, SliceType\n", 48 | "\n", 49 | "nv = NiiVue(\n", 50 | " show_3d_crosshair=True,\n", 51 | " back_color=(0.9, 0.9, 1, 1),\n", 52 | ")\n", 53 | "nv.set_slice_type(SliceType.RENDER)\n", 54 | "\n", 55 | "# Load meshes\n", 56 | "mesh_layers = [\n", 57 | " {\n", 58 | " \"path\": DATA_FOLDER / \"Human.colin.R.FUNCTIONAL.71723.func.gii\",\n", 59 | " \"colormap\": \"rocket\",\n", 60 | " \"opacity\": 0.7,\n", 61 | " },\n", 62 | "]\n", 63 | "\n", 64 | "meshes = [\n", 65 | " {\n", 66 | " \"path\": DATA_FOLDER / \"Human.colin.Cerebral.R.VERY_INFLATED.71723.surf.gii\",\n", 67 | " \"rgba255\": [255, 255, 255, 255],\n", 68 | " \"layers\": mesh_layers,\n", 69 | " },\n", 70 | "]\n", 71 | "nv.load_meshes(meshes)\n", 72 | "\n", 73 | "# Set the clip plane\n", 74 | "nv.set_clip_plane(-0.1, 270, 0)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "44cffb51-66b6-4668-be46-78bd45ff9bfe", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "# Timepoint Slider\n", 85 | "slider_timepoint = widgets.IntSlider(min=0, max=1, value=0, description=\"Timepoint\")\n", 86 | "\n", 87 | "\n", 88 | "def on_timepoint_change(change):\n", 89 | " \"\"\"Set mesh layer frame 4D.\"\"\"\n", 90 | " nv.set_mesh_layer_property(nv.meshes[0].id, 0, \"frame_4d\", change[\"new\"])\n", 91 | "\n", 92 | "\n", 93 | "slider_timepoint.observe(on_timepoint_change, names=\"value\")\n", 94 | "\n", 95 | "# Opacity Slider\n", 96 | "slider_opacity = widgets.IntSlider(min=1, max=10, value=7, description=\"Opacity\")\n", 97 | "\n", 98 | "\n", 99 | "def on_opacity_change(change):\n", 100 | " \"\"\"Set mesh layer opacity.\"\"\"\n", 101 | " nv.set_mesh_layer_property(nv.meshes[0].id, 0, \"opacity\", change[\"new\"] * 0.1)\n", 102 | "\n", 103 | "\n", 104 | "slider_opacity.observe(on_opacity_change, names=\"value\")" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "id": "475cf9fe-f1a6-4d87-b8a7-4d2e5995289f", 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "shader_options = nv.mesh_shader_names()\n", 115 | "\n", 116 | "shader_dropdown = widgets.Dropdown(\n", 117 | " options=shader_options,\n", 118 | " value=\"Phong\", # Default shader\n", 119 | " description=\"Shader:\",\n", 120 | ")\n", 121 | "\n", 122 | "\n", 123 | "def on_shader_change(change):\n", 124 | " \"\"\"Set mesh layer shader.\"\"\"\n", 125 | " nv.set_mesh_shader(nv.meshes[0].id, change[\"new\"])\n", 126 | "\n", 127 | "\n", 128 | "shader_dropdown.observe(on_shader_change, names=\"value\")" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "id": "f3f74d15-aeaa-42fa-bf86-bf1a8c4758f2", 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "controls = widgets.HBox([slider_timepoint, slider_opacity, shader_dropdown])" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "id": "c89e658b-fd33-46f6-b74f-6ec47b25b514", 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "display(controls)\n", 149 | "display(nv)" 150 | ] 151 | } 152 | ], 153 | "metadata": {}, 154 | "nbformat": 4, 155 | "nbformat_minor": 5 156 | } 157 | -------------------------------------------------------------------------------- /examples/mesh.matcap.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ef04a141", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "d1950524-21ee-4281-8909-4b23a6734143", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import pathlib\n", 19 | "\n", 20 | "import ipywidgets as widgets\n", 21 | "\n", 22 | "import ipyniivue\n", 23 | "from ipyniivue import NiiVue, SliceType, download_dataset" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "id": "51a48f6d", 29 | "metadata": {}, 30 | "source": [ 31 | "# Download required data" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "e8f5d640", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 42 | "\n", 43 | "# Meshes\n", 44 | "download_dataset(\n", 45 | " \"https://niivue.com/demos/images/\",\n", 46 | " dest_folder=DATA_FOLDER,\n", 47 | " files=[\n", 48 | " \"BrainMesh_ICBM152.lh.mz3\",\n", 49 | " \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 50 | " ],\n", 51 | ")\n", 52 | "\n", 53 | "# Matcaps\n", 54 | "download_dataset(\n", 55 | " api_url=\"https://niivue.com/demos/matcaps\",\n", 56 | " dest_folder=DATA_FOLDER / \"matcaps\",\n", 57 | " files=[\n", 58 | " \"Shiny.jpg\",\n", 59 | " \"Cortex.jpg\",\n", 60 | " \"Cream.jpg\",\n", 61 | " \"Fuzzy.jpg\",\n", 62 | " \"Peach.jpg\",\n", 63 | " \"Plastic.jpg\",\n", 64 | " \"Gold.jpg\",\n", 65 | " ],\n", 66 | ")" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "7087b6f4", 72 | "metadata": {}, 73 | "source": [ 74 | "# Create niivue instance" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "1269f9f8-0f3f-441d-ac79-8e92e3021281", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "nv = NiiVue(\n", 85 | " show_3d_crosshair=True,\n", 86 | " back_color=(1, 1, 1, 1),\n", 87 | ")\n", 88 | "\n", 89 | "nv.set_slice_type(SliceType.RENDER)\n", 90 | "nv.opts.is_colorbar = True" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "id": "3af1bc26", 96 | "metadata": {}, 97 | "source": [ 98 | "# Load meshes and mesh layers" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "id": "1b38ade7-7484-4625-a02e-be4855cd8db4", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "mesh_layer = {\n", 109 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 110 | " \"cal_min\": 2,\n", 111 | " \"cal_max\": 5,\n", 112 | " \"use_negative_cmap\": True,\n", 113 | " \"opacity\": 0.7,\n", 114 | "}\n", 115 | "\n", 116 | "nv.load_meshes(\n", 117 | " [\n", 118 | " {\n", 119 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.mz3\",\n", 120 | " \"layers\": [mesh_layer],\n", 121 | " },\n", 122 | " ]\n", 123 | ")\n", 124 | "\n", 125 | "nv.set_mesh_shader(nv.meshes[0].id, \"Matcap\")\n", 126 | "nv.set_clip_plane(-0.1, 270, 0)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "9b17f707", 132 | "metadata": {}, 133 | "source": [ 134 | "# Add extra widgets and setup observers" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "id": "e8a67c91-15a4-4b7b-8192-b26cba349375", 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "threshold_slider = widgets.IntSlider(\n", 145 | " value=20,\n", 146 | " min=1,\n", 147 | " max=49,\n", 148 | " description=\"Threshold\",\n", 149 | ")\n", 150 | "\n", 151 | "matcap_options = [\"Shiny\", \"Cortex\", \"Cream\", \"Fuzzy\", \"Peach\", \"Plastic\", \"Gold\"]\n", 152 | "matcap_dropdown = widgets.Dropdown(\n", 153 | " options=matcap_options,\n", 154 | " value=\"Shiny\",\n", 155 | " description=\"MatCap\",\n", 156 | ")\n", 157 | "\n", 158 | "\n", 159 | "def on_threshold_change(change):\n", 160 | " \"\"\"Set mesh layer property cal_min.\"\"\"\n", 161 | " nv.set_mesh_layer_property(\n", 162 | " mesh_id=nv.meshes[0].id,\n", 163 | " layer_index=0,\n", 164 | " attribute=\"cal_min\",\n", 165 | " value=change[\"new\"] * 0.1,\n", 166 | " )\n", 167 | "\n", 168 | "\n", 169 | "threshold_slider.observe(on_threshold_change, names=\"value\")\n", 170 | "\n", 171 | "\n", 172 | "def on_matcap_change(change):\n", 173 | " \"\"\"Load matcap texture.\"\"\"\n", 174 | " nv.set_mesh_shader(nv.meshes[0].id, \"Matcap\")\n", 175 | " matcap_name = change[\"new\"]\n", 176 | " matcap_path = DATA_FOLDER / \"matcaps\" / f\"{matcap_name}.jpg\"\n", 177 | " with open(matcap_path, \"rb\") as f:\n", 178 | " matcap_data = f.read()\n", 179 | " nv.load_mat_cap_texture(matcap_data)\n", 180 | "\n", 181 | "\n", 182 | "matcap_dropdown.observe(on_matcap_change, names=\"value\")" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "id": "a4c45439", 188 | "metadata": {}, 189 | "source": [ 190 | "# Display all" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "id": "1254b5a8", 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "widgets.VBox(\n", 201 | " [\n", 202 | " widgets.HBox([threshold_slider, matcap_dropdown]),\n", 203 | " nv,\n", 204 | " ]\n", 205 | ")" 206 | ] 207 | } 208 | ], 209 | "metadata": {}, 210 | "nbformat": 4, 211 | "nbformat_minor": 5 212 | } 213 | -------------------------------------------------------------------------------- /examples/mesh_and_volume.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "593d466e-6bf6-47df-ba63-23b627f41da0", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipyniivue\n", 13 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 14 | "\n", 15 | "# GitHub API URL for the base folder\n", 16 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 17 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "a09b7f2a-d0f7-4337-bfcc-ce5152fa9e79", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# Download data for example\n", 28 | "download_dataset(\n", 29 | " BASE_API_URL,\n", 30 | " DATA_FOLDER,\n", 31 | " files=[\n", 32 | " \"mni152.nii.gz\",\n", 33 | " \"BrainMesh_ICBM152.lh.mz3\",\n", 34 | " ],\n", 35 | ")" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "id": "71c2f1ec-fdbf-4aae-8d09-77fd5988e11c", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "# based on https://github.com/niivue/ipyniivue/blob/main/original_gallery.md#tractography-tck-trk-trx-vtk\n", 46 | "\n", 47 | "volumes = [\n", 48 | " {\n", 49 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 50 | " \"colormap\": \"gray\",\n", 51 | " \"visible\": True,\n", 52 | " \"opacity\": 1.0,\n", 53 | " },\n", 54 | "]\n", 55 | "\n", 56 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 57 | "nv.load_volumes(volumes)\n", 58 | "\n", 59 | "nv.load_meshes(\n", 60 | " [\n", 61 | " {\n", 62 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.mz3\",\n", 63 | " \"rgba255\": [255, 255, 255, 255],\n", 64 | " }\n", 65 | " ]\n", 66 | ")\n", 67 | "\n", 68 | "\n", 69 | "nv" 70 | ] 71 | } 72 | ], 73 | "metadata": {}, 74 | "nbformat": 4, 75 | "nbformat_minor": 5 76 | } 77 | -------------------------------------------------------------------------------- /examples/mesh_layers.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "69a4c0b3", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipyniivue\n", 13 | "from ipyniivue import NiiVue, download_dataset\n", 14 | "\n", 15 | "# GitHub API URL for the base folder\n", 16 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 17 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "eaca0a33-87c7-437f-b9b5-a0c967a44faf", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# Download data for example\n", 28 | "download_dataset(\n", 29 | " BASE_API_URL,\n", 30 | " DATA_FOLDER,\n", 31 | " files=[\n", 32 | " \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 33 | " \"BrainMesh_ICBM152.lh.mz3\",\n", 34 | " \"CIT168.mz3\",\n", 35 | " ],\n", 36 | ")" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "36d80248-e8a6-4f27-92c5-98dabf2aec81", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "nv = NiiVue(\n", 47 | " show_3d_crosshair=True, back_color=(1, 1, 1, 1), mesh_xray=0.3, is_colorbar=True\n", 48 | ")\n", 49 | "\n", 50 | "mesh_layer = {\n", 51 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.motor.mz3\",\n", 52 | " \"cal_min\": 0.5,\n", 53 | " \"cal_max\": 5.5,\n", 54 | " \"use_negative_cmap\": True,\n", 55 | " \"opacity\": 0.7,\n", 56 | "}\n", 57 | "\n", 58 | "nv.load_meshes(\n", 59 | " [\n", 60 | " {\n", 61 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.mz3\",\n", 62 | " \"rgba255\": [255, 255, 255, 255],\n", 63 | " \"layers\": [mesh_layer],\n", 64 | " },\n", 65 | " {\"path\": DATA_FOLDER / \"CIT168.mz3\", \"rgba255\": [0, 0, 255, 255]},\n", 66 | " ]\n", 67 | ")\n", 68 | "\n", 69 | "nv" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "id": "956fe2ef-3180-44cc-a81e-05efc783ee05", 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "nv.meshes[0].rgba255 = [0, 200, 200, 200]" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "id": "7ae05143-ddfd-4ac5-824b-4c579730f016", 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "nv.meshes[0].layers[0].opacity = 0.2" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "id": "21b63939-9a36-48eb-83d6-76941fb6d7e1", 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "nv.set_mesh_layer_property(nv.meshes[0].id, 0, \"opacity\", 0.8)" 100 | ] 101 | } 102 | ], 103 | "metadata": {}, 104 | "nbformat": 4, 105 | "nbformat_minor": 5 106 | } 107 | -------------------------------------------------------------------------------- /examples/mosaics1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "7d5def93-aba0-4da7-881f-94f5119e858d", 6 | "metadata": {}, 7 | "source": [ 8 | "# Voxel Mosaics 1\n", 9 | "\n", 10 | "Choose axial (A), coronal (C) or sagittal (S) slices. Modify with cross slices (X), renderings (R), and horizontal overlap (H).\n", 11 | "\n", 12 | "See https://niivue.com/demos/features/mosaics.html for mirror." 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "09a54079-77fc-4986-9c30-d73b149b9d9e", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import ipywidgets as widgets\n", 23 | "from IPython.display import display\n", 24 | "\n", 25 | "from ipyniivue import NiiVue" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "id": "a6112177-7f66-4ca2-a14e-99af9cd202f0", 31 | "metadata": {}, 32 | "source": [ 33 | "## Create NiiVue Instance and Load Data" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "bb0bfa17-379b-4c74-886b-2552dd8754cb", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "nv = NiiVue(\n", 44 | " is_colorbar=True,\n", 45 | " is_alpha_clip_dark=True,\n", 46 | " center_mosaic=True,\n", 47 | " back_color=(1, 1, 1, 1),\n", 48 | ")\n", 49 | "\n", 50 | "nv.load_volumes(\n", 51 | " [\n", 52 | " {\n", 53 | " \"url\": \"https://niivue.com/demos/images/mni152.nii.gz\",\n", 54 | " \"colorbar_visible\": False,\n", 55 | " },\n", 56 | " {\n", 57 | " \"url\": \"https://niivue.com/demos/images/spmMotor.nii.gz\",\n", 58 | " \"cal_min\": 4.2,\n", 59 | " \"cal_max\": 8,\n", 60 | " \"colormap\": \"warm\",\n", 61 | " \"colormap_negative\": \"winter\",\n", 62 | " },\n", 63 | " ]\n", 64 | ")\n", 65 | "\n", 66 | "initial_mosaic = \"A -20 50 60 70 C -10 -20 -50 S R X 0 R X -0\"\n", 67 | "nv.set_slice_mosaic_string(initial_mosaic)\n", 68 | "\n", 69 | "\n", 70 | "@nv.on_canvas_attached\n", 71 | "def setup_illumination():\n", 72 | " \"\"\"Set volume render illumination after canvas attached.\"\"\"\n", 73 | " nv.set_volume_render_illumination(0.3)" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "id": "0c8b9382-ae77-47be-a452-ba02e59a5d71", 79 | "metadata": {}, 80 | "source": [ 81 | "## Create Interactive Controls" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "id": "0a73edb3-ead1-4eac-a03c-f80de5b939be", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "# mosaic string input\n", 92 | "mosaic_text = widgets.Text(\n", 93 | " value=initial_mosaic,\n", 94 | " placeholder=\"Enter mosaic string\",\n", 95 | " description=\"Mosaic String:\",\n", 96 | " style={\"description_width\": \"initial\"},\n", 97 | " layout=widgets.Layout(width=\"600px\"),\n", 98 | ")\n", 99 | "\n", 100 | "# help button\n", 101 | "help_button = widgets.Button(\n", 102 | " description=\"Help\",\n", 103 | " button_style=\"info\",\n", 104 | " tooltip=\"Click for information about mosaic strings\",\n", 105 | " layout=widgets.Layout(width=\"80px\"),\n", 106 | ")\n", 107 | "\n", 108 | "# outline slider\n", 109 | "outline_slider = widgets.IntSlider(\n", 110 | " min=0,\n", 111 | " max=8,\n", 112 | " value=0,\n", 113 | " description=\"Outline:\",\n", 114 | " continuous_update=True,\n", 115 | ")\n", 116 | "\n", 117 | "# alpha mode dropdown\n", 118 | "alpha_dropdown = widgets.Dropdown(\n", 119 | " options=[\n", 120 | " (\"Restrict colorbar to range\", 0),\n", 121 | " (\"Colorbar from 0, transparent subthreshold\", 1),\n", 122 | " (\"Colorbar from 0, translucent subthreshold\", 2),\n", 123 | " ],\n", 124 | " value=0,\n", 125 | " description=\"Alpha Mode:\",\n", 126 | " style={\"description_width\": \"initial\"},\n", 127 | " layout=widgets.Layout(width=\"400px\"),\n", 128 | ")\n", 129 | "\n", 130 | "# glossy rendering dropdown\n", 131 | "glossy_dropdown = widgets.Dropdown(\n", 132 | " options=[\n", 133 | " (\"Slices\", -1),\n", 134 | " (\"Matte\", 0),\n", 135 | " (\"Low\", 0.3),\n", 136 | " (\"Medium\", 0.6),\n", 137 | " (\"High\", 1.0),\n", 138 | " ],\n", 139 | " value=0.3,\n", 140 | " description=\"Glossy:\",\n", 141 | " style={\"description_width\": \"initial\"},\n", 142 | ")\n", 143 | "\n", 144 | "# dark mode checkbox\n", 145 | "dark_checkbox = widgets.Checkbox(\n", 146 | " value=False,\n", 147 | " description=\"Dark Mode\",\n", 148 | ")\n", 149 | "\n", 150 | "# save button\n", 151 | "save_button = widgets.Button(\n", 152 | " description=\"Save Bitmap\",\n", 153 | " button_style=\"success\",\n", 154 | " tooltip=\"Save current view as PNG\",\n", 155 | ")\n", 156 | "\n", 157 | "# output for messages\n", 158 | "output = widgets.Output()" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "id": "b69f5c09-bd2b-49c0-93a1-a4e82aface43", 164 | "metadata": {}, 165 | "source": [ 166 | "## Setup Event Handlers" 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": null, 172 | "id": "f02cfe96-fe8e-4483-bfc9-8b4bf0422e7f", 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "def on_mosaic_change(change):\n", 177 | " \"\"\"Handle mosaic string changes.\"\"\"\n", 178 | " nv.set_slice_mosaic_string(change[\"new\"])\n", 179 | "\n", 180 | "\n", 181 | "def on_help_click(b):\n", 182 | " \"\"\"Display help information.\"\"\"\n", 183 | " with output:\n", 184 | " output.clear_output()\n", 185 | " print(\"Mosaic String Help:\")\n", 186 | " print(\"===================\")\n", 187 | " print(\"Choose axial (A), coronal (C) or sagittal (S) slices.\")\n", 188 | " print(\"Modify with:\")\n", 189 | " print(\" - X: cross slices\")\n", 190 | " print(\" - R: renderings\")\n", 191 | " print(\" - H: horizontal overlap (e.g., H 0.3)\")\n", 192 | " print(\"\")\n", 193 | " print(\"Example: 'A -20 50 60 70 C -10 -20 -50 S R X 0 R X -0'\")\n", 194 | " print(\"This creates axial slices at positions -20, 50, 60, 70,\")\n", 195 | " print(\"coronal slices at -10, -20, -50, a sagittal slice,\")\n", 196 | " print(\"and renderings with cross sections.\")\n", 197 | "\n", 198 | "\n", 199 | "def on_outline_change(change):\n", 200 | " \"\"\"Handle outline width changes.\"\"\"\n", 201 | " nv.overlay_outline_width = 0.25 * change[\"new\"]\n", 202 | "\n", 203 | "\n", 204 | "def on_alpha_change(change):\n", 205 | " \"\"\"Handle alpha mode changes.\"\"\"\n", 206 | " nv.volumes[1].colormap_type = change[\"new\"]\n", 207 | "\n", 208 | "\n", 209 | "def on_glossy_change(change):\n", 210 | " \"\"\"Handle glossy rendering changes.\"\"\"\n", 211 | " value = float(change[\"new\"])\n", 212 | " nv.set_volume_render_illumination(value)\n", 213 | "\n", 214 | "\n", 215 | "def on_dark_change(change):\n", 216 | " \"\"\"Handle dark mode toggle.\"\"\"\n", 217 | " if change[\"new\"]:\n", 218 | " nv.opts.back_color = (0, 0, 0, 1)\n", 219 | " else:\n", 220 | " nv.opts.back_color = (1, 1, 1, 1)\n", 221 | "\n", 222 | "\n", 223 | "def on_save_click(b):\n", 224 | " \"\"\"Save the current scene.\"\"\"\n", 225 | " with output:\n", 226 | " output.clear_output()\n", 227 | " nv.save_scene(\"mosaic_screenshot.png\")\n", 228 | " print(\"Scene saved as 'mosaic_screenshot.png'\")\n", 229 | "\n", 230 | "\n", 231 | "# attach event handlers\n", 232 | "mosaic_text.observe(on_mosaic_change, names=\"value\")\n", 233 | "help_button.on_click(on_help_click)\n", 234 | "outline_slider.observe(on_outline_change, names=\"value\")\n", 235 | "alpha_dropdown.observe(on_alpha_change, names=\"value\")\n", 236 | "glossy_dropdown.observe(on_glossy_change, names=\"value\")\n", 237 | "dark_checkbox.observe(on_dark_change, names=\"value\")\n", 238 | "save_button.on_click(on_save_click)\n", 239 | "\n", 240 | "# initialize values\n", 241 | "on_outline_change({\"new\": outline_slider.value})\n", 242 | "on_alpha_change({\"new\": alpha_dropdown.value})" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "id": "7e61aaaf-427f-49a2-a484-40404d17a7af", 248 | "metadata": {}, 249 | "source": [ 250 | "## Display All" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": null, 256 | "id": "18c02aeb-ffbf-478c-8690-c3046aa4602a", 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "# organize controls\n", 261 | "mosaic_row = widgets.HBox([mosaic_text, help_button])\n", 262 | "controls_row1 = widgets.HBox([outline_slider, alpha_dropdown])\n", 263 | "controls_row2 = widgets.HBox([glossy_dropdown, dark_checkbox, save_button])\n", 264 | "\n", 265 | "# create main layout\n", 266 | "controls = widgets.VBox([mosaic_row, controls_row1, controls_row2, output])\n", 267 | "\n", 268 | "# display everything\n", 269 | "display(widgets.VBox([controls, nv]))" 270 | ] 271 | } 272 | ], 273 | "metadata": {}, 274 | "nbformat": 4, 275 | "nbformat_minor": 5 276 | } 277 | -------------------------------------------------------------------------------- /examples/on_event.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "93bdb4d8-14a4-42ba-b80f-554f823c9c6a", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from ipyniivue import NiiVue, download_dataset\n", 11 | "from ipyniivue.download_dataset import DATA_FOLDER\n", 12 | "\n", 13 | "# GitHub API URL for the base folder\n", 14 | "BASE_API_URL = \"https://niivue.com/demos/images/\"" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "id": "3138317c", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# Download data for example\n", 25 | "download_dataset(\n", 26 | " BASE_API_URL,\n", 27 | " DATA_FOLDER,\n", 28 | " files=[\n", 29 | " \"mni152.nii.gz\",\n", 30 | " \"hippo.nii.gz\",\n", 31 | " ],\n", 32 | ")" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "7dea48af-cb5f-4a1c-afe4-46ae3c83cd1d", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "from IPython.display import display\n", 43 | "from ipywidgets import Output\n", 44 | "\n", 45 | "from ipyniivue import SliceType\n", 46 | "\n", 47 | "out = Output()\n", 48 | "display(out)\n", 49 | "\n", 50 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 51 | "\n", 52 | "\n", 53 | "@nv.on_image_loaded\n", 54 | "def on_image_loaded(volume):\n", 55 | " \"\"\"\n", 56 | " Event handler called when an image is loaded.\n", 57 | "\n", 58 | " Parameters\n", 59 | " ----------\n", 60 | " volume : ipyniivue.Volume\n", 61 | " The loaded image volume.\n", 62 | " \"\"\"\n", 63 | " with out:\n", 64 | " print(\"Image loaded:\", volume)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "d70957d1-9fa5-4810-82fc-c3be4b48d17b", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# Load volumes\n", 75 | "nv.load_volumes(\n", 76 | " [\n", 77 | " {\n", 78 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 79 | " \"colormap\": \"gray\",\n", 80 | " \"visible\": True,\n", 81 | " \"opacity\": 1.0,\n", 82 | " },\n", 83 | " {\n", 84 | " \"path\": DATA_FOLDER / \"hippo.nii.gz\",\n", 85 | " \"colormap\": \"red\",\n", 86 | " \"visible\": True,\n", 87 | " \"opacity\": 1,\n", 88 | " },\n", 89 | " ]\n", 90 | ")\n", 91 | "\n", 92 | "# Display the widget\n", 93 | "nv" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "id": "a8f78736-184d-43a2-b93f-fc8fe5d899e7", 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "nv.add_volume(\n", 104 | " {\n", 105 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 106 | " \"colormap\": \"gray\",\n", 107 | " \"visible\": True,\n", 108 | " \"opacity\": 1.0,\n", 109 | " }\n", 110 | ")" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "afad59f1-1cf1-4e95-8df3-8a0e1f155916", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "out" 121 | ] 122 | } 123 | ], 124 | "metadata": {}, 125 | "nbformat": 4, 126 | "nbformat_minor": 5 127 | } 128 | -------------------------------------------------------------------------------- /examples/prototypes/meshes_(GIfTI, FreeSurfer, MZ3, OBJ, STL, legacy VTK).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from pathlib import Path\n", 10 | "\n", 11 | "import ipyniivue\n", 12 | "from ipyniivue import NiiVue, download_dataset\n", 13 | "\n", 14 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 15 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# Download data for example\n", 25 | "download_dataset(\n", 26 | " BASE_API_URL,\n", 27 | " DATA_FOLDER,\n", 28 | " files=[\n", 29 | " \"BrainMesh_ICBM152.lh.mz3\",\n", 30 | " \"CIT168.mz3\",\n", 31 | " ],\n", 32 | ")" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "# based on https://github.com/niivue/ipyniivue/blob/main/original_gallery.md#meshes-gifti-freesurfer-mz3-obj-stl-legacy-vtk\n", 42 | "nv = NiiVue(\n", 43 | " show_3d_crosshair=True, back_color=(1, 1, 1, 1), mesh_xray=0.3, is_colorbar=True\n", 44 | ")\n", 45 | "\n", 46 | "nv.load_meshes(\n", 47 | " [\n", 48 | " {\n", 49 | " \"path\": DATA_FOLDER / \"BrainMesh_ICBM152.lh.mz3\",\n", 50 | " \"rgba255\": [222, 164, 164, 255],\n", 51 | " },\n", 52 | " {\"path\": DATA_FOLDER / \"CIT168.mz3\", \"rgba255\": [0, 0, 255, 255]},\n", 53 | " ]\n", 54 | ")\n", 55 | "\n", 56 | "nv\n", 57 | "\n", 58 | "# nv1.setMeshShader(nv1.meshes[0].id, \"Outline\"); does not seem to exist yet." 59 | ] 60 | } 61 | ], 62 | "metadata": {}, 63 | "nbformat": 4, 64 | "nbformat_minor": 4 65 | } 66 | -------------------------------------------------------------------------------- /examples/prototypes/torso_regions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "1794ac1d-e7e7-405b-9d83-8a61082431a8", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipyniivue\n", 13 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 14 | "\n", 15 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 16 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "6e7d22d8-9ed3-4ad3-8f21-cb2f33292ae4", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Download data for example\n", 27 | "download_dataset(\n", 28 | " BASE_API_URL,\n", 29 | " DATA_FOLDER,\n", 30 | " files=[\n", 31 | " \"torso.nii.gz\",\n", 32 | " ],\n", 33 | ")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "71c2f1ec-fdbf-4aae-8d09-77fd5988e11c", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "# based on https://github.com/niivue/ipyniivue/blob/main/original_gallery.md#torso-regions\n", 44 | "\n", 45 | "volumes = [\n", 46 | " {\n", 47 | " \"path\": DATA_FOLDER / \"torso.nii.gz\",\n", 48 | " \"colormap\": (0, 0, 0, 0),\n", 49 | " \"visible\": True,\n", 50 | " \"opacity\": 1.0,\n", 51 | " },\n", 52 | "]\n", 53 | "\n", 54 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 55 | "nv.load_volumes(volumes)\n", 56 | "\n", 57 | "nv" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "593d466e-6bf6-47df-ba63-23b627f41da0", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "cmap = {\n", 68 | " \"R\": [0, 0, 185, 185, 252, 0, 103, 216, 127, 127, 0, 222],\n", 69 | " \"G\": [0, 20, 102, 102, 0, 255, 76, 132, 0, 127, 255, 154],\n", 70 | " \"B\": [0, 152, 83, 83, 0, 0, 71, 105, 127, 0, 255, 132],\n", 71 | " \"A\": [0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],\n", 72 | " \"labels\": [\n", 73 | " \"background\",\n", 74 | " \"1spleen\",\n", 75 | " \"2kidneyR\",\n", 76 | " \"3kidneyL\",\n", 77 | " \"4gallbladder\",\n", 78 | " \"5esophagus\",\n", 79 | " \"6Liver\",\n", 80 | " \"7stomach\",\n", 81 | " \"8aorta\",\n", 82 | " \"9inferiorvenacava\",\n", 83 | " \"10pancreas\",\n", 84 | " \"11bladder\",\n", 85 | " ],\n", 86 | "}\n", 87 | "nv.setDrawColormap(cmap);" 88 | ] 89 | } 90 | ], 91 | "metadata": {}, 92 | "nbformat": 4, 93 | "nbformat_minor": 5 94 | } 95 | -------------------------------------------------------------------------------- /examples/prototypes/trajectory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from pathlib import Path\n", 10 | "\n", 11 | "import ipyniivue\n", 12 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 13 | "\n", 14 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 15 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# Download data for example\n", 25 | "download_dataset(\n", 26 | " BASE_API_URL,\n", 27 | " DATA_FOLDER,\n", 28 | " files=[\"mni152.nii.gz\", \"dpsv.trx\"],\n", 29 | ")" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# based on https://github.com/niivue/ipyniivue/blob/main/original_gallery.md#tractography-tck-trk-trx-vtk\n", 39 | "\n", 40 | "volumes = [\n", 41 | " {\n", 42 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 43 | " \"colormap\": \"gray\",\n", 44 | " \"visible\": True,\n", 45 | " \"opacity\": 1.0,\n", 46 | " },\n", 47 | "]\n", 48 | "\n", 49 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 50 | "# nv.load_volumes(volumes)\n", 51 | "\n", 52 | "\n", 53 | "nv.load_meshes(\n", 54 | " [\n", 55 | " {\n", 56 | " \"path\": DATA_FOLDER / \"dpsv.trx\",\n", 57 | " \"rgba255\": [0, 142, 0, 255],\n", 58 | " }\n", 59 | " ]\n", 60 | ")\n", 61 | "\n", 62 | "\n", 63 | "nv" 64 | ] 65 | } 66 | ], 67 | "metadata": {}, 68 | "nbformat": 4, 69 | "nbformat_minor": 4 70 | } 71 | -------------------------------------------------------------------------------- /examples/saving.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "fc4fd25c-ba20-49b0-aa4d-ce02266553fc", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipyniivue\n", 13 | "from ipyniivue import NiiVue, SliceType, download_dataset\n", 14 | "\n", 15 | "# GitHub API URL for the base folder\n", 16 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 17 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "29230e89-1d68-4d8d-bbb8-2f688c690590", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "# Download data for example\n", 28 | "download_dataset(\n", 29 | " BASE_API_URL,\n", 30 | " DATA_FOLDER,\n", 31 | " files=[\n", 32 | " \"mni152.nii.gz\",\n", 33 | " \"hippo.nii.gz\",\n", 34 | " ],\n", 35 | ")" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "id": "4aa5999b-45e6-42a6-a124-6953572a881b", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "# based on https://niivue.github.io/niivue/features/basic.multiplanar.html\n", 46 | "\n", 47 | "volumes = [\n", 48 | " {\n", 49 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 50 | " \"colormap\": \"gray\",\n", 51 | " \"visible\": True,\n", 52 | " \"opacity\": 1.0,\n", 53 | " },\n", 54 | " {\n", 55 | " \"path\": DATA_FOLDER / \"hippo.nii.gz\",\n", 56 | " \"colormap\": \"red\",\n", 57 | " \"visible\": True,\n", 58 | " \"opacity\": 1,\n", 59 | " },\n", 60 | "]\n", 61 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 62 | "nv.load_volumes(volumes)\n", 63 | "nv" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "d5bd3f6b-145a-4a31-9328-6acbde412ce0", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "nv.save_document(\"test.nvd\")" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "id": "7cff3d5a-b2f7-465a-9b1a-b5e456ad9192", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "nv.save_html(\"test.html\")" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "id": "5d9ef2a7-d293-4de9-8e43-c8ca01f1f45f", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "nv.save_image()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "id": "6859daba-d0fd-40a0-98e3-1ba134e1e3e4", 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "nv.save_scene(\"test.png\")" 104 | ] 105 | } 106 | ], 107 | "metadata": {}, 108 | "nbformat": 4, 109 | "nbformat_minor": 5 110 | } 111 | -------------------------------------------------------------------------------- /examples/segment.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0c478d8f-80c7-477d-bc80-c57196bb6469", 6 | "metadata": {}, 7 | "source": [ 8 | "# Import Necessary Modules" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "548a4032-863a-4e4b-82db-098fd4ffda94", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import json\n", 19 | "import pathlib\n", 20 | "\n", 21 | "import ipywidgets as widgets\n", 22 | "\n", 23 | "import ipyniivue\n", 24 | "from ipyniivue import NiiVue, ShowRender, SliceType, download_dataset" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "3e06336d-1b09-4318-8d65-7524e1c08ccd", 30 | "metadata": {}, 31 | "source": [ 32 | "# Download Required Data" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "ec54c0c9-677d-49a0-af2f-9133477b2771", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 43 | "\n", 44 | "download_dataset(\n", 45 | " \"https://niivue.com/demos/images/\",\n", 46 | " dest_folder=DATA_FOLDER,\n", 47 | " files=[\n", 48 | " \"mni152.nii.gz\",\n", 49 | " \"mni152_pveseg.nii.gz\",\n", 50 | " ],\n", 51 | ")" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "e1c9bf8a-e57b-4b09-b000-ef63ae59fb24", 57 | "metadata": {}, 58 | "source": [ 59 | "# Setup NiiVue instance" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "id": "ad3925f0-ff97-415a-bff5-919b93464c13", 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "nv = NiiVue(\n", 70 | " back_color=(0.3, 0.3, 0.3, 1),\n", 71 | " show_3d_crosshair=True,\n", 72 | ")\n", 73 | "nv.set_radiological_convention(False)\n", 74 | "\n", 75 | "nv.load_volumes(\n", 76 | " [\n", 77 | " {\"path\": DATA_FOLDER / \"mni152.nii.gz\"},\n", 78 | " {\"path\": DATA_FOLDER / \"mni152_pveseg.nii.gz\", \"opacity\": 0.5},\n", 79 | " ]\n", 80 | ")\n", 81 | "\n", 82 | "nv.opts.multiplanar_show_render = ShowRender.ALWAYS\n", 83 | "nv.set_slice_type(SliceType.MULTIPLANAR)\n", 84 | "nv.graph.auto_size_multiplanar = True" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "3d276b99-ef28-4892-8fa9-ee0e07333cbf", 90 | "metadata": {}, 91 | "source": [ 92 | "# Add widgets" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "id": "59a5aa81-7297-465d-9fb1-b39596d14245", 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "background_checkbox = widgets.Checkbox(\n", 103 | " value=True,\n", 104 | " description=\"Background\",\n", 105 | ")\n", 106 | "\n", 107 | "mask_checkbox = widgets.Checkbox(\n", 108 | " value=False,\n", 109 | " description=\"Mask\",\n", 110 | ")\n", 111 | "\n", 112 | "smooth_checkbox = widgets.Checkbox(\n", 113 | " value=True,\n", 114 | " description=\"Smooth\",\n", 115 | ")\n", 116 | "\n", 117 | "opacity_slider = widgets.IntSlider(\n", 118 | " value=127,\n", 119 | " min=1,\n", 120 | " max=255,\n", 121 | " description=\"Opacity\",\n", 122 | ")" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "75802028-0a3c-4fb5-aaaf-4329a7c72b89", 128 | "metadata": {}, 129 | "source": [ 130 | "# Define Event Handlers" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "e57d8338-b6c4-4afc-a2dc-b771d503a1ac", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "def on_opacity_change(change):\n", 141 | " \"\"\"Adjust the opacity of the second volume.\"\"\"\n", 142 | " nv.set_opacity(1, change[\"new\"] / 255)\n", 143 | "\n", 144 | "\n", 145 | "def on_background_checkbox_change(change):\n", 146 | " \"\"\"Show or hide the background volume.\"\"\"\n", 147 | " nv.volumes[0].opacity = 1.0 if change[\"new\"] else 0.0\n", 148 | "\n", 149 | "\n", 150 | "def on_mask_checkbox_change(change):\n", 151 | " \"\"\"Set is_alpha_clip_dark.\"\"\"\n", 152 | " nv.opts.is_alpha_clip_dark = change[\"new\"]\n", 153 | "\n", 154 | "\n", 155 | "def on_smooth_checkbox_change(change):\n", 156 | " \"\"\"Set interpolation.\"\"\"\n", 157 | " nv.set_interpolation(not change[\"new\"])\n", 158 | "\n", 159 | "\n", 160 | "opacity_slider.observe(on_opacity_change, names=\"value\")\n", 161 | "background_checkbox.observe(on_background_checkbox_change, names=\"value\")\n", 162 | "mask_checkbox.observe(on_mask_checkbox_change, names=\"value\")\n", 163 | "smooth_checkbox.observe(on_smooth_checkbox_change, names=\"value\")" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "id": "c542be5f-7e79-45c5-8dd4-04aed3e41905", 169 | "metadata": {}, 170 | "source": [ 171 | "# Set Initial Values" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "id": "4755b591-db0f-426a-b613-9a04558e8a08", 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "on_opacity_change({\"new\": opacity_slider.value})\n", 182 | "on_background_checkbox_change({\"new\": background_checkbox.value})\n", 183 | "on_mask_checkbox_change({\"new\": mask_checkbox.value})\n", 184 | "on_smooth_checkbox_change({\"new\": smooth_checkbox.value})" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "7e882d8f-57e0-489f-8bf3-51749a9eb6f8", 190 | "metadata": {}, 191 | "source": [ 192 | "# Handle location changes" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": null, 198 | "id": "3580f73c-92e7-4ea7-a654-95e5aa4270d7", 199 | "metadata": {}, 200 | "outputs": [], 201 | "source": [ 202 | "location_label = widgets.HTML(\" \")\n", 203 | "\n", 204 | "\n", 205 | "def handle_location_change(data):\n", 206 | " \"\"\"Update the location label when the crosshair moves.\"\"\"\n", 207 | " location_label.value = \"  \" + data[\"string\"]\n", 208 | "\n", 209 | "\n", 210 | "nv.on_location_change(handle_location_change)" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "id": "5fd0e5bc-55d1-4247-913d-c6546396b6a8", 216 | "metadata": {}, 217 | "source": [ 218 | "# Handle Custom Colormap Input" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "id": "1a3d4517-a7be-43ef-9026-2166fbe0c853", 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "colormap_textarea = widgets.Textarea(\n", 229 | " value=\"\"\"{\n", 230 | " \"R\": [0, 0, 120, 175],\n", 231 | " \"G\": [0, 90, 60, 185],\n", 232 | " \"B\": [0, 120, 60, 175],\n", 233 | " \"labels\": [\"air\", \"CSF\", \"gray\", \"white\"]\n", 234 | "}\"\"\",\n", 235 | " description=\"Custom Colormap (JSON)\",\n", 236 | " layout=widgets.Layout(width=\"100%\", height=\"150px\"),\n", 237 | ")\n", 238 | "\n", 239 | "apply_button = widgets.Button(\n", 240 | " description=\"Apply Colormap\",\n", 241 | ")\n", 242 | "\n", 243 | "\n", 244 | "def on_apply_button_click(b):\n", 245 | " \"\"\"Apply custom colormap json.\"\"\"\n", 246 | " json_str = colormap_textarea.value\n", 247 | " try:\n", 248 | " cmap = json.loads(json_str)\n", 249 | " nv.volumes[1].set_colormap_label(cmap)\n", 250 | " except json.JSONDecodeError as e:\n", 251 | " print(\"Invalid JSON format:\", e)\n", 252 | "\n", 253 | "\n", 254 | "apply_button.on_click(on_apply_button_click)\n", 255 | "\n", 256 | "on_apply_button_click(None)" 257 | ] 258 | }, 259 | { 260 | "cell_type": "markdown", 261 | "id": "5f66cfe6-aa40-499a-9f68-fd06a2375b1c", 262 | "metadata": {}, 263 | "source": [ 264 | "# Display all" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "id": "cb4ec15a-733b-4af5-a00b-242e2184711f", 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "controls = widgets.HBox(\n", 275 | " [\n", 276 | " background_checkbox,\n", 277 | " mask_checkbox,\n", 278 | " smooth_checkbox,\n", 279 | " opacity_slider,\n", 280 | " ]\n", 281 | ")\n", 282 | "\n", 283 | "script_controls = widgets.VBox(\n", 284 | " [\n", 285 | " colormap_textarea,\n", 286 | " apply_button,\n", 287 | " ]\n", 288 | ")\n", 289 | "\n", 290 | "display_widgets = widgets.VBox(\n", 291 | " [\n", 292 | " controls,\n", 293 | " nv,\n", 294 | " location_label,\n", 295 | " script_controls,\n", 296 | " ]\n", 297 | ")\n", 298 | "\n", 299 | "display(display_widgets)" 300 | ] 301 | } 302 | ], 303 | "metadata": {}, 304 | "nbformat": 4, 305 | "nbformat_minor": 5 306 | } 307 | -------------------------------------------------------------------------------- /examples/sync.bidirectional.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "bdfd3b58-76fb-4f37-bc38-71a014830bd1", 6 | "metadata": {}, 7 | "source": [ 8 | "# Bidirectional Sync\n", 9 | "\n", 10 | "This demo mirrors https://niivue.com/demos/features/sync.bidirectional.html." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "f6480178-93e9-4e36-b701-d3c99ea45387", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import ipywidgets as widgets\n", 21 | "from IPython.display import display\n", 22 | "\n", 23 | "from ipyniivue import MultiplanarType, NiiVue" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "id": "2fa54822-6433-433e-ab59-7bd8816b30d9", 29 | "metadata": {}, 30 | "source": [ 31 | "## Create Three NiiVue Instances" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "c9c037aa-d318-46e9-b8ab-138ef4c11ba9", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "nv1 = NiiVue(\n", 42 | " height=400,\n", 43 | " multiplanar_force_render=True,\n", 44 | " back_color=(0.0, 0.0, 0.0, 1.0),\n", 45 | ")\n", 46 | "\n", 47 | "nv2 = NiiVue(\n", 48 | " height=400,\n", 49 | " multiplanar_force_render=True,\n", 50 | " back_color=(0.0, 0.0, 0.0, 1.0),\n", 51 | ")\n", 52 | "\n", 53 | "nv3 = NiiVue(\n", 54 | " height=400,\n", 55 | " multiplanar_force_render=True,\n", 56 | " back_color=(0.0, 0.0, 0.0, 1.0),\n", 57 | ")" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "id": "1da1475a-de66-4783-8feb-1183373410dc", 63 | "metadata": {}, 64 | "source": [ 65 | "## Load Volumes" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "id": "f0a140d9-6ca6-445e-9ecb-d10d913075fc", 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "nv1.load_volumes([{\"url\": \"https://niivue.com/demos/images/pcasl.nii.gz\"}])\n", 76 | "nv2.load_volumes([{\"url\": \"https://niivue.com/demos/images/aal.nii.gz\"}])\n", 77 | "nv3.load_volumes([{\"url\": \"https://niivue.com/demos/images/mni152.nii.gz\"}])" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "a3ece015-96f3-41ae-b451-8af7d7fa6b61", 83 | "metadata": {}, 84 | "source": [ 85 | "## Create Interactive Controls" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "id": "f91cf421-cd58-4f5a-975a-1d83daf0b9b2", 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "layout_dropdown = widgets.Dropdown(\n", 96 | " options=[\n", 97 | " (\"Auto\", MultiplanarType.AUTO),\n", 98 | " (\"Column\", MultiplanarType.COLUMN),\n", 99 | " (\"Grid\", MultiplanarType.GRID),\n", 100 | " (\"Row\", MultiplanarType.ROW),\n", 101 | " ],\n", 102 | " value=MultiplanarType.AUTO,\n", 103 | " description=\"Layout:\",\n", 104 | ")\n", 105 | "\n", 106 | "sync_dropdown = widgets.Dropdown(\n", 107 | " options=[\n", 108 | " (\"Sync Disabled\", 0),\n", 109 | " (\"Sync 2D\", 1),\n", 110 | " (\"Sync 3D\", 2),\n", 111 | " (\"Sync 2D and 3D\", 3),\n", 112 | " ],\n", 113 | " value=3,\n", 114 | " description=\"Broadcast:\",\n", 115 | ")\n", 116 | "\n", 117 | "status1 = widgets.HTML(value=\" \")\n", 118 | "status2 = widgets.HTML(value=\" \")\n", 119 | "status3 = widgets.HTML(value=\" \")" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "id": "82b36505-e723-4e53-b0f2-83920d244639", 125 | "metadata": {}, 126 | "source": [ 127 | "## Setup Event Handlers" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "id": "da739796-9153-4955-a4d7-8b875e7ae555", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "def on_layout_change(change):\n", 138 | " \"\"\"Handle layout dropdown changes.\"\"\"\n", 139 | " new_layout = change[\"new\"]\n", 140 | " nv1.opts.multiplanar_layout = new_layout\n", 141 | " nv2.opts.multiplanar_layout = new_layout\n", 142 | " nv3.opts.multiplanar_layout = new_layout\n", 143 | "\n", 144 | "\n", 145 | "def on_sync_change(change):\n", 146 | " \"\"\"Handle sync mode dropdown changes.\"\"\"\n", 147 | " v = change[\"new\"]\n", 148 | " is_2d = False\n", 149 | " is_3d = False\n", 150 | "\n", 151 | " if v % 2: # If odd (1 or 3)\n", 152 | " is_2d = True\n", 153 | " if v > 1: # If 2 or 3\n", 154 | " is_3d = True\n", 155 | "\n", 156 | " nv1.broadcast_to([nv2, nv3], {\"2d\": is_2d, \"3d\": is_3d})\n", 157 | " nv2.broadcast_to([nv1, nv3], {\"2d\": is_2d, \"3d\": is_3d})\n", 158 | " nv3.broadcast_to([nv1, nv2], {\"2d\": is_2d, \"3d\": is_3d})\n", 159 | "\n", 160 | "\n", 161 | "@nv1.on_location_change\n", 162 | "def handle_location1(data):\n", 163 | " \"\"\"Handle location string.\"\"\"\n", 164 | " status1.value = f\"  {data['string']}\"\n", 165 | "\n", 166 | "\n", 167 | "@nv2.on_location_change\n", 168 | "def handle_location2(data):\n", 169 | " \"\"\"Handle location string.\"\"\"\n", 170 | " status2.value = f\"  {data['string']}\"\n", 171 | "\n", 172 | "\n", 173 | "@nv3.on_location_change\n", 174 | "def handle_location3(data):\n", 175 | " \"\"\"Handle location string.\"\"\"\n", 176 | " status3.value = f\"  {data['string']}\"\n", 177 | "\n", 178 | "\n", 179 | "layout_dropdown.observe(on_layout_change, names=\"value\")\n", 180 | "sync_dropdown.observe(on_sync_change, names=\"value\")\n", 181 | "\n", 182 | "on_sync_change({\"new\": sync_dropdown.value})" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "id": "08b6e470-ba7e-48be-9687-41edcbd5e6fd", 188 | "metadata": {}, 189 | "source": [ 190 | "## Display All" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "id": "8462e01e-6265-4580-b59b-58c63138b432", 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "controls = widgets.HBox([layout_dropdown, sync_dropdown])\n", 201 | "\n", 202 | "viewers = widgets.GridspecLayout(1, 3, height=\"400px\")\n", 203 | "viewers[0, 0] = nv1\n", 204 | "viewers[0, 1] = nv2\n", 205 | "viewers[0, 2] = nv3\n", 206 | "\n", 207 | "status_row = widgets.HBox([status1, status2, status3])\n", 208 | "\n", 209 | "display(widgets.VBox([controls, viewers, status_row]))" 210 | ] 211 | } 212 | ], 213 | "metadata": {}, 214 | "nbformat": 4, 215 | "nbformat_minor": 5 216 | } 217 | -------------------------------------------------------------------------------- /examples/tracts.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Import necessary modules" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import pathlib\n", 17 | "\n", 18 | "import ipywidgets as widgets\n", 19 | "\n", 20 | "import ipyniivue\n", 21 | "from ipyniivue import NiiVue, SliceType, download_dataset" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "# Download required data" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n", 38 | "\n", 39 | "download_dataset(\n", 40 | " \"https://niivue.com/demos/images/\",\n", 41 | " dest_folder=DATA_FOLDER,\n", 42 | " files=[\n", 43 | " \"mni152.nii.gz\",\n", 44 | " \"dpsv.trx\",\n", 45 | " ],\n", 46 | ")" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "# Create NiiVue instance" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "nv = NiiVue(show_3d_crosshair=True, back_color=(0.8, 0.8, 1, 1))\n", 63 | "\n", 64 | "nv.opts.is_colorbar = True\n", 65 | "nv.set_slice_type(SliceType.RENDER)\n", 66 | "nv.set_clip_plane(-0.1, 270, 0);" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "# Load the volume and mesh data" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "nv.load_volumes(\n", 83 | " [\n", 84 | " {\"path\": DATA_FOLDER / \"mni152.nii.gz\"},\n", 85 | " ]\n", 86 | ")\n", 87 | "\n", 88 | "nv.load_meshes(\n", 89 | " [\n", 90 | " {\"path\": DATA_FOLDER / \"dpsv.trx\", \"rgba255\": [0, 142, 0, 255]},\n", 91 | " ]\n", 92 | ")\n", 93 | "\n", 94 | "nv.meshes[0].colormap = \"blue\"\n", 95 | "nv.meshes[0].rgba255 = (0, 255, 255, 255)" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "# Create sliders and dropdowns" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "radius_slider = widgets.IntSlider(\n", 112 | " value=0,\n", 113 | " min=0,\n", 114 | " max=20,\n", 115 | " description=\"Radius\",\n", 116 | ")\n", 117 | "\n", 118 | "length_slider = widgets.IntSlider(\n", 119 | " value=3,\n", 120 | " min=1,\n", 121 | " max=80,\n", 122 | " description=\"Length\",\n", 123 | ")\n", 124 | "\n", 125 | "dither_slider = widgets.IntSlider(\n", 126 | " value=1,\n", 127 | " min=0,\n", 128 | " max=10,\n", 129 | " description=\"Dither\",\n", 130 | ")\n", 131 | "\n", 132 | "fiber_coloration_options = [\n", 133 | " (\"Global direction\", \"Global\"),\n", 134 | " (\"Local direction\", \"Local\"),\n", 135 | " (\"Fixed\", \"Fixed\"),\n", 136 | " (\"First Per Vertex Type (if available)\", \"DPV0\"),\n", 137 | " (\"First Per Streamline Type (if available)\", \"DPS0\"),\n", 138 | "]\n", 139 | "fiber_color_dropdown = widgets.Dropdown(\n", 140 | " options=fiber_coloration_options,\n", 141 | " value=\"Global\",\n", 142 | " description=\"Fiber Coloration:\",\n", 143 | ")\n", 144 | "\n", 145 | "fiber_reduction_options = [(\"100%\", 1), (\"50%\", 2), (\"25%\", 4), (\"10%\", 10)]\n", 146 | "fiber_reduction_dropdown = widgets.Dropdown(\n", 147 | " options=fiber_reduction_options,\n", 148 | " value=1,\n", 149 | " description=\"Fiber Reduction\",\n", 150 | ")" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "# Set up callbacks" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "def on_radius_change(change):\n", 167 | " \"\"\"Set fiber radius.\"\"\"\n", 168 | " nv.set_mesh_property(nv.meshes[0].id, \"fiber_radius\", change[\"new\"] * 0.1)\n", 169 | "\n", 170 | "\n", 171 | "radius_slider.observe(on_radius_change, names=\"value\")\n", 172 | "\n", 173 | "\n", 174 | "def on_length_change(change):\n", 175 | " \"\"\"Set fiber length.\"\"\"\n", 176 | " nv.set_mesh_property(nv.meshes[0].id, \"fiber_length\", change[\"new\"])\n", 177 | "\n", 178 | "\n", 179 | "length_slider.observe(on_length_change, names=\"value\")\n", 180 | "\n", 181 | "\n", 182 | "def on_dither_change(change):\n", 183 | " \"\"\"Set fiber dither.\"\"\"\n", 184 | " nv.set_mesh_property(nv.meshes[0].id, \"fiber_dither\", change[\"new\"] * 0.1)\n", 185 | "\n", 186 | "\n", 187 | "dither_slider.observe(on_dither_change, names=\"value\")\n", 188 | "\n", 189 | "\n", 190 | "def on_fiber_color_change(change):\n", 191 | " \"\"\"Set fiber color.\"\"\"\n", 192 | " nv.set_mesh_property(nv.meshes[0].id, \"fiber_color\", change[\"new\"])\n", 193 | "\n", 194 | "\n", 195 | "fiber_color_dropdown.observe(on_fiber_color_change, names=\"value\")\n", 196 | "\n", 197 | "\n", 198 | "def on_fiber_reduction_change(change):\n", 199 | " \"\"\"Set fiber decimation stride.\"\"\"\n", 200 | " nv.set_mesh_property(nv.meshes[0].id, \"fiber_decimation_stride\", change[\"new\"])\n", 201 | "\n", 202 | "\n", 203 | "fiber_reduction_dropdown.observe(on_fiber_reduction_change, names=\"value\")" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "# Display all" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "controls = widgets.VBox(\n", 220 | " [\n", 221 | " radius_slider,\n", 222 | " length_slider,\n", 223 | " dither_slider,\n", 224 | " fiber_color_dropdown,\n", 225 | " fiber_reduction_dropdown,\n", 226 | " ]\n", 227 | ")\n", 228 | "\n", 229 | "widgets.VBox(\n", 230 | " [\n", 231 | " controls,\n", 232 | " nv,\n", 233 | " ]\n", 234 | ")" 235 | ] 236 | } 237 | ], 238 | "metadata": {}, 239 | "nbformat": 4, 240 | "nbformat_minor": 4 241 | } 242 | -------------------------------------------------------------------------------- /examples/widgets.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "f26da1e3-877f-4893-8baa-8b9460601205", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from pathlib import Path\n", 11 | "\n", 12 | "import ipywidgets\n", 13 | "\n", 14 | "import ipyniivue\n", 15 | "from ipyniivue import NiiVue, SliceType, WidgetObserver, download_dataset\n", 16 | "\n", 17 | "# GitHub API URL for the base folder\n", 18 | "BASE_API_URL = \"https://niivue.com/demos/images/\"\n", 19 | "DATA_FOLDER = Path(ipyniivue.__file__).parent / \"images\"" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "37b8beb5-32d3-4d0b-8294-47889185f60d", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "# Download data for example\n", 30 | "download_dataset(\n", 31 | " BASE_API_URL,\n", 32 | " DATA_FOLDER,\n", 33 | " files=[\n", 34 | " \"mni152.nii.gz\",\n", 35 | " \"hippo.nii.gz\",\n", 36 | " ],\n", 37 | ")" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "cda3a5f6-5bed-4190-82a7-057e17fa6635", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "volumes = [\n", 48 | " {\n", 49 | " \"path\": DATA_FOLDER / \"mni152.nii.gz\",\n", 50 | " \"colormap\": \"gray\",\n", 51 | " \"visible\": True,\n", 52 | " \"opacity\": 1.0,\n", 53 | " },\n", 54 | " {\n", 55 | " \"path\": DATA_FOLDER / \"hippo.nii.gz\",\n", 56 | " \"colormap\": \"red\",\n", 57 | " \"visible\": True,\n", 58 | " \"opacity\": 1.0,\n", 59 | " },\n", 60 | "]\n", 61 | "\n", 62 | "nv = NiiVue(slice_type=SliceType.MULTIPLANAR)\n", 63 | "nv.load_volumes(volumes)\n", 64 | "\n", 65 | "widgetArray = []\n", 66 | "\n", 67 | "widget_slice_type = {\n", 68 | " \"widget\": ipywidgets.RadioButtons(\n", 69 | " options=[\n", 70 | " (\"Axial\", 0),\n", 71 | " (\"Coronal\", 1),\n", 72 | " (\"Sagittal\", 2),\n", 73 | " (\"Multiplanar\", 3),\n", 74 | " (\"Render\", 4),\n", 75 | " ],\n", 76 | " value=3,\n", 77 | " description=\"Slice Type:\",\n", 78 | " ),\n", 79 | " \"obj\": nv,\n", 80 | " \"attribute\": \"slice_type\",\n", 81 | "}\n", 82 | "widgetArray.append(widget_slice_type)\n", 83 | "\n", 84 | "widget_scan_opacity = {\n", 85 | " \"widget\": ipywidgets.FloatSlider(\n", 86 | " value=1.0,\n", 87 | " min=0.0,\n", 88 | " max=1.0,\n", 89 | " step=0.1,\n", 90 | " description=\"Scan Opacity:\",\n", 91 | " orientation=\"horizontal\",\n", 92 | " ),\n", 93 | " \"obj\": nv.volumes[0],\n", 94 | " \"attribute\": \"opacity\",\n", 95 | "}\n", 96 | "widgetArray.append(widget_scan_opacity)\n", 97 | "\n", 98 | "widget_hippo_opacity = {\n", 99 | " \"widget\": ipywidgets.FloatSlider(\n", 100 | " value=1.0,\n", 101 | " min=0.0,\n", 102 | " max=1.0,\n", 103 | " step=0.1,\n", 104 | " description=\"Hippocampus Opacity:\",\n", 105 | " orientation=\"horizontal\",\n", 106 | " ),\n", 107 | " \"obj\": nv.volumes[1],\n", 108 | " \"attribute\": \"opacity\",\n", 109 | "}\n", 110 | "widgetArray.append(widget_hippo_opacity)\n", 111 | "\n", 112 | "widget_scan_colormap = {\n", 113 | " \"widget\": ipywidgets.Select(\n", 114 | " options=[\"Gray\", \"Red\", \"Blue\", \"Green\"],\n", 115 | " value=\"Gray\",\n", 116 | " description=\"Scan Colormap:\",\n", 117 | " ),\n", 118 | " \"obj\": nv.volumes[0],\n", 119 | " \"attribute\": \"colormap\",\n", 120 | "}\n", 121 | "widgetArray.append(widget_scan_colormap)\n", 122 | "\n", 123 | "widget_hippo_colormap = {\n", 124 | " \"widget\": ipywidgets.Select(\n", 125 | " options=[\"Red\", \"Blue\", \"Green\", \"Gray\"],\n", 126 | " value=\"Red\",\n", 127 | " description=\"Hippocampus Colormap:\",\n", 128 | " ),\n", 129 | " \"obj\": nv.volumes[1],\n", 130 | " \"attribute\": \"colormap\",\n", 131 | "}\n", 132 | "widgetArray.append(widget_hippo_colormap)\n", 133 | "\n", 134 | "for widget in widgetArray:\n", 135 | " WidgetObserver(**widget)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "cce13351-2f24-49fb-b8f6-4bac07f7b2dc", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "nv" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "id": "66b82692-c7b0-471d-8134-f399cf9ac399", 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "for widget in widgetArray:\n", 156 | " display(widget[\"widget\"])" 157 | ] 158 | } 159 | ], 160 | "metadata": {}, 161 | "nbformat": 4, 162 | "nbformat_minor": 5 163 | } 164 | -------------------------------------------------------------------------------- /examples/worldspace.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3e92e45e-bd2f-47ac-8470-c4ce892f58a1", 6 | "metadata": {}, 7 | "source": [ 8 | "# World Space Viewer\n", 9 | "This example demonstrates world space navigation with various orientation options\n", 10 | "\n", 11 | "See https://niivue.com/demos/features/worldspace.html for mirror." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "37ba744a-9570-4a52-b8f0-8add586fd9e8", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import ipywidgets as widgets\n", 22 | "from IPython.display import display\n", 23 | "\n", 24 | "from ipyniivue import DragMode, NiiVue" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "5f665aa5-188f-4ddc-b34b-654b7de94c34", 30 | "metadata": {}, 31 | "source": [ 32 | "## Create NiiVue Instance" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "7f25381f-9a83-4a02-9eec-b4ac90985c53", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "nv = NiiVue(\n", 43 | " drag_and_drop_enabled=True,\n", 44 | " back_color=(1, 1, 1, 1),\n", 45 | " show_3d_crosshair=True,\n", 46 | ")\n", 47 | "\n", 48 | "nv.set_slice_mm(True)\n", 49 | "nv.set_radiological_convention(False)\n", 50 | "nv.set_slice_type(nv.opts.slice_type.MULTIPLANAR)\n", 51 | "\n", 52 | "nv.load_volumes(\n", 53 | " [\n", 54 | " {\n", 55 | " \"url\": \"https://niivue.com/demos/images/FLAIR.nii.gz\",\n", 56 | " \"colormap\": \"gray\",\n", 57 | " \"opacity\": 1.0,\n", 58 | " \"visible\": True,\n", 59 | " }\n", 60 | " ]\n", 61 | ")" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "5d97d24f-fb7c-43c0-9db7-8cdd0061b2ff", 67 | "metadata": {}, 68 | "source": [ 69 | "## Create Interactive Controls" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "id": "f213e39a-3042-430a-a8eb-69731c531d90", 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "# LR checkbox (Radiological convention)\n", 80 | "lr_checkbox = widgets.Checkbox(\n", 81 | " value=False,\n", 82 | " description=\"LR\",\n", 83 | " tooltip=\"Toggle between radiological and neurological convention\",\n", 84 | ")\n", 85 | "\n", 86 | "# nose checkbox (Sagittal nose direction)\n", 87 | "nose_checkbox = widgets.Checkbox(\n", 88 | " value=False, description=\"Nose\", tooltip=\"Toggle sagittal nose direction\"\n", 89 | ")\n", 90 | "\n", 91 | "# world checkbox (World space vs voxel space)\n", 92 | "world_checkbox = widgets.Checkbox(\n", 93 | " value=True,\n", 94 | " description=\"World\",\n", 95 | " tooltip=\"Toggle between world space (mm) and voxel space\",\n", 96 | ")\n", 97 | "\n", 98 | "# drag mode dropdown\n", 99 | "drag_dropdown = widgets.Dropdown(\n", 100 | " options=[\n", 101 | " (\"contrast\", DragMode.CONTRAST),\n", 102 | " (\"measurement\", DragMode.MEASUREMENT),\n", 103 | " (\"pan/zoom\", DragMode.PAN),\n", 104 | " (\"none\", DragMode.NONE),\n", 105 | " ],\n", 106 | " value=DragMode.CONTRAST,\n", 107 | " description=\"Drag mode:\",\n", 108 | ")" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "id": "433395cc-6a36-4309-bd9a-3412efb8ab15", 114 | "metadata": {}, 115 | "source": [ 116 | "## Setup Event Handlers" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "id": "03ec3bf3-3c47-48b1-9f73-87b87900e596", 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "def on_lr_change(change):\n", 127 | " \"\"\"Handle radiological convention checkbox changes.\"\"\"\n", 128 | " nv.set_radiological_convention(change[\"new\"])\n", 129 | "\n", 130 | "\n", 131 | "def on_nose_change(change):\n", 132 | " \"\"\"Handle nose direction checkbox changes.\"\"\"\n", 133 | " nv.opts.sagittal_nose_left = change[\"new\"]\n", 134 | "\n", 135 | "\n", 136 | "def on_world_change(change):\n", 137 | " \"\"\"Handle world space checkbox changes.\"\"\"\n", 138 | " nv.set_slice_mm(change[\"new\"])\n", 139 | "\n", 140 | "\n", 141 | "def on_drag_mode_change(change):\n", 142 | " \"\"\"Handle drag mode dropdown changes.\"\"\"\n", 143 | " nv.opts.drag_mode = change[\"new\"]\n", 144 | "\n", 145 | "\n", 146 | "lr_checkbox.observe(on_lr_change, names=\"value\")\n", 147 | "nose_checkbox.observe(on_nose_change, names=\"value\")\n", 148 | "world_checkbox.observe(on_world_change, names=\"value\")\n", 149 | "drag_dropdown.observe(on_drag_mode_change, names=\"value\")" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "id": "c285bb25-7ce4-48d0-a42f-1829647e280d", 155 | "metadata": {}, 156 | "source": [ 157 | "## Display All" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "id": "2247f2d2-4935-4a1a-8cfe-8e501c1dc470", 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "controls = widgets.HBox([lr_checkbox, nose_checkbox, world_checkbox, drag_dropdown])\n", 168 | "\n", 169 | "display(widgets.VBox([controls, nv]))" 170 | ] 171 | } 172 | ], 173 | "metadata": {}, 174 | "nbformat": 4, 175 | "nbformat_minor": 5 176 | } 177 | -------------------------------------------------------------------------------- /examples/worldspace2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "f473438a-2002-4526-a6dc-87ce28cc5d41", 6 | "metadata": {}, 7 | "source": [ 8 | "## Advanced World Space Demo\n", 9 | "advanced world space features including mesh clipping, orientation options, and multiplanar viewing with meshes\n", 10 | "\n", 11 | "See https://niivue.com/demos/features/worldspace2.html for mirror." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "a7a68189-b242-4d32-93a2-616d0c1bd624", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import ipywidgets as widgets\n", 22 | "from IPython.display import display\n", 23 | "\n", 24 | "from ipyniivue import DragMode, NiiVue, ShowRender, SliceType" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "id": "d926ba4b-b02b-48fd-8cd7-5985a0d736c9", 30 | "metadata": {}, 31 | "source": [ 32 | "## Create NiiVue Instance" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "7bf9089d-1508-4476-9ca7-44731ec302b6", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "nv = NiiVue(\n", 43 | " height=400,\n", 44 | " drag_and_drop_enabled=True,\n", 45 | " back_color=(0.3, 0.2, 0.4, 1.0),\n", 46 | " show_3d_crosshair=True,\n", 47 | ")\n", 48 | "\n", 49 | "nv.set_slice_mm(True)\n", 50 | "nv.set_clip_plane(-0.1, 270, 0)\n", 51 | "nv.set_render_azimuth_elevation(120, 10)\n", 52 | "nv.set_high_resolution_capable(True)\n", 53 | "\n", 54 | "nv.load_volumes(\n", 55 | " [\n", 56 | " {\n", 57 | " \"url\": \"https://niivue.com/demos/images/mni152.nii.gz\",\n", 58 | " \"colormap\": \"gray\",\n", 59 | " \"opacity\": 1.0,\n", 60 | " }\n", 61 | " ]\n", 62 | ")\n", 63 | "\n", 64 | "nv.load_meshes(\n", 65 | " [\n", 66 | " {\n", 67 | " \"url\": \"https://niivue.com/demos/images/BrainMesh_ICBM152.lh.mz3\",\n", 68 | " \"rgba255\": [200, 162, 255, 255],\n", 69 | " },\n", 70 | " {\n", 71 | " \"url\": \"https://niivue.com/demos/images/dpsv.trx\",\n", 72 | " \"rgba255\": [255, 255, 255, 255],\n", 73 | " },\n", 74 | " ]\n", 75 | ")\n", 76 | "\n", 77 | "nv.opts.slice_type = SliceType.MULTIPLANAR" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "id": "34b70342-5120-4159-a877-58d651c58990", 83 | "metadata": {}, 84 | "source": [ 85 | "## Create Interactive Controls" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "id": "5f35d49b-af66-40b8-a64b-264c9c0a8d8a", 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "# mesh clipping slider\n", 96 | "mesh_clipping_slider = widgets.IntSlider(\n", 97 | " value=11,\n", 98 | " min=0,\n", 99 | " max=11,\n", 100 | " description=\"Mesh clipping:\",\n", 101 | " continuous_update=True,\n", 102 | " style={\"description_width\": \"initial\"},\n", 103 | ")\n", 104 | "\n", 105 | "# checkboxes\n", 106 | "radiological_checkbox = widgets.Checkbox(value=False, description=\"Radiological\")\n", 107 | "\n", 108 | "corner_text_checkbox = widgets.Checkbox(value=False, description=\"Corner text\")\n", 109 | "\n", 110 | "world_space_checkbox = widgets.Checkbox(value=True, description=\"World space\")\n", 111 | "\n", 112 | "colorbar_checkbox = widgets.Checkbox(value=False, description=\"Colorbar\")\n", 113 | "\n", 114 | "cube_checkbox = widgets.Checkbox(value=False, description=\"Cube\")\n", 115 | "\n", 116 | "pad_checkbox = widgets.Checkbox(value=False, description=\"Pad\")\n", 117 | "\n", 118 | "high_dpi_checkbox = widgets.Checkbox(value=True, description=\"HighDPI\")\n", 119 | "\n", 120 | "force_render_checkbox = widgets.Checkbox(value=False, description=\"Force Render\")\n", 121 | "\n", 122 | "# drag mode dropdown\n", 123 | "drag_mode_dropdown = widgets.Dropdown(\n", 124 | " options=[\n", 125 | " (\"contrast\", DragMode.CONTRAST),\n", 126 | " (\"measurement\", DragMode.MEASUREMENT),\n", 127 | " (\"pan/zoom\", DragMode.PAN),\n", 128 | " (\"none\", DragMode.NONE),\n", 129 | " ],\n", 130 | " value=DragMode.CONTRAST,\n", 131 | " description=\"Drag mode:\",\n", 132 | " style={\"description_width\": \"initial\"},\n", 133 | ")" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "08353015-8486-4de6-a5bf-391a412b30e9", 139 | "metadata": {}, 140 | "source": [ 141 | "## Setup Event Handlers" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "id": "ce29c6ad-abfe-4f63-9e62-d95016db5f56", 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "def on_mesh_clipping_change(change):\n", 152 | " \"\"\"Handle mesh clipping slider changes.\"\"\"\n", 153 | " dx = float(change[\"new\"])\n", 154 | " if dx > 10:\n", 155 | " dx = float(\"inf\")\n", 156 | " nv.set_mesh_thickness_on_2d(dx)\n", 157 | "\n", 158 | "\n", 159 | "def on_radiological_change(change):\n", 160 | " \"\"\"Handle radiological convention checkbox changes.\"\"\"\n", 161 | " nv.set_radiological_convention(change[\"new\"])\n", 162 | "\n", 163 | "\n", 164 | "def on_corner_text_change(change):\n", 165 | " \"\"\"Handle corner text checkbox changes.\"\"\"\n", 166 | " nv.set_corner_orientation_text(change[\"new\"])\n", 167 | "\n", 168 | "\n", 169 | "def on_world_space_change(change):\n", 170 | " \"\"\"Handle world space checkbox changes.\"\"\"\n", 171 | " nv.set_slice_mm(change[\"new\"])\n", 172 | "\n", 173 | "\n", 174 | "def on_colorbar_change(change):\n", 175 | " \"\"\"Handle colorbar checkbox changes.\"\"\"\n", 176 | " nv.opts.is_colorbar = change[\"new\"]\n", 177 | "\n", 178 | "\n", 179 | "def on_cube_change(change):\n", 180 | " \"\"\"Handle orientation cube checkbox changes.\"\"\"\n", 181 | " nv.opts.is_orient_cube = change[\"new\"]\n", 182 | "\n", 183 | "\n", 184 | "def on_pad_change(change):\n", 185 | " \"\"\"Handle multiplanar padding checkbox changes.\"\"\"\n", 186 | " pad = 5 if change[\"new\"] else 0\n", 187 | " nv.set_multiplanar_pad_pixels(pad)\n", 188 | "\n", 189 | "\n", 190 | "def on_high_dpi_change(change):\n", 191 | " \"\"\"Handle high DPI checkbox changes.\"\"\"\n", 192 | " nv.set_high_resolution_capable(change[\"new\"])\n", 193 | "\n", 194 | "\n", 195 | "def on_force_render_change(change):\n", 196 | " \"\"\"Handle force render checkbox changes.\"\"\"\n", 197 | " if change[\"new\"]:\n", 198 | " nv.opts.multiplanar_show_render = ShowRender.ALWAYS\n", 199 | " else:\n", 200 | " nv.opts.multiplanar_show_render = ShowRender.AUTO\n", 201 | "\n", 202 | "\n", 203 | "def on_drag_mode_change(change):\n", 204 | " \"\"\"Handle drag mode dropdown changes.\"\"\"\n", 205 | " nv.opts.drag_mode = change[\"new\"]" 206 | ] 207 | }, 208 | { 209 | "cell_type": "markdown", 210 | "id": "c2f6e749-6e86-406b-9ede-89c1dee1f962", 211 | "metadata": {}, 212 | "source": [ 213 | "## Attach event handlers" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": null, 219 | "id": "334235db-b8b3-44d5-8316-c07cb4e2477b", 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "mesh_clipping_slider.observe(on_mesh_clipping_change, names=\"value\")\n", 224 | "radiological_checkbox.observe(on_radiological_change, names=\"value\")\n", 225 | "corner_text_checkbox.observe(on_corner_text_change, names=\"value\")\n", 226 | "world_space_checkbox.observe(on_world_space_change, names=\"value\")\n", 227 | "colorbar_checkbox.observe(on_colorbar_change, names=\"value\")\n", 228 | "cube_checkbox.observe(on_cube_change, names=\"value\")\n", 229 | "pad_checkbox.observe(on_pad_change, names=\"value\")\n", 230 | "high_dpi_checkbox.observe(on_high_dpi_change, names=\"value\")\n", 231 | "force_render_checkbox.observe(on_force_render_change, names=\"value\")\n", 232 | "drag_mode_dropdown.observe(on_drag_mode_change, names=\"value\")" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "id": "8d0264d4-a1d8-4108-b0e2-2199b0abe3c4", 238 | "metadata": {}, 239 | "source": [ 240 | "## Initialize mesh clipping" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "id": "1b6ae48d-4771-446a-bb75-3f075f6a13f3", 247 | "metadata": {}, 248 | "outputs": [], 249 | "source": [ 250 | "on_mesh_clipping_change({\"new\": 11}) # sets to infinity cause >10" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "id": "6642b8c8-6747-41e5-9aee-3cd4c29e7b5a", 256 | "metadata": {}, 257 | "source": [ 258 | "## Display All" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "id": "780a17c6-0c47-491e-b4f6-bf9a7c7e657b", 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "controls_row1 = widgets.HBox([mesh_clipping_slider, drag_mode_dropdown])\n", 269 | "\n", 270 | "controls_row2 = widgets.HBox(\n", 271 | " [\n", 272 | " radiological_checkbox,\n", 273 | " corner_text_checkbox,\n", 274 | " world_space_checkbox,\n", 275 | " colorbar_checkbox,\n", 276 | " ]\n", 277 | ")\n", 278 | "\n", 279 | "controls_row3 = widgets.HBox(\n", 280 | " [cube_checkbox, pad_checkbox, high_dpi_checkbox, force_render_checkbox]\n", 281 | ")\n", 282 | "\n", 283 | "controls = widgets.VBox([controls_row1, controls_row2, controls_row3])\n", 284 | "\n", 285 | "display(widgets.VBox([controls, nv]))" 286 | ] 287 | } 288 | ], 289 | "metadata": {}, 290 | "nbformat": 4, 291 | "nbformat_minor": 5 292 | } 293 | -------------------------------------------------------------------------------- /js/lib.ts: -------------------------------------------------------------------------------- 1 | import type { NVConfigOptions } from "@niivue/niivue"; 2 | import type { AnyModel, Scene, TypedBufferPayload } from "./types.ts"; 3 | 4 | function delay(ms: number) { 5 | return new Promise((resolve) => setTimeout(resolve, ms)); 6 | } 7 | 8 | function dataViewToBase64(dataView: DataView) { 9 | const uint8Array = new Uint8Array(dataView.buffer); 10 | let binaryString = ""; 11 | const len = uint8Array.byteLength; 12 | for (let i = 0; i < len; i++) { 13 | binaryString += String.fromCharCode(uint8Array[i]); 14 | } 15 | return btoa(binaryString); 16 | } 17 | 18 | export function deserializeOptions( 19 | options: Partial>, 20 | ): NVConfigOptions { 21 | const result: Partial = {}; 22 | const specialValues: Record = { 23 | Infinity: Number.POSITIVE_INFINITY, 24 | "-Infinity": Number.NEGATIVE_INFINITY, 25 | NaN: Number.NaN, 26 | "-0": -0, 27 | }; 28 | 29 | for (const [key, value] of Object.entries(options) as [ 30 | keyof NVConfigOptions, 31 | unknown, 32 | ][]) { 33 | if (typeof value === "string" && value in specialValues) { 34 | // biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions 35 | (result as any)[key] = specialValues[value]; 36 | } else { 37 | // biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions 38 | (result as any)[key] = value; 39 | } 40 | } 41 | 42 | return result as NVConfigOptions; 43 | } 44 | 45 | export function handleBufferMsg( 46 | // biome-ignore lint/suspicious/noExplicitAny: targetObject can be any 47 | targetObject: any, 48 | payload: TypedBufferPayload, 49 | buffers: DataView[], 50 | callback: (data: TypedBufferPayload) => void, 51 | ): boolean { 52 | const { type, data } = payload; 53 | 54 | switch (type) { 55 | case "buffer_change": { 56 | const attrName = data.attr; 57 | const dataType = data.type; 58 | const buffer = buffers[0].buffer; 59 | const TypedArrayConstructor = getTypedArrayConstructor(dataType); 60 | const typedArray = new TypedArrayConstructor(buffer); 61 | 62 | targetObject[attrName] = typedArray; 63 | 64 | callback(payload); 65 | 66 | return true; 67 | } 68 | case "buffer_update": { 69 | const attrName = data.attr; 70 | const dataType = data.type; 71 | const indicesType = data.indices_type; 72 | const [indicesBuffer, valuesBuffer] = [ 73 | buffers[0].buffer, 74 | buffers[1].buffer, 75 | ]; 76 | 77 | const IndicesArrayConstructor = getTypedArrayConstructor(indicesType); 78 | const ValuesArrayConstructor = getTypedArrayConstructor(dataType); 79 | 80 | const indicesArray = new IndicesArrayConstructor(indicesBuffer); 81 | const valuesArray = new ValuesArrayConstructor(valuesBuffer); 82 | 83 | const existingArray = targetObject[attrName] as TypedArray; 84 | 85 | if (!existingArray || existingArray.length === 0) { 86 | console.error( 87 | `Existing array ${attrName} is empty or not initialized.`, 88 | ); 89 | return true; 90 | } 91 | 92 | applyDifferencesToTypedArray(existingArray, indicesArray, valuesArray); 93 | 94 | callback(payload); 95 | 96 | return true; 97 | } 98 | default: 99 | return false; 100 | } 101 | } 102 | 103 | export type TypedArray = 104 | | Float32Array 105 | | Uint32Array 106 | | Uint8Array 107 | | Int16Array 108 | | Int32Array 109 | | Float64Array 110 | | Uint16Array; 111 | 112 | type TypedArrayConstructor = new ( 113 | buffer: ArrayBufferLike, 114 | ) => T; 115 | 116 | const typeMapping: { [key: string]: TypedArrayConstructor } = { 117 | float32: Float32Array, 118 | uint32: Uint32Array, 119 | uint8: Uint8Array, 120 | int16: Int16Array, 121 | int32: Int32Array, 122 | float64: Float64Array, 123 | uint16: Uint16Array, 124 | }; 125 | 126 | export function getArrayType(typedArray: TypedArray): string { 127 | for (const typeStr in typeMapping) { 128 | const c = typeMapping[typeStr]; 129 | if (typedArray instanceof c) { 130 | return typeStr; 131 | } 132 | } 133 | console.log("getArrayType unsupportedarraytype err:", typedArray); 134 | throw new Error("Unsupported array type"); 135 | } 136 | 137 | export function getTypedArrayConstructor( 138 | typeStr: string, 139 | ): TypedArrayConstructor { 140 | const c = typeMapping[typeStr]; 141 | if (c) { 142 | return c; 143 | } 144 | throw new Error(`Unsupported data type: ${typeStr}`); 145 | } 146 | 147 | export function deserializeBufferToTypedArray( 148 | buffer: ArrayBuffer, 149 | typeStr: string, 150 | ): TypedArray { 151 | const TypedArrayConstructor = getTypedArrayConstructor(typeStr); 152 | return new TypedArrayConstructor(buffer); 153 | } 154 | 155 | export function applyDifferencesToTypedArray( 156 | array: TypedArray, 157 | indices: TypedArray, 158 | values: TypedArray, 159 | ): void { 160 | for (let i = 0; i < indices.length; i++) { 161 | const idx = indices[i]; 162 | array[idx] = values[i]; 163 | } 164 | } 165 | 166 | export async function forceSendState( 167 | model: AnyModel, 168 | state: Record, 169 | ) { 170 | const isMarimo = typeof model.send_sync_message === "undefined"; 171 | 172 | if (isMarimo) { 173 | model.onChange(state); 174 | } else { 175 | const msgId = model.send_sync_message(state); 176 | if (typeof model.rememberLastUpdateFor !== "undefined") { 177 | model.rememberLastUpdateFor(msgId); 178 | } 179 | } 180 | } 181 | 182 | export async function sendChunkedData( 183 | model: AnyModel, 184 | dataProperty: string, 185 | arrayBuffer: ArrayBuffer, 186 | dataType: string, 187 | _chunkSize = 5 * 1024 * 1024, 188 | wait = 0, 189 | ) { 190 | const isMarimo = typeof model.send_sync_message === "undefined"; 191 | 192 | const chunkSize = isMarimo 193 | ? Math.min(_chunkSize, 2 * 1024 * 1024) 194 | : _chunkSize; 195 | 196 | const totalSize = arrayBuffer.byteLength; 197 | const totalChunks = Math.ceil(totalSize / chunkSize); 198 | let offset = 0; 199 | let chunkIndex = 0; 200 | 201 | while (offset < totalSize) { 202 | if (!isMarimo && !model._comm_live) { 203 | break; 204 | } 205 | 206 | const chunkEnd = Math.min(offset + chunkSize, totalSize); 207 | const chunk = arrayBuffer.slice(offset, chunkEnd); 208 | const chunkView = new DataView(chunk); 209 | 210 | const attributeName: string = `chunk_${dataProperty}_${chunkIndex}`; 211 | 212 | const data = { 213 | chunk_index: chunkIndex, 214 | total_chunks: totalChunks, 215 | data_type: dataType, 216 | chunk: isMarimo ? dataViewToBase64(chunkView) : chunkView, 217 | }; 218 | const message: Record = {}; 219 | message[attributeName] = data; 220 | 221 | console.log("lib.sendChunkedData:", message); 222 | 223 | if (isMarimo) { 224 | model.onChange(message); 225 | } else { 226 | const msgId = model.send_sync_message(message); 227 | if (typeof model.rememberLastUpdateFor !== "undefined") { 228 | model.rememberLastUpdateFor(msgId); 229 | } 230 | } 231 | 232 | if (wait > 0) { 233 | await delay(wait); 234 | } 235 | 236 | offset = chunkEnd; 237 | chunkIndex += 1; 238 | } 239 | } 240 | 241 | export function gather_models( 242 | model: AnyModel, 243 | ids: Array, 244 | ): Promise> { 245 | // biome-ignore lint/suspicious/noExplicitAny: we know the type of the models 246 | const models: Array> = []; 247 | const widget_manager = model.widget_manager; 248 | for (const id of ids) { 249 | const model_id = id.slice("IPY_MODEL_".length); 250 | models.push(widget_manager.get_model(model_id)); 251 | } 252 | return Promise.all(models); 253 | } 254 | 255 | /** 256 | * A class to keep track of disposers for callbacks for updating the scene. 257 | */ 258 | export class Disposer { 259 | #disposers = new Map void>(); 260 | 261 | register( 262 | obj: { id: string } | { id: string | undefined }, 263 | disposer: () => void, 264 | ): void { 265 | const id = obj.id || ""; 266 | this.#disposers.set(id, disposer); 267 | } 268 | 269 | has(id: string): boolean { 270 | return this.#disposers.has(id); 271 | } 272 | 273 | dispose(id: string): void { 274 | const dispose = this.#disposers.get(id); 275 | if (dispose) { 276 | dispose(); 277 | this.#disposers.delete(id); 278 | } 279 | } 280 | 281 | disposeAll(): void { 282 | for (const dispose of this.#disposers.values()) { 283 | dispose(); 284 | } 285 | this.#disposers.clear(); 286 | } 287 | } 288 | 289 | function numberArraysEqual(a: number[], b: number[]): boolean { 290 | if (a.length !== b.length) return false; 291 | return a.every((val, idx) => val === b[idx]); 292 | } 293 | 294 | export function sceneDiff( 295 | oldScene: Scene | null, 296 | newScene: Scene, 297 | ): Partial { 298 | if (!oldScene) return newScene; 299 | 300 | const diff: Partial = {}; 301 | 302 | if (oldScene.renderAzimuth !== newScene.renderAzimuth) { 303 | diff.renderAzimuth = newScene.renderAzimuth; 304 | } 305 | if (oldScene.renderElevation !== newScene.renderElevation) { 306 | diff.renderElevation = newScene.renderElevation; 307 | } 308 | if (oldScene.volScaleMultiplier !== newScene.volScaleMultiplier) { 309 | diff.volScaleMultiplier = newScene.volScaleMultiplier; 310 | } 311 | if (oldScene.gamma !== newScene.gamma) { 312 | diff.gamma = newScene.gamma; 313 | } 314 | 315 | if ( 316 | oldScene.crosshairPos && 317 | newScene.crosshairPos && 318 | !numberArraysEqual(oldScene.crosshairPos, newScene.crosshairPos) 319 | ) { 320 | diff.crosshairPos = newScene.crosshairPos; 321 | } 322 | if ( 323 | oldScene.clipPlane && 324 | newScene.clipPlane && 325 | !numberArraysEqual(oldScene.clipPlane, newScene.clipPlane) 326 | ) { 327 | diff.clipPlane = newScene.clipPlane; 328 | } 329 | if ( 330 | oldScene.clipPlaneDepthAziElev && 331 | newScene.clipPlaneDepthAziElev && 332 | !numberArraysEqual( 333 | oldScene.clipPlaneDepthAziElev, 334 | newScene.clipPlaneDepthAziElev, 335 | ) 336 | ) { 337 | diff.clipPlaneDepthAziElev = newScene.clipPlaneDepthAziElev; 338 | } 339 | if ( 340 | oldScene.pan2Dxyzmm && 341 | newScene.pan2Dxyzmm && 342 | !numberArraysEqual(oldScene.pan2Dxyzmm, newScene.pan2Dxyzmm) 343 | ) { 344 | diff.pan2Dxyzmm = newScene.pan2Dxyzmm; 345 | } 346 | 347 | return diff; 348 | } 349 | -------------------------------------------------------------------------------- /js/types.ts: -------------------------------------------------------------------------------- 1 | import type { AnyModel as BaseAnyModel } from "@anywidget/types"; 2 | import type { NVConfigOptions } from "@niivue/niivue"; 3 | import type { NIFTI1 } from "nifti-reader-js"; 4 | 5 | export interface AnyModel extends BaseAnyModel { 6 | // biome-ignore lint/suspicious/noExplicitAny: callbacks are Record 7 | send_sync_message(state: object, callbacks?: any): string; 8 | rememberLastUpdateFor(msgId: string): void; 9 | _comm_live: boolean; 10 | // marimo support, since marimo uses POST requests instead of websocket 11 | onChange: (value: Partial) => void; 12 | } 13 | 14 | // just part of the NiivueObject3D in niivue 15 | export type NiivueObject3D = { 16 | id: number; 17 | extents_min: number[]; 18 | extents_max: number[]; 19 | scale: number[]; 20 | furthest_vertex_from_origin?: number; 21 | field_of_view_de_oblique_mm?: number[]; 22 | }; 23 | 24 | interface FileInput { 25 | name: string; 26 | data: DataView; 27 | } 28 | 29 | type ColorMap = { 30 | R: number[]; 31 | G: number[]; 32 | B: number[]; 33 | A: number[]; 34 | I: number[]; 35 | min?: number; 36 | max?: number; 37 | labels?: string[]; 38 | }; 39 | 40 | type LUT = { 41 | lut: Uint8ClampedArray; 42 | min?: number; 43 | max?: number; 44 | labels?: string[]; 45 | }; 46 | 47 | type Graph = { 48 | LTWH: number[]; 49 | opacity: number; 50 | vols: number[]; 51 | autoSizeMultiplanar: boolean; 52 | normalizeValues: boolean; 53 | isRangeCalMinMax: boolean; 54 | 55 | plotLTWH?: number[]; 56 | backColor?: number[]; 57 | lineColor?: number[]; 58 | textColor?: number[]; 59 | lineThickness?: number; 60 | gridLineThickness?: number; 61 | lineAlpha?: number; 62 | lines?: number[][]; 63 | selectedColumn?: number; 64 | lineRGB?: number[][]; 65 | }; 66 | 67 | export type Scene = { 68 | renderAzimuth?: number; 69 | renderElevation?: number; 70 | volScaleMultiplier?: number; 71 | crosshairPos?: number[]; 72 | clipPlane?: number[]; 73 | clipPlaneDepthAziElev?: number[]; 74 | pan2Dxyzmm?: number[]; 75 | gamma?: number; 76 | }; 77 | 78 | export type VolumeModel = AnyModel<{ 79 | path: FileInput; 80 | url: string; 81 | data: DataView; 82 | 83 | paired_img_path: FileInput; 84 | paired_img_url: string; 85 | paired_img_data: DataView; 86 | 87 | id: string; 88 | name: string; 89 | colormap: string; 90 | opacity: number; 91 | visible: boolean; 92 | colorbar_visible: boolean; 93 | cal_min: number; 94 | cal_max: number; 95 | cal_min_neg: number; 96 | cal_max_neg: number; 97 | frame_4d: number; 98 | colormap_negative: string; 99 | colormap_label: LUT; 100 | colormap_type: number; 101 | 102 | colormap_invert: boolean; 103 | n_frame_4d: number | null; 104 | modulation_image: number | null; 105 | modulate_alpha: number; 106 | 107 | hdr: Partial; // only updated via frontend...but this might change in the future.. 108 | img: DataView; 109 | dims: number[]; 110 | extents_min_ortho: number[]; 111 | extents_max_ortho: number[]; 112 | frac2mm: number[]; 113 | frac2mm_ortho: number[]; 114 | dims_ras: number[]; 115 | mat_ras: number[]; 116 | }>; 117 | 118 | export type MeshModel = AnyModel<{ 119 | path: FileInput; 120 | url: string; 121 | data: DataView; 122 | 123 | id: string; 124 | name: string; 125 | rgba255: Array; 126 | opacity: number; 127 | layers: Array; 128 | visible: boolean; 129 | 130 | colormap_invert: boolean; 131 | colorbar_visible: boolean; 132 | mesh_shader_index: number; 133 | fiber_radius: number; 134 | fiber_length: number; 135 | fiber_dither: number; 136 | fiber_color: string; 137 | fiber_decimation_stride: number; 138 | colormap: string; 139 | 140 | pts: DataView; 141 | tris: DataView; 142 | extents_min: number[]; 143 | extents_max: number[]; 144 | }>; 145 | 146 | export type MeshLayerModel = AnyModel<{ 147 | path: FileInput; 148 | url: string; 149 | data: DataView; 150 | 151 | id: string; 152 | name: string; 153 | opacity: number; 154 | colormap: string; 155 | colormap_negative: string; 156 | use_negative_cmap: boolean; 157 | cal_min: number; 158 | cal_max: number; 159 | outline_border: number; 160 | 161 | colormap_invert: boolean; 162 | frame_4d: number; 163 | colorbar_visible: boolean; 164 | }>; 165 | 166 | export type Model = AnyModel<{ 167 | this_model_id: string; 168 | 169 | height: number; 170 | volumes: Array; 171 | meshes: Array; 172 | opts: Partial>; 173 | 174 | _canvas_attached: boolean; 175 | 176 | background_masks_overlays: number; 177 | draw_lut: LUT; 178 | draw_opacity: number; 179 | draw_fill_overwrites: boolean; 180 | graph: Graph; 181 | scene: Scene; 182 | overlay_outline_width: number; 183 | overlay_alpha_shader: number; 184 | 185 | _volume_object_3d_data: NiivueObject3D; // only updated via frontend (1-way comm) 186 | 187 | draw_bitmap: DataView | null; 188 | }>; 189 | 190 | // Custom message datas 191 | type SaveDocumentData = [fileName: string, compress: boolean]; 192 | 193 | type SaveHTMLData = [fileName: string, canvasId: string]; 194 | 195 | type SaveImageData = [ 196 | filename: string, 197 | isSaveDrawing: boolean, 198 | volumeByIndex: number, 199 | ]; 200 | 201 | type SaveSceneData = [fileName: string]; 202 | 203 | type AddColormapData = [name: string, cmap: ColorMap]; 204 | 205 | type SetGammaData = [gamma: number]; 206 | 207 | type SetVolumeRenderIlluminationData = [gradientAmount: number]; 208 | 209 | type LoadPngAsTextureData = [pngUrl: string, textureNum: number]; 210 | 211 | type SetInterpolationData = [isNearest: boolean]; 212 | 213 | type SetDrawingEnabledData = [drawingEnabled: boolean]; 214 | 215 | type DrawOtsuData = [levels: number]; 216 | 217 | type MoveCrosshairInVoxData = [x: number, y: number, z: number]; 218 | 219 | type RemoveHazeData = [level: number, volIndex: number]; 220 | 221 | type LoadDrawingFromUrlData = [url: string, isBinarize: boolean]; 222 | 223 | type LoadDocumentFromUrlData = [url: string]; 224 | 225 | type SaveToDiskData = [fileName: string]; 226 | 227 | export type CustomMessagePayload = 228 | | { type: "save_document"; data: SaveDocumentData } 229 | | { type: "save_html"; data: SaveHTMLData } 230 | | { type: "save_image"; data: SaveImageData } 231 | | { type: "save_scene"; data: SaveSceneData } 232 | | { type: "add_colormap"; data: AddColormapData } 233 | | { type: "set_gamma"; data: SetGammaData } 234 | | { type: "resize_listener"; data: [] } 235 | | { type: "draw_scene"; data: [] } 236 | | { type: "update_gl_volume"; data: [] } 237 | | { 238 | type: "set_volume_render_illumination"; 239 | data: SetVolumeRenderIlluminationData; 240 | } 241 | | { type: "load_png_as_texture"; data: LoadPngAsTextureData } 242 | | { type: "set_interpolation"; data: SetInterpolationData } 243 | | { type: "set_drawing_enabled"; data: SetDrawingEnabledData } 244 | | { type: "draw_otsu"; data: DrawOtsuData } 245 | | { type: "draw_grow_cut"; data: [] } 246 | | { type: "move_crosshair_in_vox"; data: MoveCrosshairInVoxData } 247 | | { type: "remove_haze"; data: RemoveHazeData } 248 | | { type: "draw_undo"; data: [] } 249 | | { type: "close_drawing"; data: [] } 250 | | { type: "load_drawing_from_url"; data: LoadDrawingFromUrlData } 251 | | { type: "load_document_from_url"; data: LoadDocumentFromUrlData }; 252 | 253 | export type VolumeCustomMessage = { 254 | type: "save_to_disk"; 255 | data: SaveToDiskData; 256 | }; 257 | 258 | export type TypedBufferPayload = 259 | | { 260 | type: "buffer_change"; 261 | data: { 262 | attr: string; 263 | type: string; 264 | }; 265 | } 266 | | { 267 | type: "buffer_update"; 268 | data: { 269 | attr: string; 270 | type: string; 271 | indices_type: string; 272 | }; 273 | }; 274 | -------------------------------------------------------------------------------- /js/types/niivue-fix.d.ts: -------------------------------------------------------------------------------- 1 | import { Niivue } from "@niivue/niivue"; 2 | 3 | declare module "@niivue/niivue" { 4 | interface Niivue { 5 | setMeshShader(meshId: string, shader: string): void; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /js/types/niivue-min.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@niivue/niivue/min"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "node build.js --watch", 5 | "build": "node build.js", 6 | "lint": "biome ci .", 7 | "fix": "biome check --fix .", 8 | "typecheck": "tsc" 9 | }, 10 | "dependencies": { 11 | "@anywidget/types": "^0.2.0", 12 | "@niivue/niivue": "^0.62.1" 13 | }, 14 | "devDependencies": { 15 | "@biomejs/biome": "^1.9.4", 16 | "esbuild": "^0.25.2", 17 | "typescript": "^5.8.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "ipyniivue" 7 | license = { text = "BSD-2-Clause" } 8 | dynamic = ["version"] 9 | description = "A Jupyter Widget for Niivue based on anywidget." 10 | dependencies = ["anywidget", "requests", "numpy>=2.0.2"] 11 | readme = "README.md" 12 | requires-python = ">=3.9" 13 | classifiers = [ 14 | "Framework :: Jupyter", 15 | "Framework :: Jupyter :: JupyterLab", 16 | "Framework :: Jupyter :: JupyterLab :: 4", 17 | "Framework :: Jupyter :: JupyterLab :: Extensions", 18 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | ] 23 | 24 | [project.urls] 25 | homepage = "https://github.com/niivue/ipyniivue" 26 | 27 | [project.optional-dependencies] 28 | dev = ["watchfiles", "jupyterlab", "ruff", "pytest", "sphinx", "furo", "sphinxcontrib-mermaid", "myst-parser"] 29 | 30 | [pytest] 31 | nb_test_files = true 32 | 33 | [tool.hatch.envs.default] 34 | features = ["dev"] 35 | uv = true 36 | 37 | # https://github.com/ofek/hatch-vcs 38 | [tool.hatch.version] 39 | source = "vcs" 40 | 41 | [tool.hatch.build] 42 | only-packages = true 43 | artifacts = ["src/ipyniivue/static/*"] 44 | 45 | [tool.hatch.build.hooks.jupyter-builder] 46 | build-function = "hatch_jupyter_builder.npm_builder" 47 | dependencies = ["hatch-jupyter-builder>=0.5.0"] 48 | 49 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 50 | npm = "npm" 51 | build_cmd = "build" 52 | 53 | [tool.hatch.envs.default.scripts] 54 | lint = ["ruff check . {args:.}", "ruff format . --check --diff {args:.}"] 55 | format = ["ruff format . {args:.}", "ruff check . --fix {args:.}"] 56 | test = ["pytest {args:.}"] 57 | docs = "cd docs && make clean && make html" 58 | 59 | [tool.ruff.lint] 60 | pydocstyle = { convention = "numpy" } 61 | select = [ 62 | "E", # style errors 63 | "W", # style warnings 64 | "F", # flakes 65 | "D", # pydocstyle 66 | "D417", # Missing argument descriptions in Docstrings 67 | "I", # isort 68 | "UP", # pyupgrade 69 | "C4", # flake8-comprehensions 70 | "B", # flake8-bugbear 71 | "A001", # flake8-builtins 72 | "RUF", # ruff-specific rules 73 | "TCH", # flake8-type-checking 74 | "TID", # flake8-tidy-imports 75 | ] 76 | 77 | [tool.ruff.lint.per-file-ignores] 78 | "tests/*.py" = ["D", "S"] 79 | "scripts/*.py" = ["D", "S"] 80 | -------------------------------------------------------------------------------- /scripts/generate_config_options.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import math 3 | import pathlib 4 | import typing 5 | 6 | from ipyniivue.constants import ( 7 | _SNAKE_TO_CAMEL_OVERRIDES, 8 | DragMode, 9 | MultiplanarType, 10 | PenType, 11 | ShowRender, 12 | SliceType, 13 | ) 14 | 15 | RENAME_OVERRIDES = {v: k for k, v in _SNAKE_TO_CAMEL_OVERRIDES.items()} 16 | 17 | 18 | def camel_to_snake(name: str): 19 | return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") 20 | 21 | 22 | def type_hint(value: typing.Any): 23 | if isinstance(value, bool): 24 | return "t.Bool" 25 | elif isinstance(value, int): 26 | return "t.Int" 27 | elif isinstance(value, float): 28 | return "t.Float" 29 | elif isinstance(value, str): 30 | return "t.Unicode" 31 | elif isinstance(value, tuple): 32 | return "t.Tuple" 33 | elif isinstance(value, list): 34 | return "t.List" 35 | elif value is None: 36 | return "t.Any" 37 | elif isinstance(value, enum.Enum): 38 | enum_class_name = type(value).__name__ 39 | return f"t.UseEnum({enum_class_name}" 40 | else: 41 | return "t.Any" 42 | 43 | 44 | def get_default_value(value: typing.Any): 45 | if value == float("inf"): 46 | return 'float("inf")' 47 | if isinstance(value, float) and math.isnan(value): 48 | return 'float("nan")' 49 | if isinstance(value, enum.Enum): 50 | enum_class_name = type(value).__name__ 51 | return f"{enum_class_name}.{value.name}" 52 | if isinstance(value, str): 53 | return f'"{value}"' 54 | return repr(value) 55 | 56 | 57 | def generate_config_options(options: dict[str, typing.Any]): 58 | lines = [ 59 | "# This file is automatically generated by scripts/generate_options_mixin.py", 60 | "# Do not edit this file directly", 61 | "", 62 | '"""Defines a class for NiiVue configuration options."""', 63 | "", 64 | "import traitlets as t", 65 | "", 66 | "from ipyniivue.constants import (", 67 | " DragMode,", 68 | " MultiplanarType,", 69 | " PenType,", 70 | " ShowRender,", 71 | " SliceType,", 72 | ")", 73 | "", 74 | '__all__ = ["CAMEL_TO_SNAKE", "SNAKE_TO_CAMEL", "ConfigOptions"]', 75 | "", 76 | "", 77 | "class ConfigOptions(t.HasTraits):", 78 | ' """Configuration options for NiiVue."""', 79 | "", 80 | " _parent = None", 81 | "", 82 | ] 83 | 84 | for option, value in options.items(): 85 | # Convert camelCase to snake_case 86 | snake_name = RENAME_OVERRIDES.get(option, camel_to_snake(option)) 87 | hint = type_hint(value) 88 | default_value = get_default_value(value) 89 | if "UseEnum" in hint: 90 | # For Enums, default_value is passed as a keyword argument 91 | lines.append( 92 | f" {snake_name} = {hint}, " 93 | f"default_value={default_value}).tag(sync=False)" 94 | ) 95 | else: 96 | lines.append(f" {snake_name} = {hint}({default_value}).tag(sync=False)") 97 | lines.append("") 98 | 99 | # Add __init__ method 100 | lines.append(" def __init__(self, parent=None, **kwargs):") 101 | lines.append(" super().__init__(**kwargs)") 102 | lines.append(" self._parent = parent") 103 | lines.append("") 104 | 105 | # Add observe method 106 | option_names = [ 107 | RENAME_OVERRIDES.get(option, camel_to_snake(option)) 108 | for option in options.keys() 109 | ] 110 | 111 | lines.append(" _OBSERVED_TRAITS = (") 112 | for name in option_names: 113 | lines.append(f' "{name}",') 114 | lines.append(" )") 115 | lines.append("") 116 | 117 | lines.append(" @t.observe(*_OBSERVED_TRAITS)") 118 | lines.append(" def _propagate_parent_change(self, change):") 119 | lines.append( 120 | " if self._parent and callable(" 121 | 'getattr(self._parent, "_notify_opts_changed", None)):' 122 | ) 123 | lines.append(" self._parent._notify_opts_changed()") 124 | lines.append("") 125 | 126 | option_names = options.keys() 127 | snake_case_names = [ 128 | RENAME_OVERRIDES.get(option, camel_to_snake(option)) for option in option_names 129 | ] 130 | mappings = dict(zip(option_names, snake_case_names)) 131 | lines.append("CAMEL_TO_SNAKE = {") 132 | for orig_name, snake_name in mappings.items(): 133 | lines.append(f' "{orig_name}": "{snake_name}",') 134 | lines.append("}") 135 | lines.append("") 136 | 137 | lines.append("SNAKE_TO_CAMEL = {v: k for k, v in CAMEL_TO_SNAKE.items()}") 138 | 139 | return "\n".join(lines) 140 | 141 | 142 | if __name__ == "__main__": 143 | # Copied from niivue (should be able to automatically generate this) 144 | DEFAULT_OPTIONS = { 145 | "textHeight": -1.0, 146 | "fontSizeScaling": 0.4, 147 | "fontMinPx": 13, 148 | "colorbarHeight": 0.05, 149 | "colorbarWidth": -1.0, 150 | "showColorbarBorder": True, 151 | "crosshairWidth": 1.0, 152 | "crosshairWidthUnit": "voxels", 153 | "crosshairGap": 0.0, 154 | "rulerWidth": 4.0, 155 | "show3Dcrosshair": False, 156 | "backColor": (0.0, 0.0, 0.0, 1.0), 157 | "crosshairColor": (1.0, 0.0, 0.0, 1.0), 158 | "fontColor": (0.5, 0.5, 0.5, 1.0), 159 | "selectionBoxColor": (1.0, 1.0, 1.0, 0.5), 160 | "clipPlaneColor": (0.7, 0.0, 0.7, 0.5), 161 | "paqdUniforms": (0.3, 0.5, 0.5, 1.0), 162 | "clipThick": 2.0, 163 | "clipVolumeLow": (0.0, 0.0, 0.0), 164 | "clipVolumeHigh": (1.0, 1.0, 1.0), 165 | "rulerColor": (1.0, 0.0, 0.0, 0.8), 166 | "colorbarMargin": 0.05, 167 | "trustCalMinMax": True, 168 | "clipPlaneHotKey": "KeyC", 169 | "viewModeHotKey": "KeyV", 170 | "doubleTouchTimeout": 500, 171 | "longTouchTimeout": 1000, 172 | "keyDebounceTime": 50, 173 | "isNearestInterpolation": False, 174 | "isResizeCanvas": True, 175 | "atlasOutline": 0.0, 176 | "atlasActiveIndex": 0, 177 | "isRuler": False, 178 | "isColorbar": False, 179 | "isOrientCube": False, 180 | "tileMargin": 0.0, 181 | "multiplanarPadPixels": 0, 182 | "multiplanarForceRender": False, 183 | "multiplanarEqualSize": False, 184 | "multiplanarShowRender": ShowRender.AUTO, 185 | "isRadiologicalConvention": False, 186 | "meshThicknessOn2D": float("inf"), 187 | "dragMode": DragMode.CONTRAST, 188 | "dragModePrimary": DragMode.CROSSHAIR, 189 | "mouseEventConfig": None, 190 | "touchEventConfig": None, 191 | "yoke3Dto2DZoom": False, 192 | "isDepthPickMesh": False, 193 | "isCornerOrientationText": False, 194 | "isOrientationTextVisible": True, 195 | "showAllOrientationMarkers": False, 196 | "heroImageFraction": 0.0, 197 | "heroSliceType": SliceType.RENDER, 198 | "sagittalNoseLeft": False, 199 | "isSliceMM": False, 200 | "isV1SliceShader": False, 201 | "forceDevicePixelRatio": 0.0, 202 | "logLevel": "info", 203 | "loadingText": "loading ...", 204 | "isForceMouseClickToVoxelCenters": False, 205 | "dragAndDropEnabled": True, 206 | "drawingEnabled": False, 207 | "penValue": 1.0, 208 | "penType": PenType.PEN, 209 | "floodFillNeighbors": 6, 210 | "isFilledPen": False, 211 | "thumbnail": "", 212 | "maxDrawUndoBitmaps": 8, 213 | "sliceType": SliceType.MULTIPLANAR, 214 | "meshXRay": 0.0, 215 | "isAntiAlias": None, 216 | "limitFrames4D": float("nan"), 217 | "isAdditiveBlend": False, 218 | "showLegend": True, 219 | "legendBackgroundColor": (0.3, 0.3, 0.3, 0.5), 220 | "legendTextColor": (1.0, 1.0, 1.0, 1.0), 221 | "multiplanarLayout": MultiplanarType.AUTO, 222 | "renderOverlayBlend": 1.0, 223 | "sliceMosaicString": "", 224 | "centerMosaic": False, 225 | "penSize": 1.0, 226 | "interactive": True, 227 | "clickToSegment": False, 228 | "clickToSegmentRadius": 3.0, 229 | "clickToSegmentBright": True, 230 | "clickToSegmentAutoIntensity": False, 231 | "clickToSegmentIntensityMax": float("nan"), 232 | "clickToSegmentIntensityMin": float("nan"), 233 | "clickToSegmentPercent": 0.0, 234 | "clickToSegmentMaxDistanceMM": float("inf"), 235 | "clickToSegmentIs2D": False, 236 | "selectionBoxLineThickness": 4.0, 237 | "selectionBoxIsOutline": False, 238 | "scrollRequiresFocus": False, 239 | "showMeasureUnits": True, 240 | "measureTextJustify": "center", 241 | "measureTextColor": (1.0, 0.0, 0.0, 1.0), 242 | "measureLineColor": (1.0, 0.0, 0.0, 1.0), 243 | "measureTextHeight": 0.06, 244 | "isAlphaClipDark": False, 245 | "gradientOrder": 1, 246 | "gradientOpacity": 0.0, 247 | "renderSilhouette": 0.0, 248 | "gradientAmount": 0.0, 249 | "invertScrollDirection": False, 250 | "is2DSliceShader": False, 251 | } 252 | code = generate_config_options(DEFAULT_OPTIONS) 253 | loc = pathlib.Path(__file__).parent / "../src/ipyniivue/config_options.py" 254 | loc.write_text(code) 255 | -------------------------------------------------------------------------------- /src/ipyniivue/__init__.py: -------------------------------------------------------------------------------- 1 | """A Jupyter widget for Niivue based on anywidget.""" 2 | 3 | import importlib.metadata 4 | 5 | from .constants import ( # noqa: F401 6 | ColormapType, 7 | DragMode, 8 | MultiplanarType, 9 | PenType, 10 | ShowRender, 11 | SliceType, 12 | ) 13 | from .download_dataset import download_dataset # noqa: F401 14 | from .traits import ( # noqa: F401 15 | LUT, 16 | ColorMap, 17 | Graph, 18 | ) 19 | from .widget import ( # noqa: F401 20 | Mesh, 21 | MeshLayer, 22 | NiiVue, 23 | Volume, 24 | WidgetObserver, 25 | ) 26 | 27 | __version__ = importlib.metadata.version("ipyniivue") 28 | -------------------------------------------------------------------------------- /src/ipyniivue/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides classes for reusable, readable versions of important data. 3 | 4 | This module uses enum to enumerate important constants. 5 | """ 6 | 7 | import enum 8 | 9 | __all__ = [ 10 | "ColormapType", 11 | "DragMode", 12 | "MultiplanarType", 13 | "PenType", 14 | "ShowRender", 15 | "SliceType", 16 | ] 17 | 18 | 19 | class SliceType(enum.Enum): 20 | """ 21 | An enumeration of slice types for NiiVue instances. 22 | 23 | Members 24 | ------- 25 | AXIAL : int 26 | Axial slice type (value 0). 27 | CORONAL : int 28 | Coronal slice type (value 1). 29 | SAGITTAL : int 30 | Sagittal slice type (value 2). 31 | MULTIPLANAR : int 32 | Multiplanar view type (value 3). 33 | RENDER : int 34 | Render view type (value 4). 35 | """ 36 | 37 | AXIAL = 0 38 | CORONAL = 1 39 | SAGITTAL = 2 40 | MULTIPLANAR = 3 41 | RENDER = 4 42 | 43 | 44 | class PenType(enum.Enum): 45 | """ 46 | An enumeration of pen types for drawing tools. 47 | 48 | Members 49 | ------- 50 | PEN : int 51 | Standard pen (freehand drawing) (value 0). 52 | RECTANGLE : int 53 | Rectangle drawing mode (value 1). 54 | ELLIPSE : int 55 | Ellipse drawing mode (value 2). 56 | """ 57 | 58 | PEN = 0 59 | RECTANGLE = 1 60 | ELLIPSE = 2 61 | 62 | 63 | class ShowRender(enum.Enum): 64 | """ 65 | An enumeration for specifying when to show rendering in NiiVue instances. 66 | 67 | Members 68 | ------- 69 | NEVER : int 70 | Never show rendering (value 0). 71 | ALWAYS : int 72 | Always show rendering (value 1). 73 | AUTO : int 74 | Automatically determine whether to show rendering (value 2). 75 | """ 76 | 77 | NEVER = 0 78 | ALWAYS = 1 79 | AUTO = 2 80 | 81 | 82 | class MultiplanarType(enum.Enum): 83 | """ 84 | An enumeration of multiplanar types for NiiVue instances. 85 | 86 | Members 87 | ------- 88 | AUTO : int 89 | Automatic multiplanar layout (value 0). 90 | COLUMN : int 91 | Vertical column multiplanar layout (value 1). 92 | GRID : int 93 | Grid multiplanar layout (value 2). 94 | ROW : int 95 | Horizontal row multiplanar layout (value 3). 96 | """ 97 | 98 | AUTO = 0 99 | COLUMN = 1 100 | GRID = 2 101 | ROW = 3 102 | 103 | 104 | class DragMode(enum.Enum): 105 | """ 106 | An enumeration of drag modes for NiiVue instances. 107 | 108 | Members 109 | ------- 110 | NONE : int 111 | No drag mode active (value 0). 112 | CONTRAST : int 113 | Contrast adjustment mode (value 1). 114 | MEASUREMENT : int 115 | Measurement mode for taking measurements (value 2). 116 | PAN : int 117 | Pan mode for moving around the image (value 3). 118 | SLICER_3D : int 119 | 3D slicer interaction mode (value 4). 120 | CALLBACK_ONLY : int 121 | Callback only mode (value 5). 122 | ROI_SELECTION : int 123 | ROI (Region of Interest) selection mode (value 6). 124 | ANGLE : int 125 | Angle measurement mode (value 7). 126 | CROSSHAIR : int 127 | Crosshair interaction mode (value 8). 128 | WINDOWING : int 129 | Windowing adjustment mode (value 9). 130 | """ 131 | 132 | NONE = 0 133 | CONTRAST = 1 134 | MEASUREMENT = 2 135 | PAN = 3 136 | SLICER_3D = 4 137 | CALLBACK_ONLY = 5 138 | ROI_SELECTION = 6 139 | ANGLE = 7 140 | CROSSHAIR = 8 141 | WINDOWING = 9 142 | 143 | 144 | class ColormapType(enum.Enum): 145 | """ 146 | An enumeration of colormap types. 147 | 148 | Members 149 | ------- 150 | MIN_TO_MAX : int 151 | Colormap spans from minimum to maximum values (value 0). 152 | ZERO_TO_MAX_TRANSPARENT_BELOW_MIN : int 153 | Colormap spans from zero to maximum, transparent below minimum (value 1). 154 | ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN : int 155 | Colormap spans from zero to maximum, translucent below minimum (value 2). 156 | """ 157 | 158 | MIN_TO_MAX = 0 159 | ZERO_TO_MAX_TRANSPARENT_BELOW_MIN = 1 160 | ZERO_TO_MAX_TRANSLUCENT_BELOW_MIN = 2 161 | 162 | 163 | _SNAKE_TO_CAMEL_OVERRIDES = { 164 | "show_3d_crosshair": "show3Dcrosshair", 165 | "mesh_thickness_on_2d": "meshThicknessOn2D", 166 | "yoke_3d_to_2d_zoom": "yoke3Dto2DZoom", 167 | "is_slice_mm": "isSliceMM", 168 | "limit_frames_4d": "limitFrames4D", 169 | "click_to_segment_is_2d": "clickToSegmentIs2D", 170 | "is_v1_slice_shader": "isV1SliceShader", 171 | "mesh_xray": "meshXRay", 172 | "click_to_segment_max_distance_mm": "clickToSegmentMaxDistanceMM", 173 | "is_2d_slice_shader": "is2DSliceShader", 174 | } 175 | -------------------------------------------------------------------------------- /src/ipyniivue/download_dataset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Downloads the data needed to run provided examples. 3 | 4 | This module is designed to download the nii files needed to run examples and is 5 | set to do so if another url is not provided. It can, however, download other 6 | data, like fonts, from the NiiVue repo or elsewhere. 7 | """ 8 | 9 | from pathlib import Path 10 | 11 | import requests 12 | 13 | import ipyniivue 14 | 15 | BASE_API_URL = ( 16 | "https://api.github.com/repos/niivue/niivue/contents/packages/niivue/demos/images" 17 | ) 18 | DATA_FOLDER = Path(ipyniivue.__file__).parent / "images" 19 | 20 | 21 | def download_dataset(api_url=None, dest_folder=None, force_download=False, files=None): 22 | """ 23 | Download the datasets used for demos and testing. 24 | 25 | Parameters 26 | ---------- 27 | api_url : str, optional 28 | The API URL to fetch the dataset from. Defaults to the base URL. 29 | dest_folder : Path, optional 30 | The destination folder to save the downloaded files. Defaults to the 31 | images directory in ipyniivue. 32 | force_download : bool, optional 33 | If True, download files even if they already exist locally. Defaults to 34 | False. 35 | files : list of str, optional 36 | A list of file paths (relative to the base API URL) to download. If 37 | None, all files will be downloaded. 38 | 39 | Raises 40 | ------ 41 | FileNotFoundError 42 | If a specified file is not found in the dataset. 43 | Exception 44 | If a file cannot be downloaded due to HTTP errors. 45 | """ 46 | if api_url is None: 47 | api_url = BASE_API_URL 48 | if dest_folder is None: 49 | dest_folder = DATA_FOLDER 50 | 51 | dest_folder.mkdir(parents=True, exist_ok=True) 52 | 53 | if files: 54 | for file_path in files: 55 | local_file_path = dest_folder / file_path 56 | if local_file_path.exists() and not force_download: 57 | print(f"{file_path} already exists.") 58 | continue 59 | 60 | file_api_url = f"{api_url}/{file_path}" 61 | response = requests.get(file_api_url) 62 | if response.status_code != 200: 63 | raise FileNotFoundError( 64 | f"File {file_path} not found (HTTP {response.status_code})." 65 | ) 66 | 67 | content_type = response.headers.get("Content-Type") 68 | 69 | if ( 70 | content_type 71 | and "application/json" in content_type.lower() 72 | and not file_path.endswith(".json") 73 | ): 74 | file_info = response.json() 75 | download_url = file_info.get("download_url") 76 | if not download_url: 77 | raise Exception(f"No download URL for {file_path}.") 78 | else: 79 | download_url = file_api_url 80 | 81 | print(f"Downloading {file_path}...") 82 | file_response = requests.get(download_url) 83 | file_response.raise_for_status() 84 | local_file_path.parent.mkdir(parents=True, exist_ok=True) 85 | local_file_path.write_bytes(file_response.content) 86 | 87 | else: 88 | response = requests.get(api_url) 89 | response.raise_for_status() 90 | file_list = response.json() 91 | 92 | for item in file_list: 93 | if item["type"] != "file": 94 | continue 95 | 96 | local_file_path = dest_folder / item["name"] 97 | if local_file_path.exists() and not force_download: 98 | print(f"{item['name']} already exists.") 99 | continue 100 | 101 | print(f"Downloading {item['name']}...") 102 | download_url = item["download_url"] 103 | file_response = requests.get(download_url) 104 | file_response.raise_for_status() 105 | local_file_path.write_bytes(file_response.content) 106 | 107 | print(f"Dataset downloaded successfully to {dest_folder}.") 108 | -------------------------------------------------------------------------------- /tests/test_ipyniivue.py: -------------------------------------------------------------------------------- 1 | def test_it_loads(): 2 | import ipyniivue 3 | 4 | assert ipyniivue.__version__ is not None 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "strict": true, 7 | "noEmit": true, 8 | "verbatimModuleSyntax": true, 9 | "skipLibCheck": true, 10 | "allowImportingTsExtensions": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------