├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .napari
└── DESCRIPTION.md
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── bin
├── check.sh
├── clean.sh
├── lint.sh
├── setup.sh
└── test.sh
├── codecov.yml
├── dev-environment.yml
├── doc
├── Architecture.rst
├── Benchmarking.rst
├── Configuration.rst
├── Development.rst
├── Initialization.rst
├── Install.rst
├── Makefile
├── README.md
├── Troubleshooting.rst
├── Usage.rst
├── Use_Cases.rst
├── conf.py
├── environment.yml
├── examples
│ ├── bonej.rst
│ ├── ops.rst
│ ├── scripting.rst
│ ├── trackmate.rst
│ └── trackmate_reader.rst
├── index.rst
├── make.bat
└── requirements.txt
├── environment.yml
├── mkdocs.yml
├── pyproject.toml
├── scripts
└── examples
│ └── Example_Script.py
├── src
└── napari_imagej
│ ├── __init__.py
│ ├── java.py
│ ├── model.py
│ ├── napari.yml
│ ├── readers
│ └── trackMate_reader.py
│ ├── resources
│ ├── __init__.py
│ ├── export.svg
│ ├── export_detailed.svg
│ ├── gear.svg
│ ├── imagej2-16x16-flat-disabled.png
│ ├── imagej2-16x16-flat.png
│ ├── import.svg
│ └── repl.svg
│ ├── settings.py
│ ├── types
│ ├── converters
│ │ ├── __init__.py
│ │ ├── enum_likes.py
│ │ ├── enums.py
│ │ ├── images.py
│ │ ├── labels.py
│ │ ├── meshes.py
│ │ ├── points.py
│ │ ├── shapes.py
│ │ └── trackmate.py
│ ├── enum_likes.py
│ ├── enums.py
│ ├── type_conversions.py
│ ├── type_hints.py
│ ├── type_utils.py
│ └── widget_mappings.py
│ ├── utilities
│ ├── _module_utils.py
│ ├── event_subscribers.py
│ ├── events.py
│ └── progress_manager.py
│ └── widgets
│ ├── info_bar.py
│ ├── layouts.py
│ ├── menu.py
│ ├── napari_imagej.py
│ ├── parameter_widgets.py
│ ├── repl.py
│ ├── result_runner.py
│ ├── result_tree.py
│ ├── searchbar.py
│ └── widget_utils.py
└── tests
├── __init__.py
├── conftest.py
├── test_java.py
├── test_scripting.py
├── test_settings.py
├── types
├── test_converters.py
├── test_enums.py
├── test_trackmate.py
├── test_type_conversions.py
└── test_widget_mappings.py
├── utilities
├── test_module_utils.py
└── test_progress.py
├── utils.py
└── widgets
├── test_info_bar.py
├── test_menu.py
├── test_napari_imagej.py
├── test_parameter_widgets.py
├── test_result_runner.py
├── test_result_tree.py
├── test_searchbar.py
└── widget_utils.py
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: tests
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | tags:
11 | - "*-[0-9]+.*"
12 | pull_request:
13 | branches:
14 | - main
15 |
16 | env:
17 | NAPARI_IMAGEJ_TEST_TIMEOUT: 60000
18 |
19 | jobs:
20 | test-pip:
21 | name: ${{ matrix.platform }} (python ${{ matrix.python-version }}, java ${{matrix.java-version}})
22 | runs-on: ${{ matrix.platform }}
23 | strategy:
24 | matrix:
25 | platform: [ubuntu-latest, windows-latest, macos-latest]
26 | python-version: ['3.9', '3.12']
27 | java-version: ['8', '21']
28 |
29 | steps:
30 | - uses: actions/checkout@v2
31 |
32 | - name: Set up Python ${{ matrix.python-version }}
33 | uses: actions/setup-python@v2
34 | with:
35 | python-version: ${{ matrix.python-version }}
36 |
37 | - name: Set up Java ${{ matrix.java-version }}
38 | uses: actions/setup-java@v3
39 | with:
40 | java-version: ${{matrix.java-version}}
41 | distribution: 'zulu'
42 |
43 | - name: Setup Qt libraries
44 | uses: tlambert03/setup-qt-libs@v1
45 |
46 | # strategy borrowed from vispy for installing opengl libs on windows
47 | - name: Install Windows OpenGL
48 | if: runner.os == 'Windows'
49 | run: |
50 | git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
51 | powershell gl-ci-helpers/appveyor/install_opengl.ps1
52 |
53 | # We run headless on CI. This yields issues on Mac, where running Java
54 | # headless will alter the screen size on Python, leading to errors. Adding
55 | # this environment variable prevents Java from modifying the screen size.
56 | - name: Set MacOS environment variables
57 | if: runner.os == 'MacOS'
58 | run: |
59 | echo "AWT_FORCE_HEADFUL=true" >> $GITHUB_ENV
60 |
61 | - name: Install napari-imagej
62 | run: |
63 | python -m pip install --upgrade pip
64 | python -m pip install -e '.[dev]'
65 |
66 | - name: Test napari-imagej
67 | uses: coactions/setup-xvfb@v1
68 | with:
69 | run:
70 | bash bin/test.sh
71 |
72 | ensure-clean-code:
73 | runs-on: ubuntu-latest
74 | steps:
75 | - uses: actions/checkout@v2
76 | - uses: actions/setup-python@v3
77 | with:
78 | python-version: 3.9
79 | # This step ensures that everything is installed
80 | - name: Upgrade pip
81 | run: |
82 | python -m pip install --upgrade pip
83 |
84 | - name: Run Ruff
85 | uses: astral-sh/ruff-action@v1
86 |
87 | - name: Validate pyproject.toml
88 | run: |
89 | python -m pip install "validate-pyproject[all]"
90 | python -m validate_pyproject pyproject.toml
91 |
92 | conda-dev-test:
93 | name: Conda Setup & Code Coverage
94 | runs-on: ubuntu-latest
95 | defaults:
96 | # Steps that rely on the activated environment must be run with this shell setup.
97 | # See https://github.com/marketplace/actions/setup-miniconda#important
98 | run:
99 | shell: bash -l {0}
100 | steps:
101 | - uses: actions/checkout@v2
102 | - name: Cache conda
103 | uses: actions/cache@v4
104 | env:
105 | # Increase this value to reset cache if dev-environment.yml has not changed
106 | CACHE_NUMBER: 0
107 | with:
108 | path: ~/conda_pkgs_dir
109 | key:
110 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{
111 | hashFiles('dev-environment.yml') }}
112 | - uses: conda-incubator/setup-miniconda@v2
113 | with:
114 | # Create env with dev packages
115 | auto-update-conda: true
116 | python-version: "3.10"
117 | miniforge-version: latest
118 | environment-file: dev-environment.yml
119 | # Activate napari-imagej-dev environment
120 | activate-environment: napari-imagej-dev
121 | auto-activate-base: false
122 | # Use mamba for faster setup
123 | use-mamba: true
124 |
125 | - name: Setup Qt libraries
126 | uses: tlambert03/setup-qt-libs@v1
127 |
128 | - name: Test napari-imagej
129 | uses: GabrielBB/xvfb-action@v1
130 | with:
131 | run: |
132 | conda run -n napari-imagej-dev --no-capture-output python -m pytest -s -p no:faulthandler --color=yes --cov-report=xml --cov=.
133 |
134 | # We could do this in its own action, but we'd have to setup the environment again.
135 | - name: Upload Coverage to Codecov
136 | uses: codecov/codecov-action@v2
137 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 | .napari_cache
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask instance folder
58 | instance/
59 |
60 | # Sphinx documentation
61 | doc/_build/
62 |
63 | # MkDocs documentation
64 | /site/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # Pycharm and VSCode
70 | .idea/
71 | venv/
72 | .vscode/
73 |
74 | # IPython Notebook
75 | .ipynb_checkpoints
76 |
77 | # pyenv
78 | .python-version
79 |
80 | # OS
81 | .DS_Store
82 |
83 | # written by setuptools_scm
84 | **/_version.py
85 |
86 | # Scripts
87 | /scripts/*
88 | !/scripts/examples
89 |
--------------------------------------------------------------------------------
/.napari/DESCRIPTION.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
91 |
92 | The developer has not yet provided a napari-hub specific description.
93 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | # ruff version
4 | rev: v0.6.2
5 | hooks:
6 | # run the linter
7 | - id: ruff
8 | # run the formatter
9 | - id: ruff-format
10 | - repo: https://github.com/abravalheri/validate-pyproject
11 | rev: v0.10.1
12 | hooks:
13 | - id: validate-pyproject
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: "ubuntu-20.04"
5 | tools:
6 | python: "mambaforge-4.10"
7 |
8 | conda:
9 | environment: doc/environment.yml
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022, ImageJ2 developers.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 | include requirements.txt
4 | # Include napari plugin yaml
5 | include src/napari_imagej/napari.yml
6 | # Include resources
7 | recursive-include src/napari_imagej/resources *.*
8 |
9 | recursive-exclude * __pycache__
10 | recursive-exclude * *.py[co]
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | help:
2 | @echo "Available targets:\n\
3 | clean - remove build files and directories\n\
4 | setup - create mamba developer environment\n\
5 | lint - run code formatters and linters\n\
6 | test - run automated test suite\n\
7 | docs - generate documentation site\n\
8 | dist - generate release archives\n\
9 | \n\
10 | Remember to 'mamba activate pyimagej-dev' first!"
11 |
12 | clean:
13 | bin/clean.sh
14 |
15 | setup:
16 | bin/setup.sh
17 |
18 | check:
19 | @bin/check.sh
20 |
21 | lint: check
22 | bin/lint.sh
23 |
24 | test: check
25 | bin/test.sh
26 |
27 | docs: check
28 | cd doc && $(MAKE) html
29 |
30 | dist: check clean
31 | python -m build
32 |
33 | .PHONY: tests
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # napari-imagej
2 |
3 | ### A [napari] plugin for access to [ImageJ2]
4 |
5 | [](https://github.com/imagej/napari-imagej/raw/main/LICENSE)
6 | [](https://pypi.org/project/napari-imagej)
7 | [](https://python.org)
8 | [](https://github.com/imagej/napari-imagej/actions)
9 | [](https://codecov.io/gh/imagej/napari-imagej)
10 | [](https://napari-hub.org/plugins/napari-imagej)
11 |
12 | **napari-imagej** aims to provide access to all [ImageJ2] functionality through the [napari] graphical user interface. It builds on the foundation of [PyImageJ], a project allowing ImageJ2 access from Python.
13 |
14 | **With napari-imagej, you can access:**
15 |
16 | 1. The napari-imagej widget, providing *headless access* to:
17 | * [ImageJ2 Commands] - 100+ image processing algorithms
18 | * [ImageJ Ops] - 500+ *functional* image processing algorithms
19 | * [SciJava Scripts] - migrated from Fiji or ImageJ2, or written yourself!
20 | 2. The ImageJ user interface, providing access to *the entire ImageJ ecosystem* within napari.
21 |
22 | See the [project roadmap](https://github.com/orgs/imagej/projects/2) for future directions.
23 |
24 | ## Getting Started
25 |
26 | Learn more about the project [here](https://napari-imagej.readthedocs.io/en/latest/), or jump straight to [installation](https://napari-imagej.readthedocs.io/en/latest/Install.html)!
27 |
28 | ## Usage
29 |
30 | * [Image Processing with ImageJ Ops](https://napari-imagej.readthedocs.io/en/latest/examples/ops.html)
31 | * [Puncta Segmentation with SciJava Scripts](https://napari-imagej.readthedocs.io/en/latest/examples/scripting.html)
32 |
33 | ## Troubleshooting
34 |
35 | The [FAQ](https://napari-imagej.readthedocs.io/en/latest/Troubleshooting.html) outlines solutions to many common issues.
36 |
37 | For more obscure issues, feel free to reach out on [forum.image.sc](https://forum.image.sc).
38 |
39 | If you've found a bug, please [file an issue]!
40 |
41 | ## Contributing
42 |
43 | We welcome any and all contributions made onto the napari-imagej repository.
44 |
45 | Development discussion occurs on the [Image.sc Zulip chat](https://imagesc.zulipchat.com/#narrow/stream/328100-scyjava).
46 |
47 | For technical details involved with contributing, please see [here](https://napari-imagej.readthedocs.io/en/latest/Development.html)
48 |
49 | ## License
50 |
51 | Distributed under the terms of the [BSD-2] license,
52 | "napari-imagej" is free and open source software.
53 |
54 | ## Citing
55 |
56 | _napari-imagej: ImageJ ecosystem access from napari_, Nature Methods, 2023 Aug 18
57 |
58 | DOI: [10.1038/s41592-023-01990-0](https://doi.org/10.1038/s41592-023-01990-0)
59 |
60 | [Apache Software License 2.0]: https://www.apache.org/licenses/LICENSE-2.0
61 | [black]: https://github.com/psf/black
62 | [BSD-2]: https://opensource.org/licenses/BSD-2-Clause
63 | [Cookiecutter]: https://github.com/audreyr/cookiecutter
64 | [cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin
65 | [conda]: https://docs.conda.io/
66 | [conda-forge]: https://conda-forge.org/
67 | [file an issue]: https://github.com/imagej/napari-imagej/issues
68 | [flake8]: https://flake8.pycqa.org/
69 | [GNU GPL v3.0]: https://www.gnu.org/licenses/gpl-3.0.txt
70 | [GNU LGPL v3.0]: https://www.gnu.org/licenses/lgpl-3.0.txt
71 | [ImageJ2]: https://imagej.net/software/imagej2
72 | [ImageJ2 Commands]: https://github.com/imagej/imagej-plugins-commands
73 | [ImageJ Ops]: https://imagej.net/libs/imagej-ops
74 | [install mamba]: https://mamba.readthedocs.io/en/latest/installation.html
75 | [isort]: https://pycqa.github.io/isort/
76 | [mamba]: https://mamba.readthedocs.io/
77 | [MIT]: https://opensource.org/licenses/MIT
78 | [Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt
79 | [napari]: https://github.com/napari/napari
80 | [napari hub]: https://www.napari-hub.org/
81 | [npe2]: https://github.com/napari/npe2
82 | [pip]: https://pypi.org/project/pip/
83 | [pull request]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
84 | [PyImageJ]: https://github.com/imagej/pyimagej
85 | [PyPI]: https://pypi.org/
86 | [SciJava Scripts]: https://imagej.net/scripting
87 | [tox]: https://tox.readthedocs.io/
88 |
--------------------------------------------------------------------------------
/bin/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | case "$CONDA_PREFIX" in
4 | */napari-imagej-dev)
5 | ;;
6 | *)
7 | echo "Please run 'make setup' and then 'mamba activate napari-imagej-dev' first."
8 | exit 1
9 | ;;
10 | esac
11 |
--------------------------------------------------------------------------------
/bin/clean.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | dir=$(dirname "$0")
4 | cd "$dir/.."
5 |
6 | find . -name __pycache__ -type d | while read d
7 | do rm -rfv "$d"
8 | done
9 | rm -rfv .pytest_cache build dist doc/_build doc/rtd src/*.egg-info
10 |
--------------------------------------------------------------------------------
/bin/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | dir=$(dirname "$0")
4 | cd "$dir/.."
5 |
6 | exitCode=0
7 | ruff check
8 | code=$?; test $code -eq 0 || exitCode=$code
9 | ruff format --check
10 | code=$?; test $code -eq 0 || exitCode=$code
11 | validate-pyproject pyproject.toml
12 | code=$?; test $code -eq 0 || exitCode=$code
13 | exit $exitCode
14 |
--------------------------------------------------------------------------------
/bin/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | dir=$(dirname "$0")
4 | cd "$dir/.."
5 |
6 | mamba env create -f dev-environment.yml
7 |
--------------------------------------------------------------------------------
/bin/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | dir=$(dirname "$0")
4 | cd "$dir/.."
5 |
6 | modes="
7 | | Testing ImageJ2 + original ImageJ |NAPARI_IMAGEJ_INCLUDE_IMAGEJ_LEGACY=TRUE
8 | | Testing ImageJ2 standalone |NAPARI_IMAGEJ_INCLUDE_IMAGEJ_LEGACY=FALSE
9 | | Testing Fiji Is Just ImageJ(2) |NAPARI_IMAGEJ_IMAGEJ_DIRECTORY_OR_ENDPOINT=sc.fiji:fiji:2.15.0
10 | "
11 |
12 | echo "$modes" | while read mode
13 | do
14 | test "$mode" || continue
15 | msg="${mode%|*}|"
16 | expr=${mode##*|}
17 | var=${expr%=*}
18 | value=${expr##*=}
19 | echo "-------------------------------------"
20 | echo "$msg"
21 | echo "-------------------------------------"
22 | export $var="$value"
23 | if [ $# -gt 0 ]
24 | then
25 | python -m pytest -p no:faulthandler $@
26 | else
27 | python -m pytest -p no:faulthandler tests
28 | fi
29 | code=$?
30 | if [ $code -ne 0 ]
31 | then
32 | # HACK: `while read` creates a subshell, which can't modify the parent
33 | # shell's variables. So we save the failure code to a temporary file.
34 | echo $code >exitCode.tmp
35 | fi
36 | unset $var
37 | done
38 | exitCode=0
39 | if [ -f exitCode.tmp ]
40 | then
41 | exitCode=$(cat exitCode.tmp)
42 | rm -f exitCode.tmp
43 | fi
44 | exit $exitCode
45 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imagej/napari-imagej/fb980a8c70d126fbb9bc26f2c10f8c59fdf9bff4/codecov.yml
--------------------------------------------------------------------------------
/dev-environment.yml:
--------------------------------------------------------------------------------
1 | # Use this file to construct an environment
2 | # for developing napari-imagej from source.
3 | #
4 | # First, install mambaforge:
5 | #
6 | # https://github.com/conda-forge/miniforge#mambaforge
7 | #
8 | # Then run:
9 | #
10 | # mamba env create -f dev-environment.yml
11 | # conda activate napari-imagej-dev
12 | #
13 | # In addition to the dependencies needed for using napari-imagej, it includes
14 | # tools for developer-related actions like running automated tests (pytest),
15 | # linting the code (black), and generating the API documentation (sphinx).
16 | # If you want an environment without these tools, use environment.yml.
17 | name: napari-imagej-dev
18 | channels:
19 | - conda-forge
20 | dependencies:
21 | - python >= 3.9, < 3.13
22 | # Project dependencies
23 | - confuse >= 2.0.0
24 | - imglyb >= 2.1.0
25 | - jpype1 >= 1.4.1
26 | - labeling >= 0.1.12
27 | - magicgui >= 0.5.1
28 | - napari >= 0.4.17
29 | - numpy
30 | - openjdk=11
31 | - pandas
32 | - pyimagej >= 1.5.0
33 | - scyjava >= 1.9.1
34 | - superqt >= 0.7.0
35 | - xarray < 2024.10.0
36 | # Version rules to avoid problems
37 | - qtconsole != 5.4.2
38 | - typing_extensions != 4.6.0
39 | # Developer tools
40 | - myst-parser
41 | - pre-commit
42 | - pyqt5-sip
43 | - python-build
44 | - pytest
45 | - pytest-cov
46 | - pytest-env
47 | - pytest-qt
48 | - ruff
49 | - sphinx
50 | - sphinx-copybutton
51 | - sphinx_rtd_theme
52 | - qtpy
53 | # Project from source
54 | - pip
55 | - pip:
56 | - validate-pyproject[all]
57 | - -e '.'
58 |
--------------------------------------------------------------------------------
/doc/Architecture.rst:
--------------------------------------------------------------------------------
1 | napari-imagej Architecture
2 | ==========================
3 |
4 | napari-imagej is divided into a few components, described briefly below:
5 |
6 | The napari-imagej Widget
7 | ------------------------
8 |
9 | The napari-imagej widget provides the ability to access the ImageJ ecosystem from within napari.
10 |
11 | Menu
12 | ####
13 |
14 | The napari-imagej menu provides buttons used to:
15 |
16 | * `Configure <./Configuration.html>`_ the backing ImageJ2 distribution
17 | * Launch the ImageJ UI
18 | * Transfer data between the ImageJ and napari user interfaces
19 |
20 | The usage of these buttons is shown below:
21 |
22 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/menu_usage.png
23 |
24 | The ImageJ button in the napari-imagej toolbar launches the ImageJ user interface. From this interface, any ImageJ ecosystem routine can be executed, including third-party plugins. Data, including multi-channel images, can be passed between the napari and ImageJ interfaces using the transfer buttons, also located in the napari-imagej toolbar.
25 |
26 | Search Bar, Results Tree and Result Runner
27 | ##########################################
28 |
29 | The remaining components of the napari-imagej widget enable the user to execute ImageJ ecosystem functionality *without* launching the ImageJ UI.
30 |
31 | The searchbar is used to identify ecosystem functionality through keywords; napari-imagej will populate the results tree with routines matching the keywords provided in the search bar.
32 |
33 | When the user selects a routine in the results tree, the result runner at the bottom of the widget will provide a set of actions pertaining to the selection. These actions might include:
34 |
35 | * Executing the routine (either through a new window widget, shown below, or through a modal dialog)
36 | * Viewing the routine source code
37 | * Viewing the routine's `wiki page `_
38 |
39 | The usage of these components is shown below:
40 |
41 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/search_usage.png
42 |
43 | Headless ImageJ ecosystem routines are executable directly from the napari interface by typing search terms into the napari-imagej search bar. A napari widget for executing a routine can then be generated by selecting any of the corresponding results shown in the panel beneath.
44 |
45 |
46 |
47 | Type Logic and Type Converters
48 | ------------------------------
49 |
50 | To call ImageJ routines on Python data structures, we require two additional steps when calling any ImageJ routine:
51 |
52 | #. *Before calling the routine* we must convert the provided Python objects into equivalent Java objects.
53 | #. *After calling the routine* we must convert the returned Java objects into equivalent Python objects.
54 |
55 | Without the first step, ImageJ would not understand the user's arguments. Without the second step, napari would not understand the routine's outputs.
56 |
57 | napari-imagej implements both of these steps transparently as shown below, through the use of a type conversion layer. User inputs from Module widgets are converted to Java equivalents before the ImageJ routine is called. Routine outputs are converted to Python equivalents before those outputs are provided back to the user.
58 |
59 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/data_conversion.png
60 |
61 | To convert inputs, the data conversion layer maintains a list of conversion functions :math:`\{P_i:p_i\rightarrow j_i\}` used to convert objects of Python type :math:`p_i` into objects of Java type :math:`j_i`. As an example, one such function might convert napari ``Image`` layers into imglib2 ``Img``\s.
62 |
63 | To convert outputs, the layer maintains a similar set of conversion functions :math:`\{J_i:j_i\rightarrow p_i\}` used to convert objects of java type :math:`j_i` into Python type :math:`p_i`. For example, one such function might convert imglib2 ``Img``\s into napari ``Image`` layers.
64 |
65 | napari-imagej will only allow ImageJ routines to be called if it can map **each** Python input to some function :math:`P_i`, and **each** Java output to some function :math:`J_i`. When those routines are called, the following steps occur:
66 |
67 | #. Each input is passed to its corresponding input converter :math:`P_i`
68 | #. The ImageJ routine is called on the returns of the inputs converters
69 | #. Each routine output is passed to its corresponding output converter :math:`J_i`
70 | #. The returns of the output converters are provided back to the user
71 |
72 | The Backing PyImageJ Instance
73 | -----------------------------
74 |
75 | Without access to a backing `ImageJ2 `_ instance, napari-imagej could not execute ImageJ ecosystem functionality. The `PyImageJ `_, able to provide ImageJ2 access in Python, is napari-imagej's gateway to this functionality.
76 |
77 | By using the settings button in the napari-imagej `menu <#menu>`_, this backing instance can be configured to enable ImageJ, ImageJ2, Fiji, and third-party functionality. Please see the `configuration documentation <./Configuration.html>`_ for more information.
78 |
--------------------------------------------------------------------------------
/doc/Configuration.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Configuration
3 | =============
4 |
5 | This document explains how to augment napari-imagej to configure available functionality.
6 |
7 | We assume familiarity in launching napari-imagej. Please see `this page <./Initialization.html>`_ for more information on launching napari-imagej.
8 |
9 | Accessing napari-imagej Settings
10 | --------------------------------
11 |
12 | As soon as you launch napari-imagej, you can access napari-imagej's configuration dialog by clicking on the gear in the napari-imagej menu:
13 |
14 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_wheel.png
15 |
16 | The configuration dialog is accessed through the gear button on the napari-imagej menu
17 |
18 | Configuring Settings
19 | --------------------
20 |
21 | Within this modal dialog are many different settings, many pertaining to the underlying ImageJ2 instance.
22 |
23 | Note that many of these settings **pertain to the underlying ImageJ2 instance**, requiring a restart of napari to take effect.
24 |
25 | *ImageJ directory or endpoint*
26 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27 |
28 | This setting allows users to provide a string identifying *which Java components* should be used to form the ImageJ2 instance. This line is passed directly to PyImageJ.
29 |
30 | If you pass a **directory**, PyImageJ will look in that directory for *an existing ImageJ2 instance*.
31 |
32 | If you pass one or more **components** in `Maven coordinate `_ form, PyImageJ will launch an ImageJ2 instance from those components, *downloading them if necessary*.
33 |
34 | Here are some example endpoint constructions:
35 |
36 | .. list-table:: endpoint options
37 | :header-rows: 1
38 |
39 | * - To use:
40 | - ``ImageJ directory or endpoint``
41 | - Reproducible?
42 | * - Newest available ImageJ2
43 | - ``net.imagej:imagej``
44 | - NO
45 | * - Specific version of ImageJ2
46 | - ``net.imagej:imagej:2.9.0``
47 | - YES
48 | * - Newest available Fiji
49 | - ``sc.fiji:fiji``
50 | - NO
51 | * - Newest available ImageJ2 PLUS specific plugins
52 | - ``net.imagej:imagej+net.preibisch:BigStitcher``
53 | - NO
54 |
55 | Note that the endpoint can be set programmatically by running the following code before starting napari-imagej:
56 |
57 | .. code-block:: python
58 |
59 | from napari_imagej import settings
60 | settings.imagej_directory_or_endpoint = "sc.fiji:fiji:2.13.0+org.morphonets:SNT:MANAGED"
61 |
62 | *ImageJ base directory*
63 | ^^^^^^^^^^^^^^^^^^^^^^^
64 |
65 | Path to the ImageJ base directory on your local machine. Defaults to the current working directory.
66 |
67 | This directory is most commonly used for discovering SciJava scripts; ImageJ2 will search the provided directory for a `scripts` folder, automatically exposing any scripts within.
68 |
69 | *include original ImageJ features*
70 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
71 |
72 | This button is used to declare whether original ImageJ functionality should be exposed.
73 |
74 | If active, all original ImageJ functionality (ij.* packages) will be available, and the napari-imagej GUI button will launch the classic ImageJ user interface.
75 |
76 | If disabled, only ImageJ2 functionality will be available, and the napari-imagej GUI button will launch the ImageJ2 user interface.
77 |
78 | *enable ImageJ GUI if possible*
79 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 |
81 | This checkbox tells napari-imagej whether to make the ImageJ GUI available. If unchecked, ImageJ2 will be run headlessly, disabling the ImageJ UI and making original ImageJ functionality unavailable.
82 |
83 | By default, the ImageJ GUI will be available whenever possible; however, the ImageJ GUI **is unavailable on macOS**. Therefore, on macOS, napari-imagej will behave as if this setting is always ``False``.
84 |
85 | More details can be found `here `_.
86 |
87 | *JVM command line arguments*
88 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89 |
90 | Used to define command line arguments that should be passed to the JVM at startup.
91 |
92 | One common use case for this feature is to increase the maximum heap space available to the JVM, as shown below:
93 |
94 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/benchmarking_settings.png
95 |
96 | Specifying 32GB of memory available to ImageJ ecosystem routines in the JVM.
97 |
98 |
99 | .. _Fiji: https://imagej.net/software/fiji/
100 | .. _ImageJ2: https://imagej.net/software/imagej2/
101 | .. _napari: https://napari.org
102 |
--------------------------------------------------------------------------------
/doc/Development.rst:
--------------------------------------------------------------------------------
1 | Developer's Guide
2 | =================
3 |
4 | This document describes how to contribute to the napari-imagej source.
5 |
6 | If your goal is only to *use* napari-imagej to call ImageJ ecosystem routines in napari, see `this page <./Install.html>`_.
7 |
8 | Configuring a Mamba environment for development
9 | -----------------------------------------------
10 |
11 | napari-imagej requires Java and Python components, and as such we *highly* recommend contributors use virtual environments
12 | to manage their development environment
13 |
14 | The first step towards development is installing napari-imagej from source. With Mamba_, this is particularly easy.
15 |
16 | .. code-block:: bash
17 |
18 | git clone https://github.com/imagej/napari-imagej
19 | cd napari-imagej
20 | mamba env create -f dev-environment.yml
21 |
22 | This virtual environment must then be activated to work on the napari-imagej source:
23 |
24 | .. code-block:: bash
25 |
26 | mamba activate napari-imagej-dev
27 |
28 | Testing
29 | -------
30 |
31 | napari-imagej uses pytest_ to automate testing. By installing the developement environment above, ``pytest`` will come installed.
32 |
33 | To test napari-imagej, simply run:
34 |
35 | .. code-block:: bash
36 |
37 | pytest
38 |
39 | Documentation
40 | -------------
41 |
42 | napari-imagej uses `Read the Docs`_ for user-facing documentation. If you make front-end changes to napari-imagej, please describe your changes with the files in the ``doc`` folder of the napari-imagej source.
43 |
44 | Once you've made your changes, run the following:
45 |
46 | .. code-block:: bash
47 |
48 | make docs
49 |
50 | This will build the documentation into HTML files locally, stored in the ``doc/_build/html`` folder. You can then view the documentation locally by loading ``doc/_build/html/index.html`` in the browser of your choice.
51 |
52 | Production documentation is available online at https://napari-imagej.readthedocs.io/
53 |
54 | Formatting
55 | ----------
56 |
57 | black_, flake8_, and isort_ are used to lint and standardize the napari-imagej source.
58 |
59 | To manually format the source, run (macOS/Linux):
60 |
61 | .. code-block:: bash
62 |
63 | make clean
64 |
65 | napari-imagej also includes pre-commit_ configuration for those who want it. By using pre-commit, staged changes will be formatted before they can be commited to a repository. pre-commit can be set up using:
66 |
67 | .. code-block:: bash
68 |
69 | pre-commit install
70 |
71 | Building Distribution Bundles
72 | -----------------------------
73 |
74 | You can run the following to bundle napari-imagej (macOS/Linux):
75 |
76 | .. code-block:: bash
77 |
78 | make dist
79 |
80 | .. _black: https://black.readthedocs.io/en/stable/
81 | .. _flake8: https://flake8.pycqa.org/en/latest/
82 | .. _isort: https://pycqa.github.io/isort/
83 | .. _Mamba: https://mamba.readthedocs.io
84 | .. _Read the Docs: https://readthedocs.org/
85 | .. _pre-commit: https://pre-commit.com/
86 | .. _pytest: https://docs.pytest.org
87 |
--------------------------------------------------------------------------------
/doc/Initialization.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | Initialization
3 | ==============
4 |
5 | This document assumes familiarity with napari_.
6 |
7 | Starting napari-imagej
8 | ----------------------
9 |
10 | With napari running, napari-imagej can be found through ``Plugins->ImageJ2 (napari-imagej)``. If you don't see this menu option, return to the
11 | `installation guide <./Install.html>`_ and ensure you are launching napari from a python environment with the napari-imagej plugin installed. If you're still having trouble, please see the `troubleshooting section <./Troubleshooting.html#napari-imagej-does-not-appear-in-the-plugins-menu-of-napari>`__.
12 |
13 | Once triggered, napari-imagej will start up the JVM, and then the ImageJ2 gateway. This setup can take a few seconds, and is complete when the napari-imagej searchbar is cleared and enabled.
14 |
15 | **On the first initialization, napari-imagej must download an ImageJ2 distribution. This download can take minutes, depdending on the user's bandwidth.**
16 |
17 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/startup.gif
18 |
19 | Once napari-imagej is fully initilized, you can see the `Use Cases <./Use_Cases.html>`_ page for examples of available functionality. Alternatively, if you're new to ImageJ, you may want to start with a `high-level overview `_.
20 |
21 | **Note**: napari-imagej always downloads the latest version of ImageJ2_, along with classic ImageJ functionality. To launch a *different* ImageJ2 distribution, such as Fiji_, please see the `Configuration <./Configuration.html>`_ page
22 |
23 | Starting the ImageJ GUI
24 | -----------------------
25 |
26 | While all ImageJ2 functionality should be accessible direclty through the napari-imagej widget, many original ImageJ functions require the ImageJ graphical user interface (GUI) to be visible.
27 |
28 | If you try to run one of these commands through the napari-imagej search bar you will receive a message indicating the GUI is required, with an option to show it. Alternatively, at any point you can launch the ImageJ GUI via the GUI button in the napari-imagej menu.
29 |
30 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_gui_button.png
31 |
32 | The GUI is launched through the ImageJ button on the napari-imagej menu
33 |
34 | If your ImageJ GUI button is greyed out, see the `troubleshooting section <./Troubleshooting.html#the-imagej2-gui-button-is-greyed-out>`__.
35 |
36 | .. _Fiji: https://imagej.net/software/fiji/
37 | .. _ImageJ2: https://imagej.net/software/imagej2/
38 | .. _napari: https://napari.org
39 |
--------------------------------------------------------------------------------
/doc/Install.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Installation
3 | ============
4 |
5 | If you're looking to use napari-imagej, there are a few ways to get it running.
6 |
7 | Installing within napari
8 | ========================
9 |
10 | If you have napari installed already, you can install napari-imagej by following these steps:
11 |
12 | #. Install OpenJDK 8 or OpenJDK 11
13 |
14 | napari-imagej should work with whichever distribution of OpenJDK you prefer; we recommend `zulu jdk+fx 8 `_. You can also install OpenJDK from your platform's package manager.
15 |
16 | #. Install Maven
17 |
18 | You can either `download it manually `_ or install it via your platform's package manager. The ``mvn --version`` command can be used to verify installation.
19 |
20 | #. Install napari-imagej with napari
21 |
22 | With napari running, `navigate `_ to the plugins menu ``Plugins>Install/Uninstall Plugins`` and type into the search bar ``napari-imagej``. Click ``Install`` to install napari-imagej!
23 |
24 | Once the installation is complete, napari-imagej is ready to use!
25 |
26 | Installing from Mamba (Recommended)
27 | ===================================
28 |
29 | Mamba_ is the easiest way to install napari-imagej. To install Mamba, follow the instructions `here `_.
30 |
31 | #. Create a Mamba environment:
32 |
33 | .. code-block:: bash
34 |
35 | mamba create -n napari-imagej napari-imagej openjdk=8
36 |
37 | This command installs napari-imagej, napari, and OpenJDK8 into a new Mamba environment, named ``napari-imagej``.
38 |
39 | #. Activate the Mamba environment:
40 |
41 | This step must be done every time that you'd like to use napari-imagej:
42 |
43 | .. code-block:: bash
44 |
45 | mamba activate napari-imagej
46 |
47 | Installing from pip
48 | ===================
49 |
50 | napari-imagej can also be installed using ``pip``; however, it requires more steps. You'll need Python3_ if you don't have it already.
51 |
52 | We recommend using virtualenv_ to isolate napari-imagej from your system-wide or user-wide Python environments. Alternatively, you can use Mamba_ purely for its virtual environment capabilities, and then ``pip install`` everything into that environment:
53 |
54 | .. code-block:: bash
55 |
56 | mamba create -n napari-imagej python pip
57 | mamba activate napari-imagej
58 |
59 | #. Install OpenJDK 8 or OpenJDK 11
60 |
61 | napari-imagej should work with whichever distribution of OpenJDK you prefer; we recommend `zulu jdk+fx 8 `_. You can also install OpenJDK from your platform's package manager.
62 |
63 | #. Install Maven
64 |
65 | You can either `download it manually `_ or install it via your platform's package manager. The ``mvn --version`` command can be used to verify installation.
66 |
67 | #. Install napari-imagej
68 |
69 | Using pip, we can install napari-imagej:
70 |
71 | .. code-block:: bash
72 |
73 | pip install napari-imagej
74 |
75 |
76 | Installing from Source
77 | ======================
78 |
79 | If you're looking to develop napari-imagej, you'll likely want to install from source.
80 |
81 | Using Mamba
82 | -----------
83 |
84 | Mamba_ is the easiest way to install napari-imagej. To install Mamba, follow the instructions `here `_.
85 |
86 | #. Clone the napari-imagej repository
87 |
88 | From a suitable location, use the following command to clone the napari-imagej repository:
89 |
90 | .. code-block:: bash
91 |
92 | git clone https://github.com/imagej/napari-imagej
93 | cd napari-imagej
94 |
95 | #. Install napari-imagej
96 |
97 | The following line will download all necessary components to run napari-imagej, installing them into a mamba environment named ``napari-imagej``.
98 |
99 | .. code-block:: bash
100 |
101 | mamba env create
102 |
103 | Using pip
104 | ---------
105 | napari-imagej can also be installed using ``pip``; however, it requires more steps. You'll need Python3_ if you don't have it already.
106 |
107 | We recommend using virtualenv_ to isolate napari-imagej from your system-wide or user-wide Python environments. Alternatively, you can use Mamba_ purely for its virtual environment capabilities, and then ``pip install`` everything into that environment:
108 |
109 | .. code-block:: bash
110 |
111 | mamba create -n napari-imagej python pip
112 | mamba activate napari-imagej
113 |
114 | #. Install OpenJDK 8 or OpenJDK 11
115 |
116 | napari-imagej should work with whichever distribution of OpenJDK you prefer; we recommend `zulu jdk+fx 8 `_. You can also install OpenJDK from your platform's package manager.
117 |
118 | #. Install Maven
119 |
120 | You can either `download it manually `_ or install it via your platform's package manager. The ``mvn --version`` command can be used to verify installation.
121 |
122 | #. Install napari-imagej
123 |
124 | The following code section will **clone the napari-imagej source into a subfolder of the local directory** and install all Python components necessary for napari-imagej.
125 |
126 | .. code-block:: bash
127 |
128 | git clone https://github.com/imagej/napari-imagej
129 | cd napari-imagej
130 | pip install .
131 |
132 | .. _Mamba: https://mamba.readthedocs.io/en/latest/
133 | .. _napari_imagej: https://github.com/imagej/napari-imagej
134 | .. _Python3: https://www.python.org/
135 | .. _virtualenv: https://virtualenv.pypa.io/en/latest/
136 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # 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 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | # napari-imagej Reference Documentation
2 |
3 | To access napari-imagej's installation, initialization, tutorial, troubleshooting and API reference documentation please visit the [napari-imagej ReadTheDocs](https://napari-imagej.readthedocs.io/en/latest/index.html) website or build the documentation locally (see [building the reference documentation](https://napari-imagej.readthedocs.io/en/latest/Development.html#building-the-reference-documentation) for more details).
4 |
--------------------------------------------------------------------------------
/doc/Troubleshooting.rst:
--------------------------------------------------------------------------------
1 | Troubleshooting
2 | ===============
3 |
4 |
5 | napari-imagej Does Not Appear in the Plugins Menu of napari!
6 | ------------------------------------------------------------
7 |
8 | npe2_ is a useful tool for validating a napari plugin's setup. When running napari-imagej within Mamba_, use:
9 |
10 | .. code-block:: bash
11 |
12 | mamba activate napari-imagej
13 | mamba install npe2 -c conda-forge
14 | npe2 validate napari-imagej
15 |
16 | If ``npe2 validate`` returns an error, this indicates that napari-imagej was not installed correctly. In this case, please ensure that you have followed `Installation <./Install.html>`_ instructions.
17 |
18 | The Search Bar Is Disabled with the Message "Initializing ImageJ..."
19 | --------------------------------------------------------------------
20 |
21 | Since napari-imagej is calling Java code under the hood, it must launch a Java Virtual Machine (JVM). The JVM is not launched until the user starts napari-imagej. As we cannot search Java functionality *until the JVM is running*, the search bar is not enabled until the JVM is ready.
22 |
23 | The first launch of napari-imagej can take significantly longer than subsequent launches while the underlying framework downloads the Java artifacts needed to run ImageJ2. **Downloading these libraries can take minutes**. These libraries are cached, however, so subsequent launches should not take more than a few seconds.
24 |
25 | The ImageJ2 GUI Button Is Greyed Out!
26 | -------------------------------------
27 |
28 | There are two common cases for a disabled ImageJ2 GUI button:
29 |
30 | #. When napari-imagej is first launched, the button will be disabled until the ImageJ2 Gateway is ready to process data. Please see `here <#the-search-bar-is-disabled-with-the-message-initializing-imagej>`_.
31 |
32 | #. On some systems (namely **macOS**), PyImageJ can **only** be run headlessly. In headless PyImageJ environments, the ImageJ2 GUI cannot be launched. Please see `this page `_ for more information.
33 |
34 | .. _Mamba: https://mamba.readthedocs.io/en/latest/
35 | .. _npe2: https://github.com/napari/npe2
36 |
37 | The Image Dimension Labels Are Wrong in ImageJ after Transferring from napari
38 | -----------------------------------------------------------------------------
39 |
40 | Internally, napari does not utilize image dimension labels (*i.e.* ``X``, ``Y``, ``Channel``, *etc...*) and instead assumes that the *n*-dimensional arrays (*i.e* images) conform to the `scikit-image dimension order`_ convention. ImageJ2 however *does* care about dimension labels and uses them to define certain operations.
41 |
42 | For example, if you open the sample `live cell wide-field microscopy data of dividing HeLa cell nuclei `_ (which has the dimension order ``(X, Y, Time)`` in ImageJ's convention) in napari, transfer the data over to ImageJ2 with the napari-imagej transfer button and examine the properties of the image you will find that ImageJ2 has confused the ``Time`` dimension for ``Channel``. ImageJ2 thinks the transferred data has 40 channels instead of 40 frames.
43 |
44 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_adjust_props.png
45 |
46 | The reason this happens is because ImageJ2 is not given dimension labels when data is transferred from napari. When ImageJ2 has no dimension label information for a given image then the ``(X, Y, Channel, Z, Time)`` dimension order and labels are applied to the image. In this example, the ``X`` and ``Y`` dimension labels are set properly, but the last dimension (which we know should be ``Time``) is set to ``Channel``. Note that this also means if your napari image has a shape that does conform to the scikit-image dimension order ``(t, pln, row, col, ch)`` it is possible that transferred images could be transposed into unintended orthogonal views of the data.
47 |
48 | To fix the dimension labels on your image data open the image's properties and assign the correct dimension value to the appropriate field. In this example we want to assign ``Channel (c)`` the value 1 (there is only 1 channel) and ``Frames (t)`` the value 40 (there are 40 frames in the dataset). You can also set the unit type (*e.g* micron, pixel, etc...) and size for the image in the image properties.
49 |
50 | .. _scikit-image dimension order: https://scikit-image.org/docs/stable/user_guide/numpy_images.html#a-note-on-the-time-dimension
51 |
--------------------------------------------------------------------------------
/doc/Usage.rst:
--------------------------------------------------------------------------------
1 | Basic Usage
2 | ===========
3 |
4 | napari-imagej offers two different mechanisms for accessing napari-imagej functionality - both are described below:
5 |
6 | The napari-imagej widget
7 | ------------------------
8 |
9 | The napari-imagej widget provides headless access to all ImageJ2 functionality, and all third-party plugins written in the ImageJ2 framework. These plugins can be found and run with the napari-imagej searchbar, as shown in the figure below:
10 |
11 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/gauss_search.png
12 |
13 | The napari-imagej widget, used to identify ImageJ functionality matching the search term "gauss"
14 |
15 | By clicking on an item in the search results, a set of actions is displayed at the bottom of the widget. To execute the selected functionality, users can click the ``Run`` button to launch a modal dialog, or they can click the ``Widget`` buton to launch a new napari widget (as shown below). Either button will allow the user to provide inputs to the ImageJ2 routine, and once the user confirms the selections the outputs of the routine will appear within the napari application.
16 |
17 |
18 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/gauss_widget.png
19 |
20 | By clicking on the ``Widget`` button, a new napari widget is added to prompt the user for ImageJ2 routine input.
21 |
22 | .. |ImageJ2| image:: ../src/napari_imagej/resources/imagej2-16x16-flat.png
23 |
24 | .. NB: The svgs must have a fixed width to appear nicely inline
25 |
26 | .. |import| image:: ../src/napari_imagej/resources/import.svg
27 | :width: 1.5em
28 | :height: 1.5em
29 | :class: no-scaled-link
30 | .. |export| image:: ../src/napari_imagej/resources/export.svg
31 | :width: 1.5em
32 | :height: 1.5em
33 | :class: no-scaled-link
34 | .. |advanced export| image:: ../src/napari_imagej/resources/export_detailed.svg
35 | :width: 1.5em
36 | :height: 1.5em
37 | :class: no-scaled-link
38 |
39 |
40 | Using the ImageJ2 UI
41 | --------------------
42 |
43 | Many ImageJ ecosystem routines cannot be used headlessly - for this reason, napari-imagej also exposes the ImageJ UI to allow their execution within napari.
44 |
45 | To launch the ImageJ UI, press the |ImageJ2| button in the napari-imagej menu. Once the ImageJ UI is visible, ImageJ can be used as normal (to learn more, see `the ImageJ2 documentation `__).
46 |
47 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_gui_button.png
48 |
49 | The GUI is launched through the ImageJ button on the napari-imagej menu
50 |
51 | Transferring images
52 | ^^^^^^^^^^^^^^^^^^^
53 |
54 | To run ImageJ functionality through the ImageJ UI, users must export their data to the ImageJ UI. This can be accomplished on basic images using the |export| button, which transfers the **active** napari ``Layer`` to the ImageJ UI. Additionally, users can press the |advanced export| button to provide additional details towards the transfer, including:
55 |
56 | * An additional ``Points`` or ``Shapes`` ``Layer`` to be linked as ROIs
57 | * Dimension labels for each axis of the image ``Layer``
58 |
59 | Note that these buttons are only enabled when there is a ``Layer`` that can be transferred.
60 |
61 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/export_detailed_dialog.png
62 |
63 | Using the |advanced export| button, users can provide metadata for richer data transfer to the ImageJ UI
64 |
65 | The |import| button can be used to transfer the **active** ImageJ window back into the napari application.
66 |
67 | Using the SciJava REPL
68 | --------------------------------
69 |
70 | You can use the SciJava REPL to interactively run SciJava code. This makes it possible to do things like paste existing SciJava scripts into the REPL. More information on scripting in SciJava can be found `here `_.
71 |
72 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/script_repl.png
73 |
74 | The REPL can be shown/hidden by clicking on the command prompt icon.
75 |
76 |
--------------------------------------------------------------------------------
/doc/Use_Cases.rst:
--------------------------------------------------------------------------------
1 | Use Cases
2 | =========
3 |
4 | Below we document use cases for the napari-imagej plugin.
5 |
6 | .. toctree::
7 | :maxdepth: 3
8 |
9 | examples/trackmate.rst
10 |
11 | examples/trackmate_reader.rst
12 |
13 | examples/bonej.rst
14 |
15 | examples/scripting.rst
16 |
17 | examples/ops.rst
18 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 | import os
9 | import sys
10 |
11 | sys.path.insert(0, os.path.abspath("../src/napari-imagej"))
12 |
13 | project = "napari-imagej"
14 | copyright = "2023, ImageJ2 developers"
15 | author = "ImageJ2 developers"
16 |
17 | # -- General configuration ---------------------------------------------------
18 |
19 | # Add any Sphinx extension module names here, as strings. They can be
20 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
21 | # ones.
22 | extensions = [
23 | "myst_parser",
24 | "sphinx.ext.autodoc",
25 | "sphinx.ext.viewcode",
26 | "sphinx_copybutton",
27 | ]
28 |
29 | templates_path = ["_templates"]
30 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.md"]
31 |
32 |
33 | # -- Options for HTML output -------------------------------------------------
34 |
35 | # Always show the Edit on GitHub buttons
36 | # Set the correct path for Edit on GitHub
37 | html_context = {
38 | 'display_github': True,
39 | 'github_user': 'imagej',
40 | 'github_repo': 'napari-imagej',
41 | 'github_version': 'main/doc/',
42 | }
43 |
44 |
45 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
46 | html_theme = "sphinx_rtd_theme"
47 |
--------------------------------------------------------------------------------
/doc/environment.yml:
--------------------------------------------------------------------------------
1 | name: napari-imagej-dev
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | # python
6 | - python=3.10
7 | # dependencies
8 | - myst-parser
9 | - sphinx-copybutton
10 | - sphinx_rtd_theme
11 | # pip
12 | - pip
13 | - pip:
14 | - readthedocs-sphinx-search
15 |
--------------------------------------------------------------------------------
/doc/examples/ops.rst:
--------------------------------------------------------------------------------
1 | Image Processing with ImageJ Ops (headless)
2 | ===========================================
3 |
4 | The `ImageJ Ops`_ project contains hundreds of algorithms for module image processing, and is shipped with every ImageJ2 installation. This document explains how to use ImageJ Ops from the napari interface.
5 |
6 | .. important::
7 |
8 | This Use Case was run with the following Mamba environment::
9 |
10 | mamba env create -n ex-ops -y -c conda-forge python=3.11 openjdk=11.0 napari=0.5.0 napari-imagej=0.2.0
11 |
12 | and napari-imagej was configured to use the following endpoint::
13 |
14 | net.imagej:imagej:2.15.0
15 |
16 | Ops, explained
17 | --------------
18 |
19 | Ops are reusable, stateless algorithms, having:
20 |
21 | * A name, describing the algorithm that the Op follows. Example names include ``math.add``, and ``filter.gauss``.
22 |
23 | * A functional type. Most Ops fall into one of two types:
24 |
25 | * ``Function``\s store output in a new napari ``Layer`` upon every execution, maximizing convenience.
26 |
27 | * ``Computer``\s store computational results in a storage buffer, usually an existing napari ``Layer`` *passed by the user*. Making use of shared memory for zero-copy data transfer, ``Computer``\s maximize speed.
28 |
29 | * A set of input parameters. Optional parameters can be identified by a trailing ``?``
30 |
31 | * A set of return types. Some Ops can run as either a ``Function`` or as a ``Computer`` passing the tradeoff of convenience and speed to the user; these Ops append a trailing ``?`` to their outputs.
32 |
33 | A simple gaussian Blur
34 | ----------------------
35 |
36 | A `gaussian blur `_ is easily performed in ImageJ Ops.
37 |
38 | Ops are searchable directly from the napari-imagej search bar. Therefore, looking for a gaussian blur is as simple as typing ``gauss`` into the search bar:
39 |
40 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/gauss_search.png
41 |
42 | From this version of ImageJ Ops, we can see three different Gaussian Blur Ops. Let's look at the first one:
43 |
44 | ``filter.gauss(img "out"?, img "in", number "sigma", outOfBoundsFactory "outOfBounds"?) -> (img "out"?)``
45 |
46 | This filter takes the following parameters:
47 |
48 | * An *optional* image ``out``. Since the output is optional, napari-imagej will allow us to leave this parameter empty. If we do, the output will instead be placed into a new napari ``Layer``
49 | * An image ``in`` - the input data to the Gaussian Blur
50 | * A number ``sigma`` - defines the sigma parameter to the gaussian function
51 | * An *optional* ``OutOfBoundsFactory`` ``outOfBounds`` - defines the values used in computation when outside of the input data interval.
52 |
53 | This filter returns the following objects:
54 |
55 | * An image ``out`` *if the user did not provide a pre-allocated output*.
56 |
57 | We can run this Op by clicking on it, and then selecting one of the following buttons:
58 |
59 | * ``Run`` produces a *modal dialog* that will disappear once the user provides all inputs.
60 | * ``Widget`` produces a *new napari widget* that will persist until closed.
61 |
62 | Below we see the effect of pressing the ``Widget`` button:
63 |
64 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/gauss_widget.png
65 |
66 | With this widget, we only need to enter in our inputs, and then press the run button. Note that napari will allow users to omit any parameters with a ``-----``.
67 |
68 | Below, we run this Op on a focal plane of the `EmbryoCE `_ image from https://samples.scif.io:
69 |
70 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/gauss_op.gif
71 |
72 | .. _ImageJ Ops: https://imagej.net/libs/imagej-ops/index
73 |
74 |
--------------------------------------------------------------------------------
/doc/examples/scripting.rst:
--------------------------------------------------------------------------------
1 | Puncta Segmentation with SciJava Scripts (headless)
2 | ===================================================
3 |
4 | Using `SciJava Scripts`_ we can automate the execution of sequential calls to ImageJ ecosystem functionality. Scripts written in any of SciJava's `supported scripting languages `_ will be automatically discovered and searchable within napari-imagej, just like other commands.
5 |
6 | *Notably*, all SciJava Scripts can be run headlessly; since SciJava Scripts can headlessly call classic ImageJ functionality, **SciJava Scripts allow running classic ImageJ functionality without the ImageJ GUI**.
7 |
8 | For this example, we translated PyImageJ's `Puncta Segmentation`_ into a SciJava Script. This SciJava Script can be executed in napari-imagej **or** in ImageJ2, increasing portability!
9 |
10 | For more information on the use case itself, please see the original PyImageJ Puncta Segmentation example.
11 |
12 | .. important::
13 |
14 | This Use Case was run with the following Mamba environment::
15 |
16 | mamba env create -n ex-segment -y -c conda-forge python=3.11 openjdk=11.0 napari=0.5.0 napari-imagej=0.2.0
17 |
18 | and napari-imagej was configured to use the following endpoint::
19 |
20 | net.imagej:imagej:2.15.0
21 |
22 | Configuration
23 | -------------
24 |
25 | To run this use case, the following settings were used. For information on configuring napari-imagej, please see `here <../Configuration.html>`__.
26 |
27 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_imagej_2.15.0.png
28 |
29 | Configuration for the Puncta Segmentation use case
30 |
31 | The Code
32 | --------
33 |
34 | The script for Puncta Segmentation is written below:
35 |
36 | .. code-block:: python
37 | :caption: Puncta_Segmentation.py
38 |
39 | #@ Img ds_src
40 | #@ ConvertService convert
41 | #@ DatasetService ds
42 | #@ OpService ops
43 | #@output org.scijava.table.Table sci_table
44 |
45 | from ij import IJ, ImagePlus, Prefs
46 | from ij.measure import ResultsTable
47 | from ij.plugin.filter import ParticleAnalyzer
48 | from net.imglib2.algorithm.neighborhood import HyperSphereShape
49 | from net.imglib2.img.display.imagej import ImageJFunctions
50 | from org.scijava.table import Table
51 |
52 | # save the ImageJ settings since we need to ensure black background is checked
53 | blackBackground = Prefs.blackBackground
54 |
55 | # if using a dataset with a light background and dark data, you can comment this line out
56 | # otherwise, the background will be measured instead of the points
57 | Prefs.blackBackground = True
58 |
59 | # convert image to 32-bit
60 | ds_src = ops.convert().int32(ds_src)
61 |
62 | # supress background noise
63 | mean_radius = HyperSphereShape(5)
64 | ds_mean = ds.create(ds_src.copy())
65 | ops.filter().mean(ds_mean, ds_src.copy(), mean_radius)
66 | ds_mul = ops.math().multiply(ds_src, ds_mean)
67 |
68 | # use gaussian subtraction to enhance puncta
69 | img_blur = ops.filter().gauss(ds_mul.copy(), 1.2)
70 | img_enhanced = ops.math().subtract(ds_mul, img_blur)
71 |
72 | # apply threshold
73 | img_thres = ops.threshold().renyiEntropy(img_enhanced)
74 |
75 | # convert ImgPlus to ImagePlus
76 | impThresholded=ImageJFunctions.wrap(img_thres, "wrapped")
77 |
78 | # get ResultsTable and set ParticleAnalyzer
79 | rt = ResultsTable.getResultsTable()
80 | ParticleAnalyzer.setResultsTable(rt)
81 |
82 | # set measurements
83 | IJ.run("Set Measurements...", "area center shape")
84 |
85 | # run the analyze particle plugin
86 | IJ.run(impThresholded, "Analyze Particles...", "clear");
87 |
88 | # convert results table -> scijava table -> pandas dataframe
89 | sci_table = convert.convert(rt, Table)
90 |
91 | # restore the settings to their original values
92 | Prefs.blackBackground = blackBackground
93 |
94 |
95 | Note that the code is mostly the same, with the following exceptions:
96 |
97 | #. Calls to ImageJ Services using the ImageJ Gateway (e.g. ``ij.convert``) are replaced with Scripting Parameters (e.g. ``#@ ConvertService convert``)
98 | #. Java Classes are imported using the ``from...import`` syntax, instead of using ``sj.jimport``.
99 | #. Calls to ``ij.py.show`` are removed - automating the process means we don't want to see these.
100 | #. The output is a ``org.scijava.table.Table``, **not** a pandas ``DataFrame``. We don't need to perform this conversion in napari-imagej; napari-imagej takes care of that for us!
101 |
102 | Installing the script
103 | ---------------------
104 |
105 | Copy the code block above and paste it into a new file called ``Puncta_Segmentation.py``. As for where to put that file, the rules for `adding SciJava Scripts to ImageJ2 `_ also apply when adding scripts to napari-imagej if you are using a local ImageJ2 (e.g. a subdirectory of ``Fiji.app/scripts/``).
106 |
107 | **However**, when napari-imagej is *not* provided with a local ImageJ2 instance, it must `download one <../Configuration.html#imagej-directory-or-endpoint>`_. This ImageJ2 can be tucked away, so napari-imagej will *by default* look within the **ImageJ base directory** for a ``scripts`` subdirectory, which must then have further subdirectories that contain your scripts. This behavior can be controlled via the `ImageJ base directory <../Configuration.html#imagej-base-directory>`_ in napari-imagej's settings.
108 |
109 | *Without changing this setting*, placing ``Puncta_Segmentation.py`` in a subdirectory of ``/scripts`` allows napari-imagej to discover the script.
110 |
111 | *If the ImageJ base directory has been changed*, instead place the script in a subdirectory of ``/scripts``.
112 |
113 |
114 | Running the script
115 | ------------------
116 |
117 | **Note**: this example was tested running with a `ImageJ directory or endpoint <../Configuration.html#imagej-directory-or-endpoint>`_ of ``sc.fiji:fiji:2.13.1``.
118 |
119 | With napari-imagej running, the first step is to open the input data. We'll download the same sample data as the original PyImageJ example, `available here `_.
120 |
121 | The second step is to find our script within napari-imagej. Discovered SciJava Scripts can be found under their `filename `_; so we search for "puncta segmentation"
122 |
123 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/puncta_search.png
124 |
125 | ``Puncta_Segmentation.py`` exposed within the napari-imagej searchbar as ``Puncta Segmentation``.
126 |
127 | Double-clicking on ``PunctaSegmentation`` will bring a modal dialog, prompting the user for input data. The dialog also offers to display the resulting table in a new window, which may be preferred for large result tables.
128 |
129 | Once the "OK" button is clicked, the resuling table is displayed in a new window, or a new napari widget, based on the option you selected above:
130 |
131 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/puncta_results.png
132 |
133 | .. _Puncta Segmentation: https://pyimagej.readthedocs.io/en/latest/Puncta-Segmentation.html
134 | .. _SciJava Scripts: https://imagej.net/scripting/
135 |
--------------------------------------------------------------------------------
/doc/examples/trackmate.rst:
--------------------------------------------------------------------------------
1 | Tracking HeLa cell nuclei with TrackMate
2 | ========================================
3 |
4 | The `TrackMate`_ plugin for ImageJ2 provides a streamlined interface for object tracking.
5 | This use case demonstrates how you can use napari-imagej to run the TrackMate plugin on 3D data **(X, Y, Time)**, view the results with napari, and also process them with the `napari-stracking`_ plugin.
6 |
7 | For this use case we will analyze `live cell wide-field microscopy data of HeLa cell nuclei `_ (Hoechst stain), imaged every 30 minutes for 40 frames.
8 |
9 | .. image:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_0.gif
10 | :align: center
11 |
12 | |
13 |
14 | .. important::
15 |
16 | This Use Case was run with the following Mamba environment::
17 |
18 | mamba env create -n ex-tracking -y -c conda-forge python=3.11 openjdk=11.0 napari=0.5.0 napari-imagej=0.2.0 napari-stracking=0.1.9
19 |
20 | and napari-imagej was configured to use the following endpoint::
21 |
22 | sc.fiji:fiji:2.15.0
23 |
24 | TrackMate Setup
25 | ---------------
26 |
27 | By default, napari-imagej does not include TrackMate. To use the TrackMate plugin, we must first configure napari-imagej to enable TrackMate access.
28 |
29 | We can configure napari-imagej to use a `Fiji`_ installation as follows:
30 |
31 | .. |ImageJ2| image:: ../../src/napari_imagej/resources/imagej2-16x16-flat.png
32 |
33 | 1. Activate the napari-imagej plugin by selecting the ``ImageJ2`` plugin from the Plugins menu.
34 |
35 | 2. Open the settings dialog by clicking the rightmost toolbar button, the gear symbol.
36 |
37 | 3. Change the ``ImageJ directory or endpoint`` (described `here <../Configuration.html#imagej-directory-or-endpoint>`_) to include Fiji, which bundles many popular plugins including TrackMate. Change this setting to the napari-imagej endpoint listed above.
38 |
39 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_fiji.png
40 |
41 | 4. **Restart napari** for the changes to take effect.
42 |
43 | 5. Activate the napari-imagej plugin again, as described in step (1) above.
44 |
45 | 6. If you wish, you may verify that Fiji is enabled by pasting the following code into napari's IPython console:
46 |
47 | .. code-block:: python
48 |
49 | from napari_imagej.java import _ij as ij
50 | ij.app().getApps().keys()
51 |
52 | And if ``Fiji`` is in the list, you're good!
53 |
54 | Preparing the Data
55 | ------------------
56 |
57 | To run TrackMate, we first need data.
58 |
59 | Download the `trackmate_example_data.tif`_ sample dataset.
60 |
61 | TrackMate will only work on image data that are open in the ImageJ UI. Press |ImageJ2| in the napari-imagej menu to launch the ImageJ UI, and then locate the ``File>Open...`` menu option to open the image.
62 |
63 | Running TrackMate
64 | -----------------
65 |
66 | With the data open in the ImageJ UI, start TrackMate by selecting ``Plugins>Tracking>TrackMate`` **from the ImageJ UI**.
67 |
68 | TrackMate opens as a wizard to guide you through the tracking process. Steps are advanced with the ``Next`` button, and you can always go back and adjust parameters to tune your tracks. For this example we actually do not need to execute all steps of the wizard - just those up through selecting and applying a tracker.
69 | Using the sample data, we used the following properties of these wizard steps:
70 |
71 | - **Target image: trackmate_example_data**
72 | We don't need any changes here, but ensure you have the correct image selected - you should see ``Target image: trackmate_example_data`` at the top.
73 | - **Select a detector**
74 | Select ``LoG detector`` from the dropdown menu.
75 | - **LoG detector**
76 | Choose the following options:
77 |
78 | - *Estimated object diameter*: 17 micron
79 | - *Quality threshold*: 0
80 | - *Pre-process with media filter*: Yes (checked)
81 | - *Sub-pixel localization*: Yes (checked)
82 |
83 | You can use the ``Preview`` button as a heuristic to ensure that the cells are adequately detected. The preview should be equivalent to the following screenshot:
84 |
85 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_detector_preview.png
86 |
87 | - **Detection**
88 | Wait for the progress bar at the top of this step to complete, and then press ``Next``.
89 | - **Initial thresholding**
90 | Ensure all spots are preserved. You should see ``Selected spots: 1496 out of 1496`` below the bar chart. If not all spots are selected, click and drag left while on the bar chart to ensure the blue threshold covers all bars.
91 | - **Set filters on spots**
92 | We don't need any changes here.
93 | - **Select a tracker**
94 | Select ``Simple LAP tracker`` from the dropdown.
95 | - **Simple LAP tracker**
96 | Choose the following options:
97 |
98 | - *Linking max distance*: 8.3 micron
99 | - *Gap-closing max distance*: 5.0 micron
100 | - *Gap-closing max frame gap*: 2
101 |
102 | Once the spots and tracks have been generated, you can return to napari and use the *left napari-imagej transfer button*, highlighted below, to transfer the image data and the tracks back to napari.
103 |
104 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_tracks_imported.png
105 |
106 | Transferring TrackMate results back to napari converts TrackMate's tracks into napari tracks and TrackMate's spots/detections into napari labels.
107 |
108 | Processing tracks with napari-stracking
109 | ---------------------------------------
110 |
111 | While the `napari-stracking`_ plugin is capable of performing its own particle tracking, it also comes with some track processing tools. If you did not install ``napari-stracking`` while setting up your environment, you can install ``napari-stracking`` through the following steps:
112 |
113 | 1. Open the plugin installation window by selecting ``Plugins>Install/Uninstall Plugins...`` from the napari menu
114 |
115 | 2. At the bottom of the plugin installation window, type ``napari-stracking`` into the search bar. Press ``Install`` to install napari-stracking. Once napari-stracking appears in the ``Installed Plugins`` section of the plugin installation window, napari-stracking is installed and ready to use! Press ``Close`` to return to the main napari window.
116 |
117 | With napari-stracking installed, we can use it to measure the **length** and **distance** of the tracks generated from TrackMate:
118 |
119 | #. Select ``Plugins>napari-stracking>S Tracks Features`` to open napari-stracking's feature algorithm.
120 | #. Ensure that the ``trackmate_example_data.tif-tracks`` layer is selected in the ``Tracks layer`` dropdown menu.
121 | #. In the ``Add Feature`` dropdown menu, select ``Length``, and then click ``Add`` to add track length as a feature.
122 | #. Still in the ``Add Feature`` dropdown menu, select ``Distance``, and again click ``Add`` to add track distance as a second feature.
123 | #. Click the ``Run`` button to compute the features for each track.
124 |
125 | These steps are shown visually below:
126 |
127 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_4.gif
128 |
129 | |
130 |
131 | You can also filter tracks. Here we filter for tracks that exist in all 40 frames:
132 |
133 | #. Select ``Plugins>napari-stracking>S Filter Track`` to open napari-stracking's track filtering algorithm.
134 | #. Ensure that the ``trackmate_example_data.tif-tracks`` layer is selected in the ``Tracks layer`` dropdown menu.
135 | #. In the ``Add filter`` dropdown menu, select ``Features``, and then click ``Add`` to add a filter.
136 | #. In the ``Features`` pane, select ``length`` in the ``Feature`` dropdown, and set both ``Min`` and ``Max`` to ``40``.
137 | #. Click the ``Run`` button to filter the tracks into a new tracks layer.
138 |
139 | These steps are shown visually below:
140 |
141 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_5.gif
142 |
143 | .. _TrackMate: https://imagej.net/plugins/trackmate
144 | .. _Fiji: https://fiji.sc/
145 | .. _trackmate_example_data.tif: https://media.imagej.net/napari-imagej/0.2.0/trackmate_example_data.tif
146 | .. _napari-stracking: https://www.napari-hub.org/plugins/napari-stracking
147 | .. _scikit-image dimension order: https://scikit-image.org/docs/stable/user_guide/numpy_images.html#a-note-on-the-time-dimension
148 |
--------------------------------------------------------------------------------
/doc/examples/trackmate_reader.rst:
--------------------------------------------------------------------------------
1 | Viewing TrackMate Data in the napari Viewer
2 | ===========================================
3 |
4 | The `TrackMate `_ plugin for ImageJ2 provides a streamlined interface for object tracking. This example shows napari-imagej's capability to view TrackMate tracks in napari, including segmentation labels, *without opening the ImageJ UI*.
5 |
6 | **Note:** TrackMate is not included by default with ImageJ. To set up napari-imagej with TrackMate, see `these instructions <./trackmate.html#trackmate-plugin-setup>`_.
7 |
8 | .. important::
9 |
10 | This Use Case was run with the following Mamba environment::
11 |
12 | mamba env create -n ex-track-read -y -c conda-forge python=3.11 openjdk=11.0 napari=0.5.0 napari-imagej=0.2.0
13 |
14 | and napari-imagej was configured to use the following endpoint::
15 |
16 | sc.fiji:fiji:2.15.0
17 |
18 | TrackMate Setup
19 | ---------------
20 |
21 | By default, napari-imagej does not include TrackMate. To use the TrackMate plugin, we must first configure napari-imagej to enable TrackMate access.
22 |
23 | We can configure napari-imagej to use a `Fiji`_ installation as follows:
24 |
25 | .. |ImageJ2| image:: ../../src/napari_imagej/resources/imagej2-16x16-flat.png
26 |
27 | 1. Activate the napari-imagej plugin by selecting the ``ImageJ2`` plugin from the Plugins menu.
28 |
29 | 2. Open the settings dialog by clicking the rightmost toolbar button, the gear symbol.
30 |
31 | 3. Change the ``ImageJ directory or endpoint`` (described `here <../Configuration.html#imagej-directory-or-endpoint>`_) to include Fiji, which bundles many popular plugins including TrackMate. Change this setting to the napari-imagej endpoint listed above.
32 |
33 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/settings_fiji.png
34 |
35 | 4. **Restart napari** for the changes to take effect.
36 |
37 | 5. Activate the napari-imagej plugin again, as described in step (1) above.
38 |
39 | 6. If you wish, you may verify that Fiji is enabled by pasting the following code into napari's IPython console:
40 |
41 | .. code-block:: python
42 |
43 | from napari_imagej.java import _ij as ij
44 | ij.app().getApps().keys()
45 |
46 | And if ``Fiji`` is in the list, you're good!
47 |
48 | TrackMate XML
49 | -------------
50 |
51 | TrackMate can store generated models in XML. For information on obtaining an XML file from generated Tracks, please see the `TrackMate documentation `_.
52 |
53 | Obtaining sample data
54 | ---------------------
55 |
56 | For this example, we use data from the following publication: |zenodo badge|
57 |
58 | .. |zenodo badge| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5864646.svg
59 | :target: https://doi.org/10.5281/zenodo.5864646
60 |
61 | This data tracks breast cancer cells, taken as a 2D image with time and channel dimensions. The data was segmented using `Cellpose `_.
62 |
63 | You will need to download two files:
64 | #. `BreastCancerCells_multiC.xml `_
65 | #. `BreastCancerCells_multiC.tif `_
66 |
67 | Opening the data
68 | -------------------
69 |
70 | Once napari is running, you can open the data within napari through ``File>Open File(s)...``, and selecting the ``.xml`` sample file that was downloaded.
71 |
72 | There might be a slight delay while the files open. This process can be an expensive operation as we require a running JVM and conversion of the TrackMate ``Model`` into napari ``Layers``; however, the reader plugin displays a progress bar in the ``Activity`` pane.
73 |
74 | When complete, you should see the image, track and label layers in napari:
75 |
76 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/trackmate_reader.gif
77 | :align: center
78 |
79 | .. _Fiji: https://fiji.sc/
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. napari-imagej documentation master file, created by
2 | sphinx-quickstart on Tue Feb 7 14:11:33 2023.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | napari-imagej: ImageJ Ecosystem Access within napari
7 | ====================================================
8 |
9 | The `napari `_ application has brought n-dimensional image browsing and analysis to the Python ecosystem. At the same time, the ImageJ software ecosystem, including `ImageJ `_, `ImageJ2 `_, `Fiji `_ and `thousands of community plugins `_, have been curated over decades, and are utilized by a highly active community of their own.
10 |
11 | The napari-imagej plugin strives to unite these communities by providing access to an ImageJ2 instance within a napari widget. From this widget, users can launch the ImageJ user interface to run **any** ImageJ ecosystem functionality, and can additionally access **ImageJ2** framework functionality directly.
12 |
13 | napari-imagej handles the burden of data transfer between these two applications, enabling accessible, convenient, synergistic workflows.
14 |
15 | .. figure:: https://media.imagej.net/napari-imagej/0.2.0/front_page.png
16 |
17 | Using ImageJ's `Analyze Particles `_ routine within napari
18 |
19 | .. toctree::
20 | :maxdepth: 3
21 | :caption: Contents:
22 |
23 | Install
24 |
25 | Initialization
26 |
27 | Usage
28 |
29 | Configuration
30 |
31 | Troubleshooting
32 |
33 | Use_Cases
34 |
35 | Development
36 |
37 | Architecture
38 |
39 | Benchmarking
40 |
--------------------------------------------------------------------------------
/doc/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 |
--------------------------------------------------------------------------------
/doc/requirements.txt:
--------------------------------------------------------------------------------
1 | -e .
2 | sphinx_rtd_theme
3 | readthedocs-sphinx-search
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | # Use this file to set up napari and napari-imagej.
2 | #
3 | # First, install mambaforge:
4 | #
5 | # https://github.com/conda-forge/miniforge#mambaforge
6 | #
7 | # Then run:
8 | #
9 | # mamba env create
10 | # conda activate napari-imagej
11 | #
12 | # It includes the dependencies needed for using napari-imagej but not tools
13 | # for developer-related actions like running automated tests (pytest),
14 | # linting the code (black), and generating the API documentation (sphinx).
15 | # If you want an environment including these tools, use dev-environment.yml.
16 |
17 | name: napari-imagej
18 | channels:
19 | - conda-forge
20 | dependencies:
21 | - python >= 3.9, < 3.13
22 | # Project depenencies
23 | - confuse >= 2.0.0
24 | - imglyb >= 2.1.0
25 | - jpype1 >= 1.4.1
26 | - labeling >= 0.1.12
27 | - magicgui >= 0.5.1
28 | - napari >= 0.4.17
29 | - numpy
30 | - openjdk=11
31 | - pandas
32 | - pyimagej >= 1.5.0
33 | - scyjava >= 1.9.1
34 | - superqt >= 0.7.0
35 | - xarray < 2024.10.0
36 | # Version rules to avoid problems
37 | - qtconsole != 5.4.2
38 | - typing_extensions != 4.6.0
39 | # Project from source
40 | - pip
41 | - pip:
42 | - validate-pyproject[all]
43 | - '.'
44 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: napari-imagej
2 | site_description: Use ImageJ functionality from napari
3 | site_author: ImageJ2 developers
4 |
5 | theme: readthedocs
6 |
7 |
8 | repo_url: https://github.com/imagej/napari-imagej
9 |
10 | pages:
11 | - Home: index.md
12 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [ "setuptools>=61.2" ]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "napari-imagej"
7 | version = "0.2.1.dev0"
8 | description = "ImageJ functionality from napari"
9 | readme = "README.md"
10 | license = {text = "BSD-2-Clause"}
11 | authors = [{name = "ImageJ2 developers", email = "ctrueden@wisc.edu"}]
12 | keywords = ["java", "imagej", "imagej2", "fiji", "napari"]
13 | classifiers = [
14 | "Development Status :: 4 - Beta",
15 | "Intended Audience :: Developers",
16 | "Intended Audience :: Science/Research",
17 | "Framework :: napari",
18 | "Programming Language :: Python :: 3 :: Only",
19 | "Programming Language :: Python :: 3.9",
20 | "Programming Language :: Python :: 3.10",
21 | "Programming Language :: Python :: 3.11",
22 | "Programming Language :: Python :: 3.12",
23 | "Operating System :: Microsoft :: Windows",
24 | "Operating System :: Unix",
25 | "Operating System :: MacOS",
26 | "License :: OSI Approved :: BSD License",
27 | "Topic :: Scientific/Engineering",
28 | "Topic :: Scientific/Engineering :: Image Processing",
29 | "Topic :: Scientific/Engineering :: Visualization",
30 | "Topic :: Software Development :: Libraries :: Java Libraries",
31 | "Topic :: Software Development :: Libraries :: Python Modules",
32 | ]
33 |
34 | # NB: Keep this in sync with environment.yml AND dev-environment.yml!
35 | requires-python = ">=3.9, <3.13"
36 | dependencies = [
37 | "confuse >= 2.0.0",
38 | "imglyb >= 2.1.0",
39 | "jpype1 >= 1.4.1",
40 | "labeling >= 0.1.12",
41 | "magicgui >= 0.5.1",
42 | "napari >= 0.4.17",
43 | "numpy",
44 | "pandas",
45 | "pyimagej >= 1.5.0",
46 | "scyjava >= 1.9.1",
47 | "superqt >= 0.7.0",
48 | "xarray < 2024.10.0",
49 | # Version rules to avoid problems
50 | "qtconsole != 5.4.2", # https://github.com/napari/napari/issues/5700
51 | "typing_extensions != 4.6.0", # https://github.com/pydantic/pydantic/issues/5821
52 | ]
53 |
54 | [project.optional-dependencies]
55 | # NB: Keep this in sync with dev-environment.yml!
56 | # Development tools
57 | dev = [
58 | "build",
59 | "myst-parser",
60 | "pre-commit",
61 | "pyqt5",
62 | "pytest",
63 | "pytest-cov",
64 | "pytest-env",
65 | "pytest-qt",
66 | "ruff",
67 | "sphinx",
68 | "sphinx-copybutton",
69 | "sphinx-rtd-theme",
70 | "qtpy",
71 | "validate-pyproject[all]",
72 | ]
73 |
74 | [project.urls]
75 | homepage = "https://github.com/imagej/napari-imagej"
76 | documentation = "https://napari-imagej.readthedocs.io"
77 | source = "https://github.com/imagej/napari-imagej"
78 | download = "https://pypi.org/project/napari-imagej/#files"
79 | tracker = "https://github.com/imagej/napari-imagej/issues"
80 |
81 | [project.entry-points."napari.manifest"]
82 | napari-imagej = "napari_imagej:napari.yml"
83 |
84 | [tool.setuptools]
85 | package-dir = {"" = "src"}
86 | include-package-data = true
87 |
88 | [tool.setuptools.packages.find]
89 | where = ["src"]
90 |
91 | # ruff configuration
92 | [tool.ruff]
93 | line-length = 88
94 | src = ["src", "tests"]
95 | include = ["pyproject.toml", "src/**/*.py", "tests/**/*.py"]
96 | extend-exclude = ["bin", "build", "dist", "doc", "scripts"]
97 |
98 | [tool.ruff.lint]
99 | extend-ignore = ["E203"]
100 |
101 | [tool.ruff.lint.per-file-ignores]
102 | # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
103 | "__init__.py" = ["E402", "F401"]
104 |
105 | [tool.pytest.ini_options]
106 | addopts = "-s -p no:faulthandler"
107 | env = [
108 | "NAPARI_IMAGEJ_TESTING=yes",
109 | "NAPARI_IMAGEJ_JVM_COMMAND_LINE_ARGUMENTS=-Dfoo=bar",
110 | ]
111 |
--------------------------------------------------------------------------------
/scripts/examples/Example_Script.py:
--------------------------------------------------------------------------------
1 | #@ boolean (label="boolean") pBoolean
2 | #@ byte (label="byte") pByte
3 | #@ char (label="char") pChar
4 | #@ double (label="double") pDouble
5 | #@ float (label="float") pFloat
6 | #@ int (label="int") pInt
7 | #@ long (label="long") pLong
8 | #@ short (label="short") pShort
9 | #@ Boolean (label="Boolean") oBoolean
10 | #@ Byte (label="Byte") oByte
11 | #@ Character (label="Character") oCharacter
12 | #@ Double (label="Double") oDouble
13 | #@ Float (label="Float") oFloat
14 | #@ Integer (label="Integer") oInteger
15 | #@ Long (label="Long") oLong
16 | #@ Short (label="Short") oShort
17 | #@ int (min=0, max=1000) boundedInteger
18 | #@ double (min=0.2, max=1000.7, stepSize=12.34) boundedDouble
19 | #@ BigInteger bigInteger
20 | #@ BigDecimal bigDecimal
21 | #@ String string
22 | #@ File file
23 | # Colors aren't supported - see https://github.com/imagej/napari-imagej/issues/62
24 | # #@ ColorRGB color
25 | #@output String result
26 |
27 | # A Jython script exercising various parameter types.
28 | # It is the duty of the scripting framework to harvest
29 | # the parameter values from the user, and then display
30 | # the 'result' output parameter, based on its type.
31 |
32 | from java.lang import StringBuilder
33 |
34 | sb = StringBuilder()
35 |
36 | sb.append("Widgets Jython results:\n")
37 |
38 | sb.append("\n")
39 | sb.append("\tboolean = " + str(pBoolean) + "\n")
40 | sb.append("\tbyte = " + str(pByte) + "\n")
41 | sb.append("\tchar = " + "'" + str(pChar) + "'\n")
42 | sb.append("\tdouble = " + str(pDouble) + "\n")
43 | sb.append("\tfloat = " + str(pFloat) + "\n")
44 | sb.append("\tint = " + str(pInt) + "\n")
45 | sb.append("\tlong = " + str(pLong) + "\n")
46 | sb.append("\tshort = " + str(pShort) + "\n")
47 |
48 | sb.append("\n")
49 | sb.append("\tBoolean = " + str(oBoolean) + "\n")
50 | sb.append("\tByte = " + str(oByte) + "\n")
51 | sb.append("\tCharacter = " + "'" + str(oCharacter) + "'\n")
52 | sb.append("\tDouble = " + str(oDouble) + "\n")
53 | sb.append("\tFloat = " + str(oFloat) + "\n")
54 | sb.append("\tInteger = " + str(oInteger) + "\n")
55 | sb.append("\tLong = " + str(oLong) + "\n")
56 | sb.append("\tShort = " + str(oShort) + "\n")
57 |
58 | sb.append("\n")
59 | sb.append("\tbounded integer = " + str(boundedInteger) + "\n")
60 | sb.append("\tbounded double = " + str(boundedDouble) + "\n")
61 |
62 | sb.append("\n")
63 | sb.append("\tBigInteger = " + str(bigInteger) + "\n")
64 | sb.append("\tBigDecimal = " + str(bigDecimal) + "\n")
65 | sb.append("\tString = " + str(string) + "\n")
66 | sb.append("\tFile = " + str(file) + "\n")
67 | # Colors aren't supported - see https://github.com/imagej/napari-imagej/issues/62
68 | # sb.append("\tcolor = " + color + "\n")
69 |
70 | result = sb.toString()
71 |
72 |
--------------------------------------------------------------------------------
/src/napari_imagej/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | napari-imagej brings the power of ImageJ, ImageJ2, and Fiji to the napari viewer.
3 |
4 | With napari-imagej, users can call headless functionality from any of these applications
5 | on napari data structures. Users can also call SciJava scripts on data in napari,
6 | automatically discoverable within the plugin. Users can launch the ImageJ or ImageJ2
7 | user interfaces from napari-imagej and can explicitly transfer data to and from the
8 | napari user interface. Most importantly, all of this functionality is accessible WITHOUT
9 | any explicit conversion between the two ecosystems!
10 |
11 | napari-imagej is NOT designed to be used on its own; instead, it should be launched
12 | from within the napari application. Please see (https://napari.org/stable/#installation)
13 | to get started using the napari application. Once napari is installed, you can then
14 | add napari-imagej as a plugin. Please see (https://www.napari-hub.org/) for a list
15 | of available plugins, including napari-imagej.
16 |
17 | napari-imagej is built upon the PyImageJ project:
18 | https://pyimagej.readthedocs.io/en/latest/
19 | """
20 |
21 | import scyjava as sj
22 |
23 | from napari_imagej.model import NapariImageJ
24 |
25 | __author__ = "ImageJ2 developers"
26 | __version__ = sj.get_version("napari-imagej")
27 |
28 | nij = NapariImageJ()
29 |
--------------------------------------------------------------------------------
/src/napari_imagej/model.py:
--------------------------------------------------------------------------------
1 | from jpype import JImplements, JOverride
2 |
3 | from napari_imagej.java import init_ij, jc
4 |
5 |
6 | class NapariImageJ:
7 | """
8 | An object offering a central access point to napari-imagej's core business logic.
9 | """
10 |
11 | def __init__(self):
12 | self._ij = None
13 | self._repl = None
14 | self._repl_callbacks = []
15 |
16 | @property
17 | def ij(self):
18 | if self._ij is None:
19 | self._ij = init_ij()
20 | return self._ij
21 |
22 | @property
23 | def repl(self) -> "jc.ScriptREPL":
24 | if self._repl is None:
25 | ctx = self.ij.context()
26 | model = self
27 |
28 | @JImplements("java.util.function.Consumer")
29 | class REPLOutput:
30 | @JOverride
31 | def accept(self, t):
32 | s = str(t)
33 | for callback in model._repl_callbacks:
34 | callback(s)
35 |
36 | scriptService = ctx.service(jc.ScriptService)
37 | # Find a Pythonic script language (might be Jython)
38 | names = [lang.getLanguageName() for lang in scriptService.getLanguages()]
39 | name_pref = next(n for n in names if "python" in n.toLowerCase())
40 | self._repl = jc.ScriptREPL(ctx, name_pref, REPLOutput())
41 | # NB: Adds bindings
42 | self._repl.initialize(True)
43 | return self._repl
44 |
45 | def add_repl_callback(self, repl_callback) -> None:
46 | self._repl_callbacks.append(repl_callback)
47 |
--------------------------------------------------------------------------------
/src/napari_imagej/napari.yml:
--------------------------------------------------------------------------------
1 | name: napari-imagej
2 | display_name: napari-imagej
3 | contributions:
4 | commands:
5 | - id: napari-imagej.func
6 | python_name: napari_imagej.widgets.napari_imagej:NapariImageJWidget
7 | title: Run ImageJ2 commands
8 | - id: napari-imagej.get_trackmate_reader
9 | python_name: napari_imagej.readers.trackMate_reader:napari_get_reader
10 | title: Open TrackMate XML
11 | widgets:
12 | - command: napari-imagej.func
13 | display_name: ImageJ2
14 | readers:
15 | - command: napari-imagej.get_trackmate_reader
16 | filename_patterns:
17 | - '*.xml'
18 | accepts_directories: false
19 |
--------------------------------------------------------------------------------
/src/napari_imagej/readers/trackMate_reader.py:
--------------------------------------------------------------------------------
1 | """
2 | A napari reader plugin for importing TrackMate data stored in XML
3 | """
4 |
5 | import xml.etree.ElementTree as ET
6 |
7 | from napari.utils import progress
8 | from scyjava import jimport
9 |
10 | from napari_imagej import nij
11 | from napari_imagej.java import jc
12 | from napari_imagej.types.converters.trackmate import (
13 | model_and_image_to_tracks,
14 | trackmate_present,
15 | )
16 |
17 |
18 | def napari_get_reader(path):
19 | """Returns the reader if it is suitable for the file described at path"""
20 | if isinstance(path, list):
21 | # reader plugins may be handed single path, or a list of paths.
22 | # if it is a list, it is assumed to be an image stack...
23 | # so we are only going to look at the first file.
24 | path = path[0]
25 |
26 | # if we know we cannot read the file, we immediately return None.
27 | if not path.endswith(".xml"):
28 | return None
29 |
30 | # Ensure that the xml file is a TrackMate file
31 | if not ET.parse(path).getroot().tag == "TrackMate":
32 | return None
33 |
34 | # Ensure TrackMate available
35 | if not trackmate_present():
36 | return None
37 |
38 | # otherwise we return the *function* that can read ``path``.
39 | return reader_function
40 |
41 |
42 | def reader_function(path):
43 | pbr = progress(total=4, desc="Importing TrackMate XML: Starting JVM")
44 | ij = nij.ij
45 | TmXMLReader = jimport("fiji.plugin.trackmate.io.TmXmlReader")
46 | pbr.update()
47 |
48 | pbr.set_description("Importing TrackMate XML: Building Model")
49 | instance = TmXMLReader(jc.File(path))
50 | model = instance.getModel()
51 | imp = instance.readImage()
52 | pbr.update()
53 |
54 | pbr.set_description("Importing TrackMate XML: Converting Image")
55 | py_imp = ij.py.from_java(imp)
56 | pbr.update()
57 |
58 | pbr.set_description("Importing TrackMate XML: Converting Tracks and ROIs")
59 | py_tracks, py_labels = model_and_image_to_tracks(model, imp)
60 | pbr.update()
61 |
62 | # Return data
63 | pbr.close()
64 | return [
65 | (py_imp.data, {"name": py_imp.name}, "image"),
66 | (py_tracks.data, {"name": py_tracks.name}, "tracks"),
67 | (py_labels.data, {"name": py_labels.name}, "labels"),
68 | ]
69 |
--------------------------------------------------------------------------------
/src/napari_imagej/resources/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A module used to help find napari-imagej widget resources
3 | """
4 |
5 | from pathlib import Path
6 |
7 | PATH = Path(__file__).parent.resolve()
8 | RESOURCES = {x.stem: str(x) for x in PATH.iterdir() if x.suffix != ".py"}
9 |
10 |
11 | def resource_path(name: str) -> str:
12 | """Return path to a resource in this folder."""
13 | if name not in RESOURCES:
14 | raise ValueError(
15 | f"{name} is not a known resource! Known resources: {RESOURCES}"
16 | )
17 | return RESOURCES[name]
18 |
--------------------------------------------------------------------------------
/src/napari_imagej/resources/export.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/napari_imagej/resources/export_detailed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
55 |
--------------------------------------------------------------------------------
/src/napari_imagej/resources/imagej2-16x16-flat-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imagej/napari-imagej/fb980a8c70d126fbb9bc26f2c10f8c59fdf9bff4/src/napari_imagej/resources/imagej2-16x16-flat-disabled.png
--------------------------------------------------------------------------------
/src/napari_imagej/resources/imagej2-16x16-flat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imagej/napari-imagej/fb980a8c70d126fbb9bc26f2c10f8c59fdf9bff4/src/napari_imagej/resources/imagej2-16x16-flat.png
--------------------------------------------------------------------------------
/src/napari_imagej/resources/import.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/napari_imagej/resources/repl.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
47 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A module whose submodules contain additional scyjava Converters
3 | that are useful within napari-imagej
4 |
5 | All submodules within this module are DYNAMICALLY IMPORTED; this allows
6 | automatic discovery of all Converters.
7 |
8 | Notable functions included in the module:
9 | * install_converters()
10 | - used to add the napari-imagej Converters to scyjava's conversion framework.
11 | """
12 |
13 | import pkgutil
14 | from importlib.util import module_from_spec
15 | from typing import Any, Callable, List
16 |
17 | from scyjava import (
18 | Converter,
19 | Priority,
20 | add_java_converter,
21 | add_py_converter,
22 | when_jvm_starts,
23 | )
24 |
25 | # PHASE 1 - DEFINE THE CONVERTER DECORATORS
26 |
27 | JAVA_TO_PY_CONVERTERS: List = []
28 | PY_TO_JAVA_CONVERTERS: List = []
29 |
30 |
31 | def java_to_py_converter(
32 | predicate: Callable[[Any], bool], priority: int = Priority.NORMAL
33 | ):
34 | """
35 | A decorator used to register a given function as a scyjava Converter.
36 | Decorated functions will be used to convert JAVA objects into PYTHON objects.
37 | :param predicate: defines situations in which the Converter should be used.
38 | :param priority: the scyjava Priority of this Converter, used to break ties.
39 | :return: the function
40 | """
41 |
42 | def inner(func: Callable):
43 | JAVA_TO_PY_CONVERTERS.append(
44 | Converter(predicate=predicate, converter=func, priority=priority)
45 | )
46 | return func
47 |
48 | return inner
49 |
50 |
51 | def py_to_java_converter(
52 | predicate: Callable[[Any], bool], priority: int = Priority.NORMAL
53 | ):
54 | """
55 | A decorator used to register a given function as a scyjava Converter.
56 | Decorated functions will be used to convert PYTHON objects into JAVA objects.
57 | :param predicate: defines situations in which the Converter should be used.
58 | :param priority: the scyjava Priority of this Converter, used to break ties.
59 | :return: the function
60 | """
61 |
62 | def inner(func: Callable):
63 | PY_TO_JAVA_CONVERTERS.append(
64 | Converter(predicate=predicate, converter=func, priority=priority)
65 | )
66 | return func
67 |
68 | return inner
69 |
70 |
71 | # PHASE 2 - DISCOVER ALL CONVERTERS
72 |
73 |
74 | # Dynamically import all submodules
75 | # By importing these submodules, top-level functions will be decorated,
76 | # Installing them into the scyjava conversion system.
77 | __all__ = []
78 | for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
79 | __all__.append(module_name)
80 | # Find the module specification
81 | _spec = loader.find_spec(module_name)
82 | # Get the module
83 | _module = module_from_spec(_spec)
84 | # Execute the module
85 | _spec.loader.exec_module(_module)
86 | # Store the module for later
87 | globals()[module_name] = _module
88 |
89 |
90 | # PHASE 3 - INSTALL ALL CONVERTERS
91 |
92 |
93 | def install_converters():
94 | """Installs napari-imagej specific converters"""
95 |
96 | def _install_converters():
97 | for converter in JAVA_TO_PY_CONVERTERS:
98 | add_py_converter(converter)
99 | for converter in PY_TO_JAVA_CONVERTERS:
100 | add_java_converter(converter)
101 |
102 | when_jvm_starts(_install_converters)
103 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/enum_likes.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting JavaEnumLikes into their java equivalents.
3 | """
4 |
5 | from napari_imagej.java import jc
6 | from napari_imagej.types.converters import py_to_java_converter
7 | from napari_imagej.types.enum_likes import OutOfBoundsFactory
8 |
9 |
10 | @py_to_java_converter(predicate=lambda obj: isinstance(obj, OutOfBoundsFactory))
11 | def _py_to_java_outOfBoundsFactory(obj: OutOfBoundsFactory) -> "jc.OutOfBoundsFactory":
12 | """
13 | Converts OutOfBoundsFactory JavaEnumLikes into actual OutOfBoundsFactories
14 | :param obj: the OutOfBoundsFactory JavaEnumLike
15 | :return: the actual OutOfBoundsFactory
16 | """
17 | if obj == OutOfBoundsFactory.BORDER:
18 | return jc.OutOfBoundsBorderFactory()
19 | if obj == OutOfBoundsFactory.MIRROR_EXP_WINDOWING:
20 | return jc.OutOfBoundsMirrorExpWindowingFactory()
21 | if obj == OutOfBoundsFactory.MIRROR_SINGLE:
22 | return jc.OutOfBoundsMirrorFactory(jc.OutOfBoundsMirrorFactory.Boundary.SINGLE)
23 | if obj == OutOfBoundsFactory.MIRROR_DOUBLE:
24 | return jc.OutOfBoundsMirrorFactory(jc.OutOfBoundsMirrorFactory.Boundary.DOUBLE)
25 | if obj == OutOfBoundsFactory.PERIODIC:
26 | return jc.OutOfBoundsPeriodicFactory()
27 | raise ValueError(f"{obj} is not a supported OutOfBoundsFactory!")
28 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/enums.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting Java enums to their autogenerated Python enum
3 | """
4 |
5 | from enum import Enum
6 |
7 | from napari_imagej.java import jc
8 | from napari_imagej.types.converters import java_to_py_converter, py_to_java_converter
9 | from napari_imagej.types.enums import _is_autogenerated_enum, py_enum_for
10 |
11 |
12 | @py_to_java_converter(predicate=_is_autogenerated_enum)
13 | def _py_enum_to_java_enum(obj: Enum) -> "jc.Enum":
14 | """Converts an autogenerated Python Enum into a Java Enum"""
15 | return obj.value
16 |
17 |
18 | @java_to_py_converter(
19 | predicate=lambda obj: hasattr(obj, "getClass") and py_enum_for(obj.getClass())
20 | )
21 | def _java_enum_to_py_enum(obj: "jc.Enum") -> Enum:
22 | """Converts a Java Enum into an AUTOGENERATED Python Enum"""
23 | py_enum = py_enum_for(obj.getClass())
24 | for e in py_enum:
25 | if e.value == obj:
26 | return e
27 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/images.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting between ImageJ ecosystem image types
3 | (referred to collectively as "java image"s) and napari Image layers
4 | """
5 |
6 | from logging import getLogger
7 | from typing import Any, List, Union
8 |
9 | from imagej.convert import java_to_xarray
10 | from jpype import JArray, JByte
11 | from napari.layers import Image
12 | from napari.utils.colormaps import Colormap
13 | from numpy import ones, uint8
14 | from scyjava import Priority
15 | from xarray import DataArray
16 |
17 | from napari_imagej import nij
18 | from napari_imagej.java import jc
19 | from napari_imagej.types.converters import java_to_py_converter, py_to_java_converter
20 |
21 |
22 | @java_to_py_converter(
23 | predicate=lambda obj: nij.ij.convert().supports(obj, jc.DatasetView),
24 | priority=Priority.VERY_HIGH + 1,
25 | )
26 | def _java_image_to_image_layer(image: Any) -> Union[Image, List[Image]]:
27 | """
28 | Converts a java image (i.e. something that can be converted into a DatasetView)
29 | into a napari Image layer.
30 |
31 | TODO: Pass xarray axis labels, coordinates to the Layer, once the Layer
32 | can understand them. See https://github.com/imagej/napari-imagej/issues/253.
33 | :param image: a java image
34 | :return: a napari Image layer
35 | """
36 | # Construct a DatasetView from the Java image
37 | view = nij.ij.convert().convert(image, jc.DatasetView)
38 | existing_ctables = view.getColorTables() and view.getColorTables().size() > 0
39 | data = view.getData()
40 | # Construct an xarray from the DatasetView
41 | xarr: DataArray = java_to_xarray(nij.ij, data)
42 | # General layer parameters
43 | kwargs = dict()
44 | kwargs["name"] = data.getName()
45 | kwargs["metadata"] = getattr(xarr, "attrs", {})
46 |
47 | # Channel-less data
48 | if "ch" not in xarr.dims:
49 | if existing_ctables:
50 | cmap = _color_table_to_colormap(view.getColorTables().get(0))
51 | kwargs["colormap"] = cmap
52 | pass
53 | # RGB data - set RGB flag
54 | elif xarr.sizes["ch"] in [3, 4]:
55 | kwargs["rgb"] = True
56 | # Channel data - but not RGB - need one layer per channel
57 | else:
58 | kwargs["blending"] = "additive"
59 | channels = []
60 | for d in range(xarr.sizes["ch"]):
61 | kw = kwargs.copy()
62 | kw["name"] = f"{kwargs['name']}[{d}]"
63 | if existing_ctables:
64 | cmap = _color_table_to_colormap(view.getColorTables().get(d))
65 | kw["colormap"] = cmap
66 | channels.append(Image(data=xarr.sel(ch=d), **kw))
67 | return channels
68 | return Image(data=xarr, **kwargs)
69 |
70 |
71 | @py_to_java_converter(
72 | predicate=lambda obj: isinstance(obj, Image), priority=Priority.VERY_HIGH
73 | )
74 | def _image_layer_to_dataset(image: Image, **kwargs) -> "jc.Dataset":
75 | """
76 | Converts a napari Image layer into a Dataset.
77 |
78 | :param image: a napari Image layer
79 | :return: a Dataset
80 | """
81 | # Redefine dimension order if necessary
82 | data = image.data
83 | if hasattr(data, "dims"):
84 | if "dim_order" in kwargs:
85 | dim_remapping = {
86 | old: new for old, new in zip(data.dims, kwargs["dim_order"])
87 | }
88 | data = data.rename(dim_remapping)
89 | kwargs.pop("dim_order")
90 | # Define dimension order if necessary
91 | elif "dim_order" not in kwargs:
92 | # NB "dim_i"s will be overwritten later
93 | dim_order = [f"dim_{i}" for i in range(len(data.shape))]
94 | # if RGB, last dimension is Channel
95 | if image.rgb:
96 | dim_order[-1] = "Channel"
97 |
98 | kwargs["dim_order"] = dim_order
99 |
100 | # Construct a dataset from the data
101 | dataset: "jc.Dataset" = nij.ij.py.to_dataset(data, **kwargs)
102 |
103 | # Clean up the axes
104 | axes = [
105 | x for x in [jc.Axes.X, jc.Axes.Y, jc.Axes.Z] if dataset.dimensionIndex(x) == -1
106 | ]
107 | for i in range(dataset.numDimensions()):
108 | axis = dataset.axis(i)
109 | # Overwrite EnumeratedAxes with LinearAxes
110 | if isinstance(axis, (jc.EnumeratedAxis, jc.DefaultLinearAxis)):
111 | # Copy the dim name, unless it's unnamed
112 | # in that case, assign it with X/Y/Z, if they aren't used already
113 | if any(x in axis.type().getLabel() for x in ["dim", "Unknown"]) and len(
114 | axes
115 | ):
116 | type = axes.pop(0)
117 | else:
118 | type = axis.type()
119 | # Use 1 for scale, and 0 for origin
120 | axis = jc.DefaultLinearAxis(type, 1, 0)
121 | dataset.setAxis(axis, i)
122 | # Set pixels as the unit, for lack of a better option
123 | axis.setUnit("pixels")
124 |
125 | # Add name
126 | dataset.setName(image.name)
127 |
128 | # Set RGB
129 | if (
130 | image.rgb
131 | and dataset.dimensionIndex(jc.Axes.CHANNEL) > -1
132 | and image.dtype == uint8
133 | ):
134 | dataset.setRGBMerged(True)
135 | # or add color table, if the image uses a custom colormap
136 | elif image.colormap.name != "gray":
137 | color_table = _colormap_to_color_table(image.colormap)
138 | dataset.initializeColorTables(1)
139 | dataset.setColorTable(color_table, 0)
140 | # Add properties
141 | properties = dataset.getProperties()
142 | for k, v in image.metadata.items():
143 | try:
144 | properties.put(nij.ij.py.to_java(k), nij.ij.py.to_java(v))
145 | except Exception:
146 | getLogger("napari-imagej").debug(
147 | f"Could not add property ({k}, {v}) to dataset {dataset}:"
148 | )
149 | return dataset
150 |
151 |
152 | def _colormap_to_color_table(cmap: Colormap):
153 | """
154 | Converts a napari Colormap into a SciJava ColorTable.
155 | :param cmap: The napari Colormap
156 | :return: A "equivalent" SciJava ColorTable
157 | """
158 | controls = [x / 255 for x in range(256)]
159 | py_values = cmap.map(controls)
160 | shape = py_values.shape
161 | j_values = JArray(JArray(JByte))(shape[1])
162 | for i in range(shape[1]):
163 | j_values[i] = JArray(JByte)(shape[0])
164 | for j in range(shape[0]):
165 | value = int(round(py_values[j, i] * 255))
166 | # map unsigned value to signed
167 | j_values[i][j] = value if value < 128 else value - 256
168 |
169 | return jc.ColorTable8(j_values)
170 |
171 |
172 | def _color_table_to_colormap(ctable: "jc.ColorTable"):
173 | """
174 | Converts a SciJava ColorTable into a napari Colormap.
175 | :param ctable: The SciJava ColorTable
176 | :return: An "equivalent" napari Colormap
177 | """
178 | builtins = {
179 | jc.ColorTables.RED: "red",
180 | jc.ColorTables.GREEN: "green",
181 | jc.ColorTables.BLUE: "blue",
182 | jc.ColorTables.CYAN: "cyan",
183 | jc.ColorTables.MAGENTA: "magenta",
184 | jc.ColorTables.YELLOW: "yellow",
185 | jc.ColorTables.GRAYS: "gray",
186 | }
187 | if ctable in builtins:
188 | return builtins[ctable]
189 |
190 | components = ctable.getComponentCount()
191 | bins = ctable.getLength()
192 | data = ones((bins, 4), dtype=float)
193 | for component in range(components):
194 | for bin in range(bins):
195 | data[bin, component] = float(ctable.get(component, bin)) / 255.0
196 | cmap = Colormap(colors=data)
197 | # NB prevents napari from using cached colormaps
198 | cmap.name = str(ctable)
199 |
200 | return cmap
201 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/labels.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting between ImgLib2 ImgLabelings
3 | and napari Labels
4 | """
5 |
6 | from imagej.convert import imglabeling_to_labeling
7 | from labeling.Labeling import Labeling
8 | from napari.layers import Labels
9 | from scyjava import Priority
10 |
11 | from napari_imagej import nij
12 | from napari_imagej.java import jc
13 | from napari_imagej.types.converters import java_to_py_converter, py_to_java_converter
14 |
15 |
16 | def _labeling_to_layer(labeling: Labeling):
17 | """Converts a Labeling to a Labels layer"""
18 | img, data = labeling.get_result()
19 | layer = Labels(img, metadata={"pyLabelingData": data})
20 | return layer
21 |
22 |
23 | def _layer_to_labeling(layer: Labels):
24 | """Converts a Labels layer to a Labeling"""
25 | if "pyLabelingData" in layer.metadata:
26 | metadata = vars(layer.metadata["pyLabelingData"])
27 | labeling = Labeling(shape=layer.data.shape)
28 | labeling.result_image = layer.data
29 | labeling.label_sets = metadata["labelSets"]
30 | labeling.metadata = metadata["metadata"]
31 | return labeling
32 | else:
33 | return Labeling.fromValues(layer.data)
34 |
35 |
36 | @java_to_py_converter(
37 | predicate=lambda obj: isinstance(obj, jc.ImgLabeling),
38 | priority=Priority.VERY_HIGH + 2,
39 | )
40 | def _imglabeling_to_layer(imgLabeling: "jc.ImgLabeling") -> Labels:
41 | """
42 | Converts a Java ImgLabeling to a napari Labels layer
43 | :param imgLabeling: the Java ImgLabeling
44 | :return: a Labels layer
45 | """
46 | labeling: Labeling = imglabeling_to_labeling(nij.ij, imgLabeling)
47 | return _labeling_to_layer(labeling)
48 |
49 |
50 | @py_to_java_converter(
51 | predicate=lambda obj: isinstance(obj, Labels), priority=Priority.VERY_HIGH
52 | )
53 | def _layer_to_imglabeling(layer: Labels) -> "jc.ImgLabeling":
54 | """
55 | Converts a napari Labels layer to a Java ImgLabeling
56 | :param layer: a Labels layer
57 | :return: the Java ImgLabeling
58 | """
59 | labeling: Labeling = _layer_to_labeling(layer)
60 | return nij.ij.py.to_java(labeling)
61 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/meshes.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting between ImageJ2 Meshes and napari Surfaces
3 | """
4 |
5 | import numpy as np
6 | from jpype import JArray, JDouble
7 | from napari.layers import Surface
8 | from scyjava import Priority
9 |
10 | from napari_imagej.java import jc
11 | from napari_imagej.types.converters import java_to_py_converter, py_to_java_converter
12 |
13 |
14 | @java_to_py_converter(
15 | predicate=lambda obj: isinstance(obj, jc.Mesh), priority=Priority.VERY_HIGH
16 | )
17 | def _mesh_to_surface(mesh: "jc.Mesh") -> Surface:
18 | """Converts an ImageJ2 Mesh into a napari Surface"""
19 | # Vertices
20 | vertices = mesh.vertices()
21 | py_vertices = np.zeros((vertices.size(), 3))
22 | position = JArray(JDouble)(3)
23 | for i, vertex in enumerate(vertices):
24 | vertex.localize(position)
25 | # Note that the dimensions are reversed across the language barrier
26 | py_vertices[i, 2] = position[0]
27 | py_vertices[i, 1] = position[1]
28 | py_vertices[i, 0] = position[2]
29 | # Triangles
30 | triangles = mesh.triangles()
31 | py_triangles = np.zeros((triangles.size(), 3), dtype=np.int64)
32 | for i, triangle in enumerate(triangles):
33 | py_triangles[i, 0] = triangle.vertex0()
34 | py_triangles[i, 1] = triangle.vertex1()
35 | py_triangles[i, 2] = triangle.vertex2()
36 | return Surface(data=(py_vertices, py_triangles))
37 |
38 |
39 | @py_to_java_converter(
40 | predicate=lambda obj: isinstance(obj, Surface), priority=Priority.VERY_HIGH
41 | )
42 | def _surface_to_mesh(surface: Surface) -> "jc.Mesh":
43 | """Converts a napari Surface into an ImageJ2 Mesh"""
44 | if surface.ndim != 3:
45 | raise ValueError("Can only convert 3D Surfaces to Meshes!")
46 | # Surface data is vertices, triangles, colormap data
47 | py_vertices, py_triangles, _ = surface.data
48 |
49 | mesh: "jc.Mesh" = jc.NaiveDoubleMesh()
50 | # TODO: Determine the normals
51 | for py_vertex in py_vertices:
52 | # Note that the dimensions are reversed across the language barrier
53 | mesh.vertices().add(py_vertex[2], py_vertex[1], py_vertex[0])
54 | for py_triangle in py_triangles:
55 | mesh.triangles().add(*py_triangle)
56 | return mesh
57 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/points.py:
--------------------------------------------------------------------------------
1 | """
2 | scyjava Converters for converting between ImageJ2 RealPointCollections
3 | and napari Points
4 | """
5 |
6 | import numpy as np
7 | from jpype import JArray, JDouble
8 | from napari.layers import Points
9 | from scyjava import Priority
10 |
11 | from napari_imagej.java import jc
12 | from napari_imagej.types.converters import java_to_py_converter, py_to_java_converter
13 |
14 |
15 | def arr(coords):
16 | arr = JArray(JDouble)(len(coords))
17 | arr[:] = coords
18 | return arr
19 |
20 |
21 | def realPoint_from(coords: np.ndarray):
22 | """
23 | Creates a RealPoint from a numpy [1, D] array of coordinates.
24 | :param coords: The [1, D] numpy array of coordinates
25 | :return: a RealPoint
26 | """
27 | # JPype doesn't know whether to call the float or double.
28 | # We make the choice for them using the function arr
29 | return jc.RealPoint(arr(coords))
30 |
31 |
32 | @py_to_java_converter(
33 | predicate=lambda obj: isinstance(obj, Points), priority=Priority.VERY_HIGH
34 | )
35 | def _points_to_realpointcollection(points: Points) -> "jc.RealPointCollection":
36 | """Converts a napari Points into an ImageJ2 RealPointCollection"""
37 | data = points.data
38 | n = data.shape[1]
39 | # Reshape data to align with language conventions
40 | if n == 2:
41 | data = data[:, [1, 0]] # (Y, X) in Python --> (X, Y) in Java
42 | elif n == 3:
43 | data = data[:, [2, 1, 0]] # (Z, Y, X) in Python --> (X, Y, Z) in Java
44 | else:
45 | raise ValueError(f"Do not know how to translate point of {n} dimensions")
46 | pts = [realPoint_from(x) for x in data]
47 | ptList = jc.ArrayList(pts)
48 | return jc.DefaultWritableRealPointCollection(ptList)
49 |
50 |
51 | @java_to_py_converter(
52 | predicate=lambda obj: isinstance(obj, jc.RealPointCollection),
53 | priority=Priority.VERY_HIGH,
54 | )
55 | def _realpointcollection_to_points(collection: "jc.RealPointCollection") -> Points:
56 | """Converts an ImageJ2 RealPointsCollection into a napari Points"""
57 | # data - collection.size() points, collection.numDimensions() values per point
58 | data = np.zeros((collection.size(), collection.numDimensions()))
59 | # Create a temporary array to pass to each point
60 | # N.B. we cannot just write pt.localize(data[i, :]) as JPype does not know
61 | # whether to use the localize(double[]) method or the localize(float[]) method.
62 | # We thus have to make the decision ourselves using tmp_arr, a double[].
63 | tmp_arr_dims = int(collection.numDimensions())
64 | tmp_arr = JArray(JDouble)(tmp_arr_dims)
65 | for i, pt in enumerate(collection.points()):
66 | pt.localize(tmp_arr)
67 | data[i, :] = tmp_arr
68 | # Reshape data to align with language conventions
69 | n = data.shape[1]
70 | if n == 2:
71 | data = data[:, [1, 0]] # (X, Y) in Java --> (Y, X) in Python
72 | elif n == 3:
73 | data = data[:, [2, 1, 0]] # (X, Y, Z) in Java --> (Z, Y, X) in Python
74 | else:
75 | raise ValueError(f"Do not know how to translate point of {n} dimensions")
76 | return Points(data=data)
77 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/converters/trackmate.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 | import numpy as np
3 | from napari.layers import Labels, Tracks
4 | from scyjava import JavaClasses, Priority
5 |
6 | from napari_imagej import nij
7 | from napari_imagej.java import NijJavaClasses
8 | from napari_imagej.types.converters import java_to_py_converter
9 |
10 |
11 | @lru_cache
12 | def trackmate_present():
13 | """
14 | Returns True iff TrackMate is on the classpath. Starts the JVM
15 | """
16 | try:
17 | # Start the JVM
18 | nij.ij
19 | # Try to import the class
20 | return jc.TrackMate is not None
21 | except Exception:
22 | return False
23 |
24 |
25 | def track_overlay_predicate(obj):
26 | """
27 | Returns True iff obj is a TrackMate Overlay, wrapped into a ROITree.
28 | """
29 | # Prevent ImportErrors by ensuring TrackMate is on the classpath
30 | if not trackmate_present():
31 | return False
32 | # TrackMate data is wrapped in ImageJ Rois - we need ImageJ Legacy
33 | if not (nij.ij.legacy and nij.ij.legacy.isActive()):
34 | return False
35 | # TrackMate data will be wrapped within a ROITree
36 | if not isinstance(obj, jc.ROITree):
37 | return False
38 | # Where each child is a IJRoiWrapper
39 | children = [child.data() for child in obj.children()]
40 | for child in children:
41 | if not isinstance(child, jc.IJRoiWrapper):
42 | return False
43 | # More specifically, there must be (at least) two IJRoiWrapper children.
44 | if len(children) < 2:
45 | return False
46 | # One must be a SpotOverlay
47 | if not any(isinstance(child.getRoi(), jc.SpotOverlay) for child in children):
48 | return False
49 | # And another is a TrackOverlay
50 | if not any(isinstance(child.getRoi(), jc.TrackOverlay) for child in children):
51 | return False
52 | return True
53 |
54 |
55 | def model_and_image_to_tracks(model: "jc.Model", imp: "jc.ImagePlus"):
56 | neighbor_index = model.getTrackModel().getDirectedNeighborIndex()
57 |
58 | cal = jc.TMUtils.getSpatialCalibration(imp)
59 |
60 | spots = []
61 | graph = {}
62 | branch_ids = {}
63 | index = 0
64 | for track_id in model.getTrackModel().unsortedTrackIDs(True):
65 | # Decompose the track into branches
66 | branch_decomposition = jc.ConvexBranchesDecomposition.processTrack(
67 | track_id, model.getTrackModel(), neighbor_index, True, False
68 | )
69 | branch_graph = jc.ConvexBranchesDecomposition.buildBranchGraph(
70 | branch_decomposition
71 | )
72 | # Pass 1 - assign an id to each branch, and add the spots
73 | for branch in branch_graph.vertexSet():
74 | branch_ids[branch] = index
75 | for spot in branch:
76 | x = spot.getFeature(jc.Spot.POSITION_X)
77 | y = spot.getFeature(jc.Spot.POSITION_Y)
78 | z = spot.getFeature(jc.Spot.POSITION_Z)
79 | t = spot.getFeature(jc.Spot.FRAME).intValue()
80 | spots.append([index, t, z / cal[2], y / cal[1], x / cal[0]])
81 |
82 | index += 1
83 | # Pass 2 - establish parent-child relationships
84 | for branch in branch_graph.vertexSet():
85 | branch_id = branch_ids[branch]
86 | graph[branch_id] = []
87 | parent_edges = branch_graph.incomingEdgesOf(branch)
88 | for parent_edge in parent_edges:
89 | parent_branch = branch_graph.getEdgeSource(parent_edge)
90 | graph[branch_id].append(branch_ids[parent_branch])
91 |
92 | spot_data = np.array(spots)
93 | if "Z" not in imp.dims:
94 | spot_data = np.delete(spot_data, 2, 1)
95 | # rois = [np.delete(roi, 2, 1) for roi in rois]
96 |
97 | tracks_name = f"{imp.getTitle()}-tracks"
98 | tracks = Tracks(data=spot_data, graph=graph, name=tracks_name)
99 | rois_name = f"{imp.getTitle()}-rois"
100 | java_label_img = jc.LabelImgExporter.createLabelImagePlus(
101 | model, imp, False, False, False
102 | )
103 | py_label_img = nij.ij.py.from_java(java_label_img)
104 | labels = Labels(data=py_label_img.data, name=rois_name)
105 |
106 | return (tracks, labels)
107 |
108 |
109 | @java_to_py_converter(
110 | predicate=track_overlay_predicate, priority=Priority.EXTREMELY_HIGH
111 | )
112 | def _trackMate_model_to_tracks(obj: "jc.ROITree"):
113 | """
114 | Converts a TrackMate overlay into a napari Tracks layer
115 | """
116 | trackmate_plugins = nij.ij.object().getObjects(jc.TrackMate)
117 | if len(trackmate_plugins) == 0:
118 | raise IndexError("Expected a TrackMate instance, but there was none!")
119 | model: jc.Model = trackmate_plugins[-1].getModel()
120 | src_image = obj.children()[0].data().getRoi().getImage()
121 | return model_and_image_to_tracks(model, src_image)
122 |
123 |
124 | class TrackMateClasses(NijJavaClasses):
125 | # TrackMate Types
126 |
127 | @JavaClasses.java_import
128 | def BranchTableView(self):
129 | return "fiji.plugin.trackmate.visualization.table.BranchTableView"
130 |
131 | @JavaClasses.java_import
132 | def ConvexBranchesDecomposition(self):
133 | return "fiji.plugin.trackmate.graph.ConvexBranchesDecomposition"
134 |
135 | @JavaClasses.java_import
136 | def LabelImgExporter(self):
137 | return "fiji.plugin.trackmate.action.LabelImgExporter"
138 |
139 | @JavaClasses.java_import
140 | def Model(self):
141 | return "fiji.plugin.trackmate.Model"
142 |
143 | @JavaClasses.java_import
144 | def Spot(self):
145 | return "fiji.plugin.trackmate.Spot"
146 |
147 | @JavaClasses.java_import
148 | def SpotOverlay(self):
149 | return "fiji.plugin.trackmate.visualization.hyperstack.SpotOverlay"
150 |
151 | @JavaClasses.java_import
152 | def TMUtils(self):
153 | return "fiji.plugin.trackmate.util.TMUtils"
154 |
155 | @JavaClasses.java_import
156 | def TrackMate(self):
157 | return "fiji.plugin.trackmate.TrackMate"
158 |
159 | @JavaClasses.java_import
160 | def TrackOverlay(self):
161 | return "fiji.plugin.trackmate.visualization.hyperstack.TrackOverlay"
162 |
163 |
164 | jc = TrackMateClasses()
165 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/enum_likes.py:
--------------------------------------------------------------------------------
1 | """
2 | A module defining Java "Enum-like"s.
3 | An enum-like can be created for any Enum-like Java types.
4 |
5 | We define an Enum-like Java type as a Java type that
6 | 1. is stateless
7 | 2. has a no-args constructor
8 |
9 | The enum-likes are designed for a rather narrow use case, described as follows:
10 |
11 | Some Java functionality requires an Object implementing a particular interface.
12 | That Object is usually chosen from a set of established implementations,
13 | each created without variation (giving rise to the above constraints).
14 |
15 | In such cases, we can abstract the interface to a JavaEnumLike and the implementations
16 | to enumerations of that JavaEnumLike, as done below.
17 |
18 | JavaEnumLikes are NOT intended for direct use. Instead, use enum_like() to obtain
19 | the correct JavaEnumLike for a Java enum-like!
20 | """
21 |
22 | from enum import Enum, auto
23 | from typing import List
24 |
25 | from napari_imagej.java import jc
26 |
27 |
28 | class JavaEnumLike(Enum):
29 | """An Enum that is mocking a Java class"""
30 |
31 | def __init__(self, _):
32 | # Make ourselves aware of this enum
33 | enum_type = type(self)
34 | if enum_type not in _ENUM_LIKES:
35 | _ENUM_LIKES.append(enum_type)
36 |
37 | @staticmethod
38 | def java_type():
39 | """Obtains the backing Java type this enum is mocking"""
40 |
41 |
42 | _ENUM_LIKES: List[JavaEnumLike] = []
43 |
44 |
45 | def enum_like(java_type, default=None) -> type:
46 | """
47 | Checks for an "enum_like" for java_type
48 |
49 | NB we use == instead of "is" to check backing types.
50 | "is" ensures two variables point to the same memory,
51 | whereas "==" checks equality. We want the latter when checking classes.
52 | :param java_type: the type we'd like an enum-like for
53 | :param default: the return when we can't find an enum-like
54 | :return: an enum-like for java_type, or default if we can't find one.
55 | """
56 | for enum_like in _ENUM_LIKES:
57 | if java_type == enum_like.java_type():
58 | return enum_like
59 | return default
60 |
61 |
62 | class OutOfBoundsFactory(JavaEnumLike):
63 | BORDER = auto()
64 | MIRROR_EXP_WINDOWING = auto()
65 | MIRROR_SINGLE = auto()
66 | MIRROR_DOUBLE = auto()
67 | PERIODIC = auto()
68 |
69 | @staticmethod
70 | def java_type():
71 | return jc.OutOfBoundsFactory
72 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/enums.py:
--------------------------------------------------------------------------------
1 | """
2 | A module used to automatically generate python Enums from java Enums.
3 | """
4 |
5 | from enum import Enum
6 | from functools import lru_cache
7 | from typing import Dict
8 |
9 | from napari_imagej.java import jc
10 |
11 | _ENUMS: Dict["jc.Enum", Enum] = {}
12 |
13 |
14 | @lru_cache(maxsize=None)
15 | def py_enum_for(java_type: "jc.Enum"):
16 | """
17 | Generates a Python Enum equivalent to java_type
18 | This function caches inputs, to prevent duplicates.
19 | :param java_type: a Java Enum
20 | :return: a Python Enum, with the same name and values as java_type
21 | """
22 |
23 | # Ensure we have an Enum
24 | if not java_type.isEnum():
25 | return None
26 | # Construct a Python enum equivalent to the java one
27 | value = str(java_type.getSimpleName())
28 | names = {str(v): v for v in java_type.getEnumConstants()}
29 | py_enum = Enum(value=value, names=names)
30 |
31 | # Keep track of it for later
32 | _ENUMS[py_enum] = java_type
33 | return py_enum
34 |
35 |
36 | def _is_autogenerated_enum(obj: Enum) -> bool:
37 | """
38 | Returns true if obj is an Enum that this module auto-generated.
39 | :param obj: an object that may or may not be an Enum
40 | :return: true iff obj is an enum this module automatically generated.
41 | """
42 | return isinstance(obj, Enum) and type(obj) in _ENUMS.keys()
43 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/type_conversions.py:
--------------------------------------------------------------------------------
1 | """
2 | A module containing TYPE conversion utilities.
3 |
4 | When wrapping Java functionality, we must perform data conversion in two phases.
5 |
6 | The first phase of conversion involves noting the required JAVA type of a parameter
7 | and determining the corresponding PYTHON type to use in harvesting input.
8 |
9 | The second phase of conversion involves transforming accepted PYTHON inputs into
10 | their "equivalent" JAVA objects.
11 |
12 | This module concerns itself with the first phase of conversion, while the second phase
13 | is left to PyImageJ's to_java function.
14 |
15 | Notable functions included in the module:
16 | * python_type_of()
17 | - determines an "equivalent" python type for a given SciJava ModuleItem
18 | """
19 |
20 | from typing import Callable, List, Optional, Tuple, Type
21 |
22 | from jpype import JObject
23 | from scyjava import Priority
24 |
25 | from napari_imagej import nij
26 | from napari_imagej.java import jc
27 | from napari_imagej.types.enum_likes import enum_like
28 | from napari_imagej.types.enums import py_enum_for
29 | from napari_imagej.types.type_hints import type_hints
30 | from napari_imagej.widgets.parameter_widgets import widget_supported_java_types
31 |
32 | # List of Module Item Converters, along with their priority
33 | _MODULE_ITEM_CONVERTERS: List[Tuple[Callable, int]] = []
34 |
35 |
36 | def module_item_converter(
37 | priority: int = Priority.NORMAL,
38 | ) -> Callable[["jc.ModuleInfo"], Callable]:
39 | """
40 | A decorator used to register the annotated function among the
41 | available module item converters
42 | :param priority: How much this converter should be prioritized
43 | :return: The annotated function
44 | """
45 |
46 | def converter(func: Callable):
47 | """Registers the annotated function with its priority"""
48 | _MODULE_ITEM_CONVERTERS.append((func, priority))
49 | return func
50 |
51 | return converter
52 |
53 |
54 | def type_hint_for(module_item: "jc.ModuleItem"):
55 | """Returns a python type hint for the passed Java ModuleItem."""
56 | for converter, _ in sorted(
57 | _MODULE_ITEM_CONVERTERS, reverse=True, key=lambda x: x[1]
58 | ):
59 | converted = converter(module_item)
60 | if converted is not None:
61 | return converted
62 | raise ValueError(
63 | (
64 | f"Cannot determine python type hint of {module_item.getType()}. "
65 | "Let us know about the failure at https://forum.image.sc, "
66 | "or file an issue at https://github.com/imagej/napari-imagej!"
67 | )
68 | )
69 |
70 |
71 | def _optional_of(p_type: type, item: "jc.ModuleItem") -> type:
72 | if not p_type:
73 | return p_type
74 | return p_type if item.isRequired() else Optional[p_type]
75 |
76 |
77 | @module_item_converter(priority=Priority.HIGH)
78 | def enum_like_converter(item: "jc.ModuleItem"):
79 | """
80 | Checks to see if this type can be satisfied by a PythonStandin.
81 | For a PythonStandin to work, it MUST be a pure input.
82 | This is because the python type has no functionality, as it is just an Enum choice.
83 | """
84 | if item.isInput() and not item.isOutput():
85 | return _optional_of(enum_like(item.getType(), None), item)
86 |
87 |
88 | @module_item_converter(priority=Priority.HIGH)
89 | def enum_converter(item: "jc.ModuleItem"):
90 | """
91 | Checks to see if this type can be satisfied by an autogenerated Enum
92 | """
93 | t = item.getType()
94 | if not isinstance(t, jc.Class):
95 | t = t.class_
96 | return _optional_of(py_enum_for(t), item)
97 |
98 |
99 | @module_item_converter()
100 | def widget_enabled_java_types(item: "jc.ModuleItem"):
101 | """
102 | Checks to see if this JAVA type is fully supported through magicgui widgets.
103 | This is sometimes done to expose object creation/usage when there ISN'T
104 | a good Python equivalent.
105 | """
106 | if item.isInput() and not item.isOutput():
107 | if item.getType() in widget_supported_java_types():
108 | # TODO: NB: Ideally, we'd return item.getType() here.
109 | # Unfortunately, though, that doesn't work, and I can't figure out why
110 | # due to https://github.com/imagej/napari-imagej/issues/7
111 | # For that reason, we return the Python type JObject instead.
112 | # While this return isn't WRONG, it could be MORE correct.
113 | return JObject
114 |
115 |
116 | def _checkerUsingFunc(
117 | item: "jc.ModuleItem", func: Callable[[Type, Type], bool]
118 | ) -> Optional[Type]:
119 | """
120 | The logic of this checker is as follows:
121 |
122 | type_hints contains a bunch of TypeHints, data classes mapping a Java type
123 | to a corresponding python hint.
124 |
125 | There are 3 cases:
126 | 1) The ModuleItem is a PURE INPUT:
127 | We can satisfy item with an object of python type hint.hint IF its
128 | corresponding java type hint.type can be converted to item's type.
129 | The conversion then goes:
130 | hint.hint -> hint.type -> java_type
131 | 2) The ModuleItem is a PURE OUTPUT:
132 | We can satisfy item with an object of python type hint.hint IF we can convert
133 | java_type into its corresponding java type hint.type. The conversion then goes
134 | java_type -> hint.type -> hint.hint
135 | 3) The ModuleItem is BOTH:
136 | We can satisfy item with ptype IF we satisfy both 1 and 2.
137 | hint.hint -> hint.type -> java_type -> hint.type -> hint.hint
138 |
139 | :param item: the ModuleItem we'd like to convert
140 | :return: the python equivalent of ModuleItem's type, or None if that type
141 | cannot be converted.
142 | """
143 | # Get the type of the Module item
144 | java_type = item.getType()
145 | # Case 1
146 | if item.isInput() and not item.isOutput():
147 | for hint in type_hints():
148 | # can we go from hint.type to java_type?
149 | if func(hint.type, java_type):
150 | return _optional_of(hint.hint, item)
151 | # Case 2
152 | elif item.isOutput() and not item.isInput():
153 | # NB type_pairs is ordered from least to most specific.
154 | for hint in type_hints():
155 | # can we go from java_type to hint.type?
156 | if func(java_type, hint.type):
157 | return _optional_of(hint.hint, item)
158 | # Case 3
159 | elif item.isInput() and item.isOutput():
160 | for hint in type_hints():
161 | # can we go both ways?
162 | if func(hint.type, java_type) and func(java_type, hint.type):
163 | return _optional_of(hint.hint, item)
164 |
165 | # Didn't satisfy any cases!
166 | return None
167 |
168 |
169 | @module_item_converter(priority=Priority.HIGH)
170 | def isEqualChecker(item: "jc.ModuleItem") -> Optional[Type]:
171 | """
172 | Determines whether we have a type hint for this SPECIFIC type.
173 | """
174 |
175 | def isAssignable(from_type, to_type) -> bool:
176 | # Use Types to get the raw type of each
177 | from_raw = jc.Types.raw(from_type)
178 | to_raw = jc.Types.raw(to_type)
179 | return to_raw.equals(from_raw)
180 |
181 | return _checkerUsingFunc(item, isAssignable)
182 |
183 |
184 | @module_item_converter()
185 | def isAssignableChecker(item: "jc.ModuleItem") -> Optional[Type]:
186 | """
187 | Determines whether we can simply cast from ptype to item's type java_type
188 | """
189 |
190 | def isAssignable(from_type, to_type) -> bool:
191 | # Use Types to get the raw type of each
192 | from_raw = jc.Types.raw(from_type)
193 | to_raw = jc.Types.raw(to_type)
194 | return to_raw.isAssignableFrom(from_raw)
195 |
196 | return _checkerUsingFunc(item, isAssignable)
197 |
198 |
199 | @module_item_converter(priority=Priority.LOW)
200 | def canConvertChecker(item: "jc.ModuleItem") -> Optional[Type]:
201 | """
202 | Determines whether imagej can do a conversion from ptype to item's type java_type.
203 | """
204 |
205 | def isAssignable(from_type, to_type) -> bool:
206 | return nij.ij.convert().supports(from_type, to_type)
207 |
208 | return _checkerUsingFunc(item, isAssignable)
209 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/type_hints.py:
--------------------------------------------------------------------------------
1 | """
2 | The definitive set of HARDCODED python type hints for java types.
3 |
4 | The type hints may be concrete types OR strings that can be treated
5 | as forward references.
6 |
7 | Note that many types don't belong here, as they can be determined
8 | in a programmatic way. Those types should be declared elsewhere.
9 |
10 | The hint maps are broken up into sub-maps for convenience and utility.
11 | """
12 |
13 | from dataclasses import dataclass
14 | from functools import lru_cache
15 | from typing import Callable, List, Union
16 |
17 | from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort
18 | from scyjava import Priority
19 |
20 | from napari_imagej.java import jc
21 |
22 |
23 | @dataclass
24 | class TypeHint:
25 | type: type
26 | hint: Union[str, type]
27 | priority: float = Priority.NORMAL
28 |
29 |
30 | HINT_GENERATORS: List[Callable[[], List[TypeHint]]] = []
31 |
32 |
33 | def hint_category(func: Callable[[], List[TypeHint]]) -> Callable[[], List[TypeHint]]:
34 | @lru_cache(maxsize=None)
35 | def inner() -> List[TypeHint]:
36 | # We want the map returned by func...
37 | original: List[TypeHint] = func()
38 | # ...but without any None keys.
39 | # NB the second None avoids the KeyError
40 | return list(filter(lambda hint: hint.type is not None, original))
41 |
42 | HINT_GENERATORS.append(inner)
43 | return inner
44 |
45 |
46 | @lru_cache(maxsize=None)
47 | def type_hints() -> List[TypeHint]:
48 | """
49 | Returns a List of all HARDCODED python type hints for java types,
50 | sorted by priority.
51 | """
52 | types: List[TypeHint] = []
53 | for generator in HINT_GENERATORS:
54 | types.extend(generator())
55 | types.sort(reverse=True, key=lambda hint: hint.priority)
56 | return types
57 |
58 |
59 | @hint_category
60 | def booleans() -> List[TypeHint]:
61 | return [
62 | TypeHint(JBoolean, bool),
63 | TypeHint(jc.Boolean_Arr, List[bool]),
64 | TypeHint(jc.Boolean, bool),
65 | TypeHint(jc.BooleanType, bool, Priority.LOW),
66 | ]
67 |
68 |
69 | @hint_category
70 | def numbers() -> List[TypeHint]:
71 | return [
72 | TypeHint(JByte, int),
73 | TypeHint(jc.Byte, int),
74 | TypeHint(jc.Byte_Arr, List[int]),
75 | TypeHint(JShort, int),
76 | TypeHint(jc.Short, int),
77 | TypeHint(jc.Short_Arr, List[int]),
78 | TypeHint(JInt, int),
79 | TypeHint(jc.Integer, int),
80 | TypeHint(jc.Integer_Arr, List[int]),
81 | TypeHint(JLong, int),
82 | TypeHint(jc.Long, int),
83 | TypeHint(jc.Long_Arr, List[int]),
84 | TypeHint(JFloat, float),
85 | TypeHint(jc.Float, float),
86 | TypeHint(jc.Float_Arr, List[float]),
87 | TypeHint(JDouble, float),
88 | TypeHint(jc.Double, float),
89 | TypeHint(jc.Double_Arr, List[float]),
90 | TypeHint(jc.BigInteger, int),
91 | TypeHint(jc.BigDecimal, float),
92 | TypeHint(jc.IntegerType, int, Priority.LOW),
93 | TypeHint(jc.RealType, float, Priority.LOW - 1),
94 | TypeHint(jc.ComplexType, complex, Priority.LOW - 2),
95 | TypeHint(jc.NumericType, float, Priority.VERY_LOW),
96 | ]
97 |
98 |
99 | @hint_category
100 | def strings() -> List[TypeHint]:
101 | return [
102 | TypeHint(JChar, str),
103 | TypeHint(jc.Character_Arr, str),
104 | TypeHint(jc.Character, str),
105 | TypeHint(jc.String, str),
106 | ]
107 |
108 |
109 | @hint_category
110 | def labels() -> List[TypeHint]:
111 | return [TypeHint(jc.ImgLabeling, "napari.layers.Labels", priority=Priority.HIGH)]
112 |
113 |
114 | @hint_category
115 | def images() -> List[TypeHint]:
116 | return [
117 | TypeHint(
118 | jc.RandomAccessibleInterval, "napari.layers.Image", priority=Priority.LOW
119 | ),
120 | TypeHint(
121 | jc.RandomAccessible, "napari.layers.Image", priority=Priority.VERY_LOW
122 | ),
123 | TypeHint(
124 | jc.IterableInterval, "napari.layers.Image", priority=Priority.VERY_LOW
125 | ),
126 | TypeHint(jc.ImageDisplay, "napari.layers.Image"),
127 | TypeHint(jc.Img, "napari.layers.Image"),
128 | TypeHint(jc.ImgPlus, "napari.layers.Image"),
129 | TypeHint(jc.Dataset, "napari.layers.Image"),
130 | TypeHint(jc.DatasetView, "napari.layers.Image"),
131 | TypeHint(jc.ImagePlus, "napari.layers.Image"),
132 | ]
133 |
134 |
135 | @hint_category
136 | def points() -> List[TypeHint]:
137 | return [
138 | TypeHint(jc.PointMask, "napari.layers.Points"),
139 | TypeHint(jc.RealPointCollection, "napari.layers.Points"),
140 | ]
141 |
142 |
143 | @hint_category
144 | def shapes() -> List[TypeHint]:
145 | return [
146 | TypeHint(jc.Line, "napari.layers.Shapes"),
147 | TypeHint(jc.Box, "napari.layers.Shapes"),
148 | TypeHint(jc.SuperEllipsoid, "napari.layers.Shapes"),
149 | TypeHint(jc.Polygon2D, "napari.layers.Shapes"),
150 | TypeHint(jc.Polyline, "napari.layers.Shapes"),
151 | TypeHint(jc.ROITree, "napari.layers.Shapes"),
152 | ]
153 |
154 |
155 | @hint_category
156 | def surfaces() -> List[TypeHint]:
157 | return [TypeHint(jc.Mesh, "napari.layers.Surface")]
158 |
159 |
160 | @hint_category
161 | def color_tables() -> List[TypeHint]:
162 | return [
163 | TypeHint(jc.ColorTable, "vispy.color.Colormap"),
164 | ]
165 |
166 |
167 | @hint_category
168 | def pd() -> List[TypeHint]:
169 | return [
170 | TypeHint(jc.Table, "pandas.DataFrame"),
171 | ]
172 |
173 |
174 | @hint_category
175 | def paths() -> List[TypeHint]:
176 | return [
177 | TypeHint(jc.Character_Arr, str),
178 | TypeHint(jc.Character, str),
179 | TypeHint(jc.String, str),
180 | TypeHint(jc.File, "pathlib.PosixPath"),
181 | TypeHint(jc.Path, "pathlib.PosixPath"),
182 | ]
183 |
184 |
185 | @hint_category
186 | def enums() -> List[TypeHint]:
187 | return [
188 | TypeHint(jc.Enum, "enum.Enum"),
189 | ]
190 |
191 |
192 | @hint_category
193 | def dates() -> List[TypeHint]:
194 | return [
195 | TypeHint(jc.Date, "datetime.datetime"),
196 | ]
197 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/type_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | A module containing useful functions for operating on python types
3 | """
4 |
5 | from napari_imagej.types import type_hints
6 |
7 |
8 | def _napari_layer_types():
9 | """A hardcoded set of types that should be displayable in napari"""
10 | layer_hints = [
11 | *type_hints.images(),
12 | *type_hints.points(),
13 | *type_hints.shapes(),
14 | *type_hints.surfaces(),
15 | *type_hints.labels(),
16 | ]
17 |
18 | return list(map(lambda hint: hint.type, layer_hints))
19 |
20 |
21 | def displayable_in_napari(data):
22 | """Determines whether data should be displayable in napari"""
23 | return any(filter(lambda x: isinstance(data, x), _napari_layer_types()))
24 |
25 |
26 | def type_displayable_in_napari(type):
27 | """Determines whether an object of the given type could be displayed in napari"""
28 | return any(filter(lambda x: issubclass(type, x), _napari_layer_types()))
29 |
--------------------------------------------------------------------------------
/src/napari_imagej/types/widget_mappings.py:
--------------------------------------------------------------------------------
1 | """
2 | A module used to identify Python widget mappings for given Java parameters.
3 |
4 | Notable functions included in the module:
5 | * preferred_widget_for()
6 | - finds the best widget (as a str) for a ModuleItem
7 | and corresponding python type
8 | """
9 |
10 | from typing import Callable, Dict, Optional, Union, get_args, get_origin
11 |
12 | from jpype import JByte, JDouble, JFloat, JInt, JLong, JShort
13 | from napari.layers import Image
14 |
15 | from napari_imagej.java import jc
16 | from napari_imagej.widgets.parameter_widgets import (
17 | ShapeWidget,
18 | file_widget_for,
19 | number_widget_for,
20 | numeric_type_widget_for,
21 | )
22 |
23 | PREFERENCE_FUNCTIONS = []
24 |
25 |
26 | def _widget_preference(
27 | func: Callable[["jc.ModuleItem", Union[type, str]], Optional[str]],
28 | ) -> Callable[["jc.ModuleItem", Union[type, str]], Optional[str]]:
29 | PREFERENCE_FUNCTIONS.append(func)
30 | return func
31 |
32 |
33 | def _unwrap_optional(type_hint: Union[type, str]) -> Union[type, str]:
34 | origin = get_origin(type_hint)
35 | args = get_args(type_hint)
36 | # If it is an optional - unwrap it
37 | if origin is Union and type(None) in args:
38 | # Find the (first) argument that is not None
39 | for arg in args:
40 | if arg is not None:
41 | return arg
42 | # Otherwise - do nothing
43 | return type_hint
44 |
45 |
46 | def preferred_widget_for(
47 | item: "jc.ModuleItem",
48 | type_hint: Union[type, str],
49 | ) -> Optional[Union[type, str]]:
50 | """
51 | Finds the best MAGICGUI widget for a given SciJava ModuleItem,
52 | and its corresponding Python type
53 |
54 | For ModuleItems with unknown preferences, None is returned.
55 |
56 | :param item: The ModuleItem with a style
57 | :param type_hint: The PYTHON type for the parameter
58 | :return: The best magicgui widget type, if it is known
59 | """
60 | for pref_func in PREFERENCE_FUNCTIONS:
61 | pref = pref_func(item, type_hint)
62 | if pref:
63 | return pref
64 |
65 | return None
66 |
67 |
68 | @_widget_preference
69 | def _numeric_type_preference(
70 | item: "jc.ModuleItem", type_hint: Union[type, str]
71 | ) -> Optional[Union[type, str]]:
72 | if issubclass(item.getType(), jc.NumericType):
73 | return numeric_type_widget_for(item.getType())
74 | return None
75 |
76 |
77 | @_widget_preference
78 | def _number_preference(
79 | item: "jc.ModuleItem", type_hint: Union[type, str]
80 | ) -> Optional[Union[type, str]]:
81 | # Primitives
82 | if item.getType() == JByte:
83 | return number_widget_for(jc.Byte)
84 | if item.getType() == JShort:
85 | return number_widget_for(jc.Short)
86 | if item.getType() == JInt:
87 | return number_widget_for(jc.Integer)
88 | if item.getType() == JLong:
89 | return number_widget_for(jc.Long)
90 | if item.getType() == JFloat:
91 | return number_widget_for(jc.Float)
92 | if item.getType() == JDouble:
93 | return number_widget_for(jc.Double)
94 |
95 | # Boxed primitives
96 | if issubclass(item.getType(), jc.Number):
97 | return number_widget_for(item.getType())
98 | return None
99 |
100 |
101 | @_widget_preference
102 | def _mutable_output_preference(
103 | item: "jc.ModuleItem", type_hint: Union[type, str]
104 | ) -> Optional[Union[type, str]]:
105 | # We only care about mutable outputs
106 | if item.isInput() and item.isOutput():
107 | # If the type hint is an (optional) Image, use MutableOutputWidget
108 | if (
109 | type_hint == "napari.layers.Image"
110 | or type_hint == Image
111 | or type_hint == Optional[Image]
112 | ):
113 | return "napari_imagej.widgets.parameter_widgets.MutableOutputWidget"
114 | # Optional['napari.layers.Image'] is hard to resolve,
115 | # so we use special case logic for it.
116 | # HACK: In Python 3.7 (and maybe 3.8), Optional['napari.layers.Image'] does
117 | # not have the same stringification as it does in Python 3.9+. Thus we have
118 | # to check two strings.
119 | if (
120 | str(type_hint)
121 | == "typing.Union[ForwardRef('napari.layers.Image'), NoneType]"
122 | or str(type_hint) == "typing.Optional[ForwardRef('napari.layers.Image')]"
123 | ):
124 | return "napari_imagej.widgets.parameter_widgets.MutableOutputWidget"
125 |
126 |
127 | @_widget_preference
128 | def _shape_preference(
129 | item: "jc.ModuleItem", type_hint: Union[type, str]
130 | ) -> Optional[Union[type, str]]:
131 | if item.isInput() and not item.isOutput():
132 | if item.getType() == jc.Shape:
133 | return ShapeWidget
134 |
135 |
136 | # The definitive mapping of scijava widget styles to magicgui widget types
137 | _supported_scijava_styles: Dict[str, Dict[type, str]] = {
138 | # ChoiceWidget styles
139 | "listBox": {str: "Select"},
140 | "radioButtonHorizontal": {str: "RadioButtons"},
141 | "radioButtonVertical": {str: "RadioButtons"},
142 | # NumberWidget styles
143 | "slider": {int: "Slider", float: "FloatSlider"},
144 | "spinner": {int: "SpinBox", float: "FloatSpinBox"},
145 | }
146 |
147 |
148 | @_widget_preference
149 | def _scijava_style_preference(
150 | item: "jc.ModuleItem", type_hint: Union[type, str]
151 | ) -> Optional[str]:
152 | style: str = item.getWidgetStyle()
153 | if style not in _supported_scijava_styles:
154 | return None
155 | style_options = _supported_scijava_styles[style]
156 | type_hint = _unwrap_optional(type_hint)
157 | for k, v in style_options.items():
158 | if issubclass(type_hint, k):
159 | return v
160 |
161 |
162 | @_widget_preference
163 | def _scijava_path_preference(
164 | item: "jc.ModuleItem", type_hint: Union[type, str]
165 | ) -> Optional[str]:
166 | if "pathlib.PosixPath" == str(type_hint):
167 | return file_widget_for(item)
168 |
--------------------------------------------------------------------------------
/src/napari_imagej/utilities/event_subscribers.py:
--------------------------------------------------------------------------------
1 | """
2 | Module containing various EventSubscribers used by
3 | napari-imagej functionality
4 | """
5 |
6 | from logging import getLogger
7 |
8 | from jpype import JImplements, JOverride
9 | from qtpy.QtCore import Signal
10 |
11 | from napari_imagej import nij
12 | from napari_imagej.java import jc
13 |
14 |
15 | @JImplements(["org.scijava.event.EventSubscriber"], deferred=True)
16 | class NapariEventSubscriber(object):
17 | @JOverride
18 | def onEvent(self, event):
19 | getLogger("napari-imagej").debug(str(event))
20 |
21 | @JOverride
22 | def getEventClass(self):
23 | return jc.ModuleEvent.class_
24 |
25 | @JOverride
26 | def equals(self, other):
27 | return isinstance(other, NapariEventSubscriber)
28 |
29 |
30 | @JImplements(["org.scijava.event.EventSubscriber"], deferred=True)
31 | class ProgressBarListener(object):
32 | def __init__(self, progress_signal: Signal):
33 | self.progress_signal = progress_signal
34 |
35 | @JOverride
36 | def onEvent(self, event):
37 | self.progress_signal.emit(event)
38 |
39 | @JOverride
40 | def getEventClass(self):
41 | return jc.ModuleEvent.class_
42 |
43 | @JOverride
44 | def equals(self, other):
45 | return isinstance(other, ProgressBarListener)
46 |
47 |
48 | @JImplements(["org.scijava.event.EventSubscriber"], deferred=True)
49 | class UIShownListener(object):
50 | def __init__(self):
51 | self.initialized = False
52 |
53 | @JOverride
54 | def onEvent(self, event):
55 | if not self.initialized:
56 | # add our custom settings to the User Interface
57 | if nij.ij.legacy and nij.ij.legacy.isActive():
58 | self._ij1_UI_setup()
59 | self._ij2_UI_setup(event.getUI())
60 | self.initialized = True
61 |
62 | @JOverride
63 | def getEventClass(self):
64 | return jc.UIShownEvent.class_
65 |
66 | @JOverride
67 | def equals(self, other):
68 | return isinstance(other, UIShownListener)
69 |
70 | def _ij1_UI_setup(self):
71 | """Configure the ImageJ Legacy GUI"""
72 | nij.ij.IJ.getInstance().exitWhenQuitting(False)
73 |
74 | def _ij2_UI_setup(self, ui: "jc.UserInterface"):
75 | """Configure the ImageJ2 Swing GUI behavior"""
76 | # Overwrite the WindowListeners so we control closing behavior
77 | self._kill_window_listeners(self._get_AWT_frame(ui))
78 |
79 | def _get_AWT_frame(self, ui: "jc.UserInterface"):
80 | appFrame = ui.getApplicationFrame()
81 | if isinstance(appFrame, jc.Window):
82 | return appFrame
83 | elif isinstance(appFrame, jc.UIComponent):
84 | return appFrame.getComponent()
85 |
86 | def _kill_window_listeners(self, window):
87 | """Replace the WindowListeners present on window with our own"""
88 | # Remove all preset WindowListeners
89 | for listener in window.getWindowListeners():
90 | window.removeWindowListener(listener)
91 |
92 | # Add our own behavior for WindowEvents
93 | @JImplements("java.awt.event.WindowListener")
94 | class NapariAdapter(object):
95 | @JOverride
96 | def windowOpened(self, event):
97 | pass
98 |
99 | @JOverride
100 | def windowClosing(self, event):
101 | # We don't want to shut down anything, we just want to hide the window.
102 | window.setVisible(False)
103 |
104 | @JOverride
105 | def windowClosed(self, event):
106 | pass
107 |
108 | @JOverride
109 | def windowIconified(self, event):
110 | pass
111 |
112 | @JOverride
113 | def windowDeiconified(self, event):
114 | pass
115 |
116 | @JOverride
117 | def windowActivated(self, event):
118 | pass
119 |
120 | @JOverride
121 | def windowDeactivated(self, event):
122 | pass
123 |
124 | listener = NapariAdapter()
125 | nij.ij.object().addObject(listener)
126 | window.addWindowListener(listener)
127 |
--------------------------------------------------------------------------------
/src/napari_imagej/utilities/events.py:
--------------------------------------------------------------------------------
1 | """
2 | Helper functions for working with the SciJava event bus.
3 | """
4 |
5 |
6 | def subscribe(ij, subscriber):
7 | # NB: We need to retain a reference to this object or GC will delete it.
8 | ij.object().addObject(subscriber)
9 | _event_bus(ij).subscribe(subscriber.getEventClass(), subscriber)
10 |
11 |
12 | def unsubscribe(ij, subscriber):
13 | _event_bus(ij).unsubscribe(subscriber.getEventClass(), subscriber)
14 |
15 |
16 | def subscribers(ij, event_class):
17 | return _event_bus(ij).getSubscribers(event_class)
18 |
19 |
20 | def _event_bus(ij):
21 | # HACK: Tap into the EventBus to obtain SciJava Module debug info.
22 | # See https://github.com/scijava/scijava-common/issues/452
23 | event_bus_field = ij.event().getClass().getDeclaredField("eventBus")
24 | event_bus_field.setAccessible(True)
25 | return event_bus_field.get(ij.event())
26 |
--------------------------------------------------------------------------------
/src/napari_imagej/utilities/progress_manager.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | from napari.utils import progress
4 |
5 | from napari_imagej.java import jc
6 |
7 |
8 | class ModuleProgressManager:
9 | """Generates and updates progress bars for SciJava Modules
10 | The progress bars generated by this class have three checkpoints:
11 |
12 | 1. Preprocessing completed
13 | 2. Processing completed
14 | 3. Postprocessing completed
15 |
16 | update_progress(module) should be called after each of these stages
17 | are complete for Module module.
18 |
19 | init_progress(module) is provided as a separate function, and must be
20 | called from the GUI thread before update_progress is called.
21 | """
22 |
23 | def __init__(self):
24 | self.prog_bars: Dict[jc.Module, progress] = {}
25 |
26 | def init_progress(self, module: "jc.Module"):
27 | prog = progress(
28 | desc=str(module.getInfo().getTitle()),
29 | total=3,
30 | )
31 | self.prog_bars[module] = prog
32 |
33 | def update_progress(self, module: "jc.Module"):
34 | if pbr := self.prog_bars.get(module):
35 | pbr.update()
36 |
37 | def close(self, module: "jc.Module"):
38 | if pbr := self.prog_bars.pop(module, None):
39 | pbr.close()
40 |
41 |
42 | pm = ModuleProgressManager()
43 |
--------------------------------------------------------------------------------
/src/napari_imagej/widgets/info_bar.py:
--------------------------------------------------------------------------------
1 | """
2 | A display showing information relative to the napari-imagej widget
3 | """
4 |
5 | from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
6 |
7 |
8 | class InfoBox(QWidget):
9 | def __init__(self):
10 | super().__init__()
11 | self.setLayout(QVBoxLayout())
12 | # Label for displaying ImageJ version
13 | self.version_bar = QLabel()
14 | self.layout().addWidget(self.version_bar)
15 |
--------------------------------------------------------------------------------
/src/napari_imagej/widgets/layouts.py:
--------------------------------------------------------------------------------
1 | """
2 | Module containing QLayouts used by various napari-imagej widgets
3 | """
4 |
5 | from qtpy.QtCore import QMargins, QPoint, QRect, QSize, Qt
6 | from qtpy.QtWidgets import QLayout, QSizePolicy
7 |
8 |
9 | class QFlowLayout(QLayout):
10 | """
11 | A QLayout that mimics a traditional Flow layout
12 | Copied from Qt for Python's Example:
13 | https://doc.qt.io/qtforpython/examples/example_widgets_layouts_flowlayout.html
14 | """
15 |
16 | def __init__(self, parent=None):
17 | super().__init__(parent)
18 |
19 | if parent is not None:
20 | self.setContentsMargins(QMargins(0, 0, 0, 0))
21 |
22 | self._item_list = []
23 |
24 | def __del__(self):
25 | item = self.takeAt(0)
26 | while item:
27 | item = self.takeAt(0)
28 |
29 | def addItem(self, item):
30 | self._item_list.append(item)
31 |
32 | def count(self):
33 | return len(self._item_list)
34 |
35 | def itemAt(self, index):
36 | if 0 <= index < len(self._item_list):
37 | return self._item_list[index]
38 |
39 | return None
40 |
41 | def takeAt(self, index):
42 | if 0 <= index < len(self._item_list):
43 | return self._item_list.pop(index)
44 |
45 | return None
46 |
47 | def expandingDirections(self):
48 | return Qt.Orientation(0)
49 |
50 | def hasHeightForWidth(self):
51 | return True
52 |
53 | def heightForWidth(self, width):
54 | height = self._do_layout(QRect(0, 0, width, 0), True)
55 | return height
56 |
57 | def setGeometry(self, rect):
58 | super(QFlowLayout, self).setGeometry(rect)
59 | self._do_layout(rect, False)
60 |
61 | def sizeHint(self):
62 | return self.minimumSize()
63 |
64 | def minimumSize(self):
65 | size = QSize()
66 |
67 | for item in self._item_list:
68 | size = size.expandedTo(item.minimumSize())
69 |
70 | size += QSize(
71 | 2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()
72 | )
73 | return size
74 |
75 | def _do_layout(self, rect, test_only):
76 | x = rect.x()
77 | y = rect.y()
78 | line_height = 0
79 | spacing = self.spacing()
80 |
81 | for item in self._item_list:
82 | style = item.widget().style()
83 | layout_spacing_x = style.layoutSpacing(
84 | QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
85 | )
86 | layout_spacing_y = style.layoutSpacing(
87 | QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
88 | )
89 | space_x = spacing + layout_spacing_x
90 | space_y = spacing + layout_spacing_y
91 | next_x = x + item.sizeHint().width() + space_x
92 | if next_x - space_x > rect.right() and line_height > 0:
93 | x = rect.x()
94 | y = y + line_height + space_y
95 | next_x = x + item.sizeHint().width() + space_x
96 | line_height = 0
97 |
98 | if not test_only:
99 | item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
100 |
101 | x = next_x
102 | line_height = max(line_height, item.sizeHint().height())
103 |
104 | return y + line_height - rect.y()
105 |
--------------------------------------------------------------------------------
/src/napari_imagej/widgets/repl.py:
--------------------------------------------------------------------------------
1 | """
2 | A widget that provides access to the SciJava REPL.
3 |
4 | This supports all of the languages of SciJava.
5 | """
6 |
7 | from qtpy.QtGui import QTextCursor
8 | from qtpy.QtWidgets import (
9 | QComboBox,
10 | QHBoxLayout,
11 | QLineEdit,
12 | QTextEdit,
13 | QVBoxLayout,
14 | QWidget,
15 | )
16 |
17 | from napari_imagej.model import NapariImageJ
18 |
19 |
20 | class REPLWidget(QWidget):
21 | def __init__(self, nij: NapariImageJ, parent: QWidget = None):
22 | """
23 | Initialize the REPLWidget.
24 |
25 | :param nij: The NapariImageJ model object to use when evaluating commands.
26 | :param parent: The parent widget (optional).
27 | """
28 | super().__init__(parent)
29 |
30 | layout = QVBoxLayout(self)
31 |
32 | self.output_textedit = QTextEdit(self)
33 | self.output_textedit.setReadOnly(True)
34 | layout.addWidget(self.output_textedit)
35 |
36 | nij.add_repl_callback(lambda s: self.process_output(s))
37 |
38 | h_layout = QHBoxLayout()
39 | layout.addLayout(h_layout)
40 |
41 | self.language_combo = QComboBox(self)
42 | h_layout.addWidget(self.language_combo)
43 |
44 | self.input_lineedit = QLineEdit(self)
45 | self.input_lineedit.returnPressed.connect(self.process_input)
46 | h_layout.addWidget(self.input_lineedit)
47 |
48 | self.script_repl = nij.repl
49 | self.language_combo.addItems(
50 | [str(el) for el in list(self.script_repl.getInterpretedLanguages())]
51 | )
52 | self.language_combo.setCurrentText(
53 | str(self.script_repl.getInterpreter().getLanguage())
54 | )
55 | self.language_combo.currentTextChanged.connect(self.change_language)
56 |
57 | def change_language(self, language: str):
58 | """
59 | Change the active scripting language of the REPL.
60 |
61 | :param language: The new scripting language to use.
62 | """
63 | self.script_repl.lang(language)
64 | self.output_textedit.clear()
65 |
66 | def process_input(self):
67 | """
68 | Process the user input and evaluate it using the REPL.
69 | """
70 | input_text = self.input_lineedit.text()
71 | self.input_lineedit.clear()
72 |
73 | # Evaluate the input using REPL's evaluate method.
74 | self.output_textedit.append(f">>> {input_text}")
75 | self.script_repl.evaluate(input_text)
76 |
77 | def process_output(self, s):
78 | """
79 | Display output given from the REPL in the output text area.
80 | """
81 | self.output_textedit.append(s)
82 |
83 | # Scroll to the bottom of the output text area.
84 | cursor = self.output_textedit.textCursor()
85 | cursor.movePosition(QTextCursor.MoveOperation.End)
86 | self.output_textedit.setTextCursor(cursor)
87 | self.output_textedit.ensureCursorVisible()
88 |
--------------------------------------------------------------------------------
/src/napari_imagej/widgets/result_runner.py:
--------------------------------------------------------------------------------
1 | """
2 | A QWidget designed to run SciJava SearchResult functionality.
3 |
4 | Calls to ResultRunner.select(result) will generate a set of actions that operate
5 | on the provided SciJava SearchResult. These actions will appear as QPushButtons.
6 | """
7 |
8 | from typing import Callable, Dict, List, Union
9 |
10 | from napari import Viewer
11 | from qtpy.QtCore import Qt, Signal
12 | from qtpy.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
13 | from superqt import QElidingLabel
14 |
15 | from napari_imagej.java import jc
16 | from napari_imagej.widgets.layouts import QFlowLayout
17 | from napari_imagej.widgets.widget_utils import python_actions_for
18 |
19 | _action_tooltips: Dict[str, str] = {
20 | "Widget": "Creates a napari widget for executing this command with varying inputs",
21 | "Run": "Runs the command immediately, asking for inputs in a pop-up dialog box",
22 | "Source": "Opens the source code in browser",
23 | "Help": "Opens the functionality's ImageJ.net wiki page",
24 | }
25 |
26 |
27 | class ActionButton(QPushButton):
28 | """
29 | A QPushButton that starts with a function, occuring on click
30 | """
31 |
32 | def __init__(self, name: str, func: Callable[[], None]):
33 | super().__init__()
34 | self.setText(name)
35 | if name in _action_tooltips:
36 | self.setToolTip(_action_tooltips[name])
37 |
38 | self.action = func
39 | self.clicked.connect(self.action)
40 |
41 |
42 | class ResultRunner(QWidget):
43 | def __init__(self, viewer: Viewer, output_signal: Signal):
44 | super().__init__()
45 | self.viewer = viewer
46 | self.output_signal = output_signal
47 |
48 | self.setLayout(QVBoxLayout())
49 |
50 | self.selected_module_label = QElidingLabel()
51 | self.layout().addWidget(self.selected_module_label)
52 | self.button_pane = QWidget()
53 | self.button_pane.setLayout(QFlowLayout())
54 | self.layout().addWidget(self.button_pane)
55 |
56 | def select(self, result: "jc.SearchResult"):
57 | """Selects result, displaying its name and its actions as buttons"""
58 | # First, remove the old information
59 | self.clear()
60 |
61 | # Then, set the label
62 | self._setText(result.name())
63 |
64 | # Finally, create buttons for each action
65 | for button in self._buttons_for(result):
66 | self.button_pane.layout().addWidget(button)
67 |
68 | def clear(self):
69 | """Clears the current selection"""
70 | self._setText("")
71 | # Remove all old buttons
72 | for child in self.button_pane.children():
73 | if isinstance(child, QPushButton):
74 | child.deleteLater()
75 |
76 | def run(self, result: "jc.SearchResult"):
77 | """
78 | Runs an action of the provided SearchResult.
79 | The action chosen depends on keyboard modifiers.
80 | By default, the highest-priority action is run.
81 | Using SHIFT, the second-highest action is run.
82 | :param result: The selected SearchResult
83 | """
84 | buttons: List[ActionButton] = self._buttons_for(result)
85 | # Run the first action UNLESS Shift is also pressed.
86 | # If so, run the second action
87 | if len(buttons) > 0:
88 | if len(buttons) > 1 and QApplication.keyboardModifiers() & Qt.ShiftModifier:
89 | buttons[1].action()
90 | else:
91 | buttons[0].action()
92 |
93 | # -- HELPER FUNCTIONALITY -- #
94 |
95 | def _setText(self, text: Union[str, "jc.String"]):
96 | """
97 | Sets the text of this widget's QLabel.
98 | """
99 | if text:
100 | self.selected_module_label.show()
101 | # NB Java strings need to be converted explicitly
102 | self.selected_module_label.setText(str(text))
103 | else:
104 | self.selected_module_label.hide()
105 |
106 | def _buttons_for(self, result: "jc.SearchResult") -> List[ActionButton]:
107 | return [
108 | ActionButton(*a)
109 | for a in python_actions_for(result, self.output_signal, self)
110 | ]
111 |
--------------------------------------------------------------------------------
/src/napari_imagej/widgets/searchbar.py:
--------------------------------------------------------------------------------
1 | """
2 | A QWidget used to provide input to SciJava Searchers.
3 |
4 | The bar is disabled until ImageJ is ready. This ensures the SciJava Searchers
5 | are ready to accept queries.
6 | """
7 |
8 | from qtpy.QtCore import Qt, Signal
9 | from qtpy.QtWidgets import QHBoxLayout, QLineEdit, QWidget
10 |
11 |
12 | class JVMEnabledSearchbar(QWidget):
13 | """
14 | A QWidget for streamlining ImageJ functionality searching
15 | """
16 |
17 | def __init__(
18 | self,
19 | ):
20 | super().__init__()
21 |
22 | # The main functionality is a search bar
23 | self.bar: JLineEdit = JLineEdit()
24 |
25 | # Set GUI options
26 | self.setLayout(QHBoxLayout())
27 | self.layout().addWidget(self.bar)
28 |
29 | def finalize(self):
30 | self.bar.finalize()
31 |
32 |
33 | class JLineEdit(QLineEdit):
34 | """
35 | A QLineEdit that is disabled until the JVM is ready
36 | """
37 |
38 | # Signal that identifies a down arrow pressed
39 | floatBelow = Signal()
40 |
41 | def __init__(self):
42 | super().__init__()
43 |
44 | # Set QtPy properties
45 | self.setText("Initializing ImageJ...Please Wait")
46 | self.setEnabled(False)
47 |
48 | def keyPressEvent(self, event):
49 | if event.key() == Qt.Key_Down:
50 | self.floatBelow.emit()
51 | else:
52 | super().keyPressEvent(event)
53 |
54 | def finalize(self):
55 | # Once the JVM is ready, allow editing
56 | self.setText("")
57 | self.setEnabled(True)
58 |
59 | def finalize_on_error(self):
60 | # If ImageJ errors
61 | self.setText("Error: Invalid ImageJ2")
62 | self.setEnabled(False)
63 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imagej/napari-imagej/fb980a8c70d126fbb9bc26f2c10f8c59fdf9bff4/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | A module containing pytest configuration and globally-used fixtures
3 | """
4 |
5 | import os
6 | import sys
7 | from typing import Callable, Generator
8 |
9 | import pytest
10 | from napari import Viewer
11 |
12 | from napari_imagej import nij, settings
13 | from napari_imagej.widgets.menu import NapariImageJMenu
14 | from napari_imagej.widgets.napari_imagej import NapariImageJWidget
15 |
16 | actual_settings_is_macos = settings._is_macos
17 |
18 |
19 | @pytest.fixture()
20 | def asserter(qtbot) -> Callable[[Callable[[], bool]], None]:
21 | """Wraps qtbot.waitUntil with a standardized timeout"""
22 |
23 | # Determine timeout length - defaults to 5
24 | timeout = int(os.environ.get("NAPARI_IMAGEJ_TEST_TIMEOUT", "5000"))
25 |
26 | # Define timeout function
27 | def assertFunc(func: Callable[[], bool]):
28 | # Let things run for up to a minute
29 | qtbot.waitUntil(func, timeout=timeout)
30 |
31 | # Return the timeout function
32 | return assertFunc
33 |
34 |
35 | @pytest.fixture(autouse=True)
36 | def install_default_settings():
37 | """Fixture ensuring any changes made earlier to the settings are reversed"""
38 | settings._is_macos = actual_settings_is_macos
39 | settings.load(False)
40 |
41 |
42 | @pytest.fixture(scope="session")
43 | def ij():
44 | """Fixture providing the ImageJ2 Gateway"""
45 | # BIG HACK: We run into the issue described in
46 | # https://github.com/imagej/pyimagej/issues/197
47 | # if we don't add this.
48 | if sys.platform == "darwin":
49 | viewer = Viewer()
50 | ij = nij.ij
51 | viewer.close()
52 | else:
53 | ij = nij.ij
54 |
55 | yield ij
56 |
57 | ij.context().dispose()
58 |
59 |
60 | @pytest.fixture()
61 | def viewer(make_napari_viewer) -> Generator[Viewer, None, None]:
62 | """Fixture providing a napari Viewer"""
63 | yield make_napari_viewer()
64 |
65 |
66 | @pytest.fixture
67 | def imagej_widget(viewer, asserter) -> Generator[NapariImageJWidget, None, None]:
68 | """Fixture providing an ImageJWidget"""
69 | # Create widget
70 | ij_widget: NapariImageJWidget = NapariImageJWidget(viewer)
71 | # Wait for imagej to be initialized
72 | ij_widget.wait_for_finalization()
73 |
74 | yield ij_widget
75 |
76 | # Cleanup -> Close the widget, trigger ImageJ shutdown
77 | ij_widget.close()
78 |
79 |
80 | @pytest.fixture
81 | def gui_widget(viewer) -> Generator[NapariImageJMenu, None, None]:
82 | """
83 | Fixture providing a GUIWidget. The returned widget will use active layer selection
84 | """
85 |
86 | # Define GUIWidget settings for this particular feature.
87 | # In particular, we want to enforce active layer selection
88 | settings.use_active_layer = True
89 |
90 | # Create widget
91 | widget: NapariImageJMenu = NapariImageJMenu(viewer)
92 |
93 | # Wait for ImageJ initialization
94 | _ = nij.ij
95 |
96 | # Finalize widget
97 | widget.finalize()
98 |
99 | yield widget
100 |
101 | # Cleanup -> Close the widget, trigger ImageJ shutdown
102 | widget.close()
103 |
--------------------------------------------------------------------------------
/tests/test_java.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from scyjava import get_version, is_version_at_least, jimport
4 |
5 | from napari_imagej import settings
6 | from napari_imagej.java import _validate_imagej, minimum_versions
7 | from tests.utils import jc
8 |
9 | version_checks = {
10 | "io.scif:scifio": "io.scif.SCIFIO",
11 | "net.imagej:ij": "ij.ImagePlus",
12 | "net.imagej:imagej": "net.imagej.Main",
13 | "net.imagej:imagej-common": "net.imagej.Dataset",
14 | "net.imagej:imagej-legacy": "net.imagej.legacy.LegacyService",
15 | "net.imagej:imagej-ops": "net.imagej.ops.OpService",
16 | "net.imglib2:imglib2-imglyb": "net.imglib2.python.ReferenceGuardingRandomAccessibleInterval", # noqa: E501
17 | "net.imglib2:imglib2-unsafe": "net.imglib2.img.unsafe.UnsafeImg",
18 | "org.scijava:scijava-common": "org.scijava.Context",
19 | "org.scijava:scijava-search": "org.scijava.search.Searcher",
20 | "sc.fiji:fiji": "sc.fiji.Main",
21 | }
22 |
23 |
24 | def test_java_components(ij):
25 | """
26 | Assert that Java components are present, and print their versions.
27 | """
28 | print()
29 | print("======= BEGIN JAVA VERSIONS =======")
30 |
31 | for coord, class_name in version_checks.items():
32 | try:
33 | jcls = jimport(class_name)
34 | except Exception:
35 | jcls = None
36 |
37 | if jcls is None:
38 | version = "NOT PRESENT"
39 | else:
40 | version = get_version(jcls)
41 | if coord in minimum_versions:
42 | assert is_version_at_least(version, minimum_versions[coord])
43 | else:
44 | version += " (no minimum)"
45 |
46 | print(f"{coord} {version}")
47 |
48 | print("======== END JAVA VERSIONS ========")
49 |
50 |
51 | def test_endpoint(ij):
52 | endpoints: List[str] = settings.endpoint().split("+")
53 | for endpoint in endpoints:
54 | gav = endpoint.split(":")
55 | if len(gav) > 2:
56 | ga = ":".join(gav[:2])
57 | if ga in version_checks:
58 | version = gav[2]
59 | exp_version = get_version(jimport(version_checks[ga]))
60 | assert is_version_at_least(version, exp_version)
61 |
62 |
63 | def test_recommended_version(ij):
64 | # Save old recommended versions
65 | import napari_imagej.java
66 |
67 | existing_recommendations = napari_imagej.java.recommended_versions
68 | napari_imagej.java.recommended_versions = {"org.scijava:scijava-common": "999.0.0"}
69 |
70 | # Setup log handler to capture warning
71 | import io
72 | import logging
73 |
74 | log_capture_string = io.StringIO()
75 | ch = logging.StreamHandler(log_capture_string)
76 | ch.setLevel(logging.WARN)
77 | logging.getLogger("napari-imagej").addHandler(ch)
78 | # Validate ImageJ - capture lower-than-recommended version
79 | _validate_imagej(ij)
80 | log_contents = log_capture_string.getvalue()
81 | log_capture_string.close()
82 | # Assert warning given
83 | nij_version = get_version("napari-imagej")
84 | sjc_version = get_version(jc.Module)
85 | assert log_contents == (
86 | f"napari-imagej v{nij_version} recommends org.scijava:scijava-common version "
87 | f"999.0.0 (Installed: {sjc_version})\n"
88 | )
89 |
90 | # restore recommended versions
91 | napari_imagej.java.recommended_versions = existing_recommendations
92 | logging.getLogger("napari-imagej").removeHandler(ch)
93 |
--------------------------------------------------------------------------------
/tests/test_scripting.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing script discovery and wrapping
3 | """
4 |
5 | from magicgui import magicgui
6 | from scyjava import JavaClasses
7 |
8 | from napari_imagej.utilities._module_utils import functionify_module_execution
9 |
10 |
11 | class JavaClassesTest(JavaClasses):
12 | """
13 | Here we override JavaClasses to get extra test imports
14 | """
15 |
16 | @JavaClasses.java_import
17 | def DefaultModuleService(self):
18 | return "org.scijava.module.DefaultModuleService"
19 |
20 |
21 | jc = JavaClassesTest()
22 |
23 |
24 | def test_example_script_exists(ij):
25 | """
26 | Asserts that Example_Script.py in the local scripts/examples directory can be found
27 | from the module service, AND that it can be converted into a magicgui widget.
28 | """
29 | module_info = ij.module().getModuleById("script:examples/Example_Script.py")
30 | assert module_info is not None
31 |
32 | module = ij.module().createModule(module_info)
33 |
34 | func, magic_kwargs = functionify_module_execution(None, module, module_info)
35 |
36 | # Normally we'd assign the call to a variable, but we aren't using it..
37 | magicgui(function=func, **magic_kwargs)
38 |
--------------------------------------------------------------------------------
/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari-imagej settings
3 | """
4 |
5 | from scyjava import jimport
6 |
7 | from napari_imagej import settings
8 |
9 |
10 | def test_java_cli_args():
11 | """
12 | Assert that cli arguments defined in confuse settings are passed to the JVM.
13 | The foo system property is set in pyproject.toml's pytest section
14 | """
15 | assert jimport("java.lang.System").getProperty("foo") == "bar"
16 |
17 |
18 | def test_validate_imagej_base_directory():
19 | """
20 | Assert that non-existent imagej_base_directory is noticed by the validate function.
21 | """
22 | settings.imagej_base_directory = "a-file-path-that-is-unlikely-to-exist"
23 | settings._is_macos = False
24 |
25 | errors = validation_errors()
26 | assert len(errors) == 1
27 | assert errors[0].startswith("ImageJ base directory is not a valid directory.")
28 |
29 |
30 | def test_validate_enable_imagej_gui():
31 | """
32 | Assert that enable_imagej_gui=True on macOS is noticed by the validate function.
33 | """
34 | settings.enable_imagej_gui = True
35 | settings._is_macos = True
36 |
37 | errors = validation_errors()
38 | assert len(errors) == 1
39 | assert errors[0].startswith("The ImageJ GUI is not available on macOS systems.")
40 |
41 |
42 | def test_validate_multiple_problems():
43 | """
44 | Assert that multiple issues are reported as expected by the validate function.
45 | """
46 | settings.imagej_base_directory = "another-file-path-that-is-unlikely-to-exist"
47 | settings.enable_imagej_gui = True
48 | settings._is_macos = True
49 |
50 | errors = validation_errors()
51 | assert len(errors) == 2
52 | assert errors[0].startswith("ImageJ base directory is not a valid directory.")
53 | assert errors[1].startswith("The ImageJ GUI is not available on macOS systems.")
54 |
55 |
56 | def test_validate_default_settings_not_macos():
57 | """
58 | Assert that the validate function succeeds with default settings on non-Mac
59 | """
60 | settings._is_macos = False
61 |
62 | errors = validation_errors()
63 | assert len(errors) == 0
64 |
65 |
66 | def test_validate_default_settings_macos():
67 | """
68 | Assert that the validate function yields a warnig with default settings on Mac
69 | """
70 | settings._is_macos = True
71 |
72 | errors = validation_errors()
73 | assert len(errors) == 1
74 | assert errors[0].startswith("The ImageJ GUI is not available on macOS systems.")
75 |
76 |
77 | def validation_errors():
78 | try:
79 | settings.validate()
80 | except ValueError as e:
81 | return e.args
82 | return []
83 |
--------------------------------------------------------------------------------
/tests/types/test_enums.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | from napari_imagej.types.enums import py_enum_for
4 | from tests.utils import jc
5 |
6 |
7 | def test_py_enum_for():
8 | """
9 | A regression test ensuring that py_enum_for produces a Python Enum
10 | equivalent to SciJava Common's ItemIO Enum.
11 | """
12 | java_enum = jc.ItemIO
13 | py_enum = py_enum_for(java_enum.class_)
14 | assert issubclass(py_enum, Enum)
15 | for p, j in zip(py_enum, java_enum.values()):
16 | assert p.name == str(j.toString())
17 | assert p.value == j
18 |
--------------------------------------------------------------------------------
/tests/types/test_trackmate.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.types.converters.trackmate
3 | """
4 |
5 | from typing import Tuple
6 |
7 | import numpy as np
8 | import pytest
9 | from napari.layers import Labels, Tracks
10 | from scyjava import JavaClasses
11 |
12 | from napari_imagej import settings
13 | from napari_imagej.types.converters.trackmate import TrackMateClasses, trackmate_present
14 |
15 |
16 | class TestTrackMateClasses(TrackMateClasses):
17 | @JavaClasses.java_import
18 | def DisplaySettings(self):
19 | return "fiji.plugin.trackmate.gui.displaysettings.DisplaySettings"
20 |
21 | @JavaClasses.java_import
22 | def HyperStackDisplayer(self):
23 | return "fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer"
24 |
25 | @JavaClasses.java_import
26 | def SelectionModel(self):
27 | return "fiji.plugin.trackmate.SelectionModel"
28 |
29 |
30 | jc = TestTrackMateClasses()
31 |
32 |
33 | TESTING_LEGACY: bool = settings.include_imagej_legacy
34 |
35 |
36 | @pytest.fixture
37 | def trackMate_example(ij):
38 | if not (TESTING_LEGACY and trackmate_present()):
39 | pytest.skip("TrackMate functionality requires ImageJ and TrackMate!")
40 |
41 | trackMate: jc.TrackMate = jc.TrackMate()
42 | model: jc.Model = trackMate.getModel()
43 | # Build track 1 with 5 spots
44 | s1 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S1")
45 | s2 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S2")
46 | s3 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S3")
47 | s4 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S4")
48 | s5a = jc.Spot(-1.0, 0.0, 0.0, 1.0, -1.0, "S5a")
49 | s6a = jc.Spot(-1.0, 0.0, 0.0, 1.0, -1.0, "S6a")
50 | s5b = jc.Spot(1.0, 1.0, 1.0, 1.0, -1.0, "S5b")
51 | s6b = jc.Spot(1.0, 1.0, 1.0, 1.0, -1.0, "S6b")
52 | # Build track 2 with 2 spots
53 | s7 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S7")
54 | s8 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S8")
55 | # Build track 3 with 2 spots
56 | s9 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S9")
57 | s10 = jc.Spot(0.0, 0.0, 0.0, 1.0, -1.0, "S10")
58 |
59 | model.beginUpdate()
60 | try:
61 | model.addSpotTo(s1, jc.Integer(0))
62 | model.addSpotTo(s2, jc.Integer(1))
63 | model.addSpotTo(s3, jc.Integer(2))
64 | model.addSpotTo(s4, jc.Integer(3))
65 | model.addSpotTo(s5a, jc.Integer(4))
66 | model.addSpotTo(s5b, jc.Integer(4))
67 | model.addSpotTo(s6a, jc.Integer(5))
68 | model.addSpotTo(s6b, jc.Integer(5))
69 | model.addEdge(s1, s2, 0.0)
70 | model.addEdge(s2, s3, 0.0)
71 | model.addEdge(s3, s4, 0.0)
72 | model.addEdge(s4, s5a, 0.0)
73 | model.addEdge(s4, s5b, 0.0)
74 | model.addEdge(s5a, s6a, 0.0)
75 | model.addEdge(s5b, s6b, 0.0)
76 |
77 | model.addSpotTo(s7, jc.Integer(0))
78 | model.addSpotTo(s8, jc.Integer(1))
79 | model.addEdge(s7, s8, 0.0)
80 |
81 | model.addSpotTo(s9, jc.Integer(0))
82 | model.addSpotTo(s10, jc.Integer(1))
83 | model.addEdge(s9, s10, 0.0)
84 | finally:
85 | model.endUpdate()
86 |
87 | ij.object().addObject(trackMate)
88 | return trackMate
89 |
90 |
91 | def test_Model_to_Tracks(asserter, ij, trackMate_example):
92 | if not (TESTING_LEGACY and trackmate_present()):
93 | pytest.skip("TrackMate functionality requires ImageJ and TrackMate!")
94 | # Convert the TrackMate instance into a Dataset with Rois
95 | # this is what we'd convert in practice.
96 | model = trackMate_example.getModel()
97 | selection_model = jc.SelectionModel(model)
98 | imp = ij.convert().convert(ij.py.to_java(np.zeros((10, 10))), jc.ImagePlus)
99 | display_settings = jc.DisplaySettings()
100 |
101 | hyper_stack_displayer = jc.HyperStackDisplayer(
102 | model, selection_model, imp, display_settings
103 | )
104 | hyper_stack_displayer.render()
105 |
106 | dataset = ij.convert().convert(imp, jc.Dataset)
107 |
108 | # Convert the dataset's rois into its Python equivalent
109 | layers = ij.py.from_java(dataset.getProperties()["rois"])
110 | # Assert we receive a Tracks Layer
111 | assert isinstance(layers, Tuple)
112 | tracks, labels = layers
113 | assert isinstance(tracks, Tracks)
114 | assert isinstance(labels, Labels)
115 | # Assert there are 5 branches
116 | assert len(tracks.graph) == 5
117 | # Assert that tracks 1 and 2 split from track 0
118 | assert tracks.graph == {0: [], 1: [0], 2: [0], 3: [], 4: []}
119 | expected_data = np.array(
120 | [
121 | [0.0, 0.0, 0.0, 0.0], # S1
122 | [0.0, 1.0, 0.0, 0.0], # S2
123 | [0.0, 2.0, 0.0, 0.0], # S3
124 | [0.0, 3.0, 0.0, 0.0], # S4
125 | [1.0, 4.0, 0.0, -1.0], # S5a
126 | [1.0, 5.0, 0.0, -1.0], # S5b
127 | [2.0, 4.0, 1.0, 1.0], # S6a
128 | [2.0, 5.0, 1.0, 1.0], # S6b
129 | [3.0, 0.0, 0.0, 0.0], # S7
130 | [3.0, 1.0, 0.0, 0.0], # S8
131 | [4.0, 0.0, 0.0, 0.0], # S9
132 | [4.0, 1.0, 0.0, 0.0],
133 | ]
134 | ) # S11
135 |
136 | assert np.array_equal(tracks.data, expected_data)
137 |
138 | # Remove the layer generated by this test
139 | results = ij.WindowManager.getCurrentImage()
140 | results.changes = False
141 | results.close()
142 |
--------------------------------------------------------------------------------
/tests/types/test_type_conversions.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.types.type_conversions
3 | """
4 |
5 | from typing import List
6 |
7 | import pytest
8 | from jpype import JObject
9 |
10 | from napari_imagej.types.enum_likes import OutOfBoundsFactory
11 | from napari_imagej.types.type_hints import type_hints
12 | from napari_imagej.utilities import _module_utils
13 | from tests.utils import DummyModuleItem, jc
14 |
15 |
16 | def test_direct_match_pairs():
17 | for hint in type_hints():
18 | # Test that jtype inputs convert to ptype inputs
19 | input_item = DummyModuleItem(jtype=hint.type, isInput=True, isOutput=False)
20 | assert _module_utils.type_hint_for(input_item) == hint.hint
21 | # Test that jtype outputs convert to ptype outputs
22 | output_item = DummyModuleItem(jtype=hint.type, isInput=False, isOutput=True)
23 | assert _module_utils.type_hint_for(output_item) == hint.hint
24 | # Test that jtype boths convert to ptype boths
25 | IO_item = DummyModuleItem(jtype=hint.type, isInput=True, isOutput=True)
26 | assert _module_utils.type_hint_for(IO_item) == hint.hint
27 |
28 |
29 | def test_assignable_match_pairs():
30 | hint_domain = list(filter(lambda hint: hint.type, type_hints()))
31 | # Suppose you need a hint for a Java parameter a that is NOT in hint_map,
32 | # but type b <: a IS in hint_map. That hint would apply then to inputs,
33 | # due to argument covariance.
34 | #
35 | # For example, suppose you need the type hint for EuclidenSpace. It is not in the
36 | # hint map, BUT Img is. Since an Img IS a EuclideanSpace, the type hint would also
37 | # apply to EuclideanSpace INPUTS because of argument covariance.
38 | assert jc.EuclideanSpace not in hint_domain
39 | input_item = DummyModuleItem(jtype=jc.EuclideanSpace, isInput=True, isOutput=False)
40 | assert _module_utils.type_hint_for(input_item) == "napari.layers.Labels"
41 |
42 | # Suppose you need a hint for a Java parameter a that is NOT in hint_map,
43 | # but type b :> a IS in hint_map. That hint would apply then to returns,
44 | # due to return contravariance.
45 | #
46 | # For example, suppose you need the type hint for ArrayImg. It is not in the
47 | # hint map, BUT Img is. Since an ArrayImg IS an Img, the type hint would also
48 | # apply to ArrayImg OUTPUTS because of return contravariance.
49 | assert jc.ArrayImg not in hint_domain
50 | output_item = DummyModuleItem(jtype=jc.ArrayImg, isInput=False, isOutput=True)
51 | assert _module_utils.type_hint_for(output_item) == "napari.layers.Image"
52 |
53 |
54 | def test_convertible_match_pairs():
55 | hint_domain = list(map(lambda hint: hint.type, type_hints()))
56 | # We want to test that napari could tell that a DoubleArray ModuleItem
57 | # could be satisfied by a List[float], as napari-imagej knows how to
58 | # convert that List[float] into a Double[], and imagej knows how to
59 | # convert that Double[] into a DoubleArray. Unfortunately, DefaultConverter
60 | # can convert Integers into DoubleArrays; because it comes first in
61 | # TypeMappings, it is the python type that returns.
62 | # This is really not napari-imagej's fault.
63 | # Since the goal was just to test that python_type_of uses ij.convert()
64 | # as an option, we will leave the conversion like this.
65 | assert jc.DoubleArray not in hint_domain
66 | input_item = DummyModuleItem(jtype=jc.DoubleArray, isInput=True, isOutput=False)
67 | assert _module_utils.type_hint_for(input_item) == "napari.layers.Labels"
68 |
69 | # We want to test that napari could tell that a DoubleArray ModuleItem
70 | # could be satisfied by a List[float], as napari-imagej knows how to
71 | # convert that List[float] into a Double[], and imagej knows how to
72 | # convert that Double[] into a DoubleArray. Unfortunately, DefaultConverter
73 | # can convert Boolean[]s into DoubleArrays; because it comes first in
74 | # TypeMappings, it is the python type that returns.
75 | # This is really not napari-imagej's fault.
76 | # Since the goal was just to test that python_type_of uses ij.convert()
77 | # as an option, we will leave the conversion like this.
78 | assert jc.DoubleArray not in hint_domain
79 | input_item = DummyModuleItem(jtype=jc.DoubleArray, isInput=False, isOutput=True)
80 | assert _module_utils.type_hint_for(input_item) == List[bool]
81 |
82 | # Test that a java both NOT in ptypes but convertible to/from some type in ptypes
83 | # gets converted to a ptype
84 | assert jc.DoubleArray not in hint_domain
85 | input_item = DummyModuleItem(jtype=jc.DoubleArray, isInput=True, isOutput=True)
86 | assert _module_utils.type_hint_for(input_item) == List[bool]
87 |
88 |
89 | def test_python_type_of_enum_like_IO():
90 | # Test that a pure input matches
91 | module_item = DummyModuleItem(
92 | jtype=jc.OutOfBoundsFactory, isInput=True, isOutput=False
93 | )
94 | assert _module_utils.type_hint_for(module_item) == OutOfBoundsFactory
95 |
96 | # Test that a mutable input does not match
97 | module_item._isOutput = True
98 | try:
99 | _module_utils.type_hint_for(module_item)
100 | pytest.fail()
101 | except ValueError:
102 | pass
103 |
104 | # Test that a pure output does not match the enum - it matches Boolean[] instead
105 | # because the DefaultConverter can convert an OutOfBoundsFactory into a Boolean[].
106 | module_item._isInput = False
107 | assert _module_utils.type_hint_for(module_item) == List[bool]
108 |
109 |
110 | def test_enum():
111 | p_type = _module_utils.type_hint_for(DummyModuleItem(jtype=jc.ItemIO))
112 | assert p_type.__name__ == "ItemIO"
113 |
114 |
115 | def test_shape():
116 | p_type = _module_utils.type_hint_for(DummyModuleItem(jtype=jc.Shape))
117 | assert p_type == JObject
118 |
--------------------------------------------------------------------------------
/tests/types/test_widget_mappings.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.types.widget_mappings
3 | """
4 |
5 | from typing import Optional
6 |
7 | import magicgui
8 | import napari
9 | import pytest
10 | from magicgui.widgets import (
11 | FloatSlider,
12 | FloatSpinBox,
13 | RadioButtons,
14 | Select,
15 | Slider,
16 | SpinBox,
17 | )
18 | from napari.layers import Image
19 |
20 | from napari_imagej.types.widget_mappings import (
21 | _supported_scijava_styles,
22 | preferred_widget_for,
23 | )
24 | from napari_imagej.utilities._module_utils import type_hint_for
25 | from napari_imagej.widgets.parameter_widgets import (
26 | DirectoryWidget,
27 | MutableOutputWidget,
28 | OpenFileWidget,
29 | SaveFileWidget,
30 | ShapeWidget,
31 | )
32 | from tests.utils import DummyModuleItem, jc
33 |
34 |
35 | def _assert_widget_matches_item(item, type_hint, widget_type, widget_class):
36 | # We only need item for the getWidgetStyle() function
37 | actual = preferred_widget_for(item, type_hint)
38 | assert widget_type == actual
39 |
40 | def func(foo):
41 | print(foo, "bar")
42 |
43 | func.__annotation__ = {"foo": type_hint}
44 |
45 | widget = magicgui.magicgui(
46 | function=func, call_button=False, foo={"widget_type": actual}
47 | )
48 | assert len(widget._list) == 1
49 | assert isinstance(widget._list[0], widget_class)
50 |
51 |
52 | parameterizations = [
53 | ("listBox", str, "Select", Select),
54 | ("radioButtonHorizontal", str, "RadioButtons", RadioButtons),
55 | ("radioButtonVertical", str, "RadioButtons", RadioButtons),
56 | ("slider", int, "Slider", Slider),
57 | ("slider", float, "FloatSlider", FloatSlider),
58 | ("spinner", int, "SpinBox", SpinBox),
59 | ("spinner", float, "FloatSpinBox", FloatSpinBox),
60 | ]
61 |
62 |
63 | @pytest.mark.parametrize(
64 | argnames=["style", "type_hint", "widget_type", "widget_class"],
65 | argvalues=parameterizations,
66 | )
67 | def test_preferred_widget_for(style, type_hint, widget_type, widget_class):
68 | """
69 | Tests that a style and type are mapped to the corresponding widget_class
70 | :param style: the SciJava style
71 | :param type_hint: the PYTHON type of a parameter
72 | :param widget_type: the name of a magicgui widget
73 | :param widget_class: the class corresponding to name
74 | """
75 | item: DummyModuleItem = DummyModuleItem()
76 | item.setWidgetStyle(style)
77 | _assert_widget_matches_item(item, type_hint, widget_type, widget_class)
78 | # let's also check optional parameter
79 | item: DummyModuleItem = DummyModuleItem(isRequired=False)
80 | item.setWidgetStyle(style)
81 | _assert_widget_matches_item(item, Optional[type_hint], widget_type, widget_class)
82 |
83 |
84 | @pytest.mark.parametrize(
85 | "type_hint",
86 | ["napari.layers.Image", Image, Optional["napari.layers.Image"], Optional[Image]],
87 | )
88 | def test_preferred_widget_for_parameter_widgets(type_hint):
89 | # MutableOutputWidget
90 | item: DummyModuleItem = DummyModuleItem(
91 | jtype=jc.ArrayImg, isInput=True, isOutput=True
92 | )
93 | widget_type = "napari_imagej.widgets.parameter_widgets.MutableOutputWidget"
94 | _assert_widget_matches_item(item, type_hint, widget_type, MutableOutputWidget)
95 |
96 |
97 | def test_all_styles_in_parameterizations():
98 | """
99 | Tests that all style mappings declared in supported_styles
100 | are tested in test_widget_for_style_and_type
101 | """
102 | _parameterizations = [p[:-1] for p in parameterizations]
103 | all_styles = []
104 | for style in _supported_scijava_styles:
105 | for type_hint, widget_type in _supported_scijava_styles[style].items():
106 | all_styles.append((style, type_hint, widget_type))
107 | assert all_styles == _parameterizations
108 |
109 |
110 | @pytest.mark.parametrize(
111 | ["style_str", "widget_type"],
112 | [
113 | ("OPEN_STYLE", OpenFileWidget),
114 | ("SAVE_STYLE", SaveFileWidget),
115 | ("DIRECTORY_STYLE", DirectoryWidget),
116 | ],
117 | )
118 | def test_file_widget(style_str, widget_type):
119 | item = DummyModuleItem(jtype=jc.File)
120 | # Get the SciJava style from the string
121 | style = getattr(jc.FileWidget, style_str)
122 | item.setWidgetStyle(style)
123 |
124 | type_hint = type_hint_for(item)
125 | _assert_widget_matches_item(item, type_hint, widget_type, widget_type)
126 |
127 |
128 | def test_shape_widget():
129 | item = DummyModuleItem(jtype=jc.Shape)
130 | type_hint = type_hint_for(item)
131 | _assert_widget_matches_item(item, type_hint, ShapeWidget, ShapeWidget)
132 |
--------------------------------------------------------------------------------
/tests/utilities/test_progress.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from napari.utils import progress
3 |
4 | from napari_imagej.utilities.progress_manager import pm
5 | from napari_imagej.widgets.widget_utils import JavaErrorMessageBox
6 | from tests.utils import jc
7 |
8 |
9 | @pytest.fixture
10 | def example_module(ij):
11 | info = ij.module().getModuleById(
12 | "command:net.imagej.ops.commands.filter.FrangiVesselness"
13 | )
14 | return ij.module().createModule(info)
15 |
16 |
17 | def test_progress(ij, example_module):
18 | pm.init_progress(example_module)
19 | example_progress: progress = pm.prog_bars[example_module]
20 | assert example_progress.n == 0
21 | pm.update_progress(example_module)
22 | assert example_progress.n == 1
23 | pm.update_progress(example_module)
24 | assert example_progress.n == 2
25 | pm.update_progress(example_module)
26 | assert example_progress.n == 3
27 | pm.close(example_module)
28 | assert example_module not in pm.prog_bars
29 |
30 |
31 | def test_progress_update_via_events(imagej_widget, ij, example_module, asserter):
32 | pm.init_progress(example_module)
33 | asserter(lambda: example_module in pm.prog_bars)
34 | pbr = pm.prog_bars[example_module]
35 | asserter(lambda: pbr.n == 0)
36 |
37 | imagej_widget.progress_handler.emit(jc.ModuleExecutingEvent(example_module))
38 | asserter(lambda: pbr.n == 1)
39 |
40 | imagej_widget.progress_handler.emit(jc.ModuleExecutedEvent(example_module))
41 | asserter(lambda: pbr.n == 2)
42 |
43 | imagej_widget.progress_handler.emit(jc.ModuleFinishedEvent(example_module))
44 | asserter(lambda: pbr.n == 3)
45 | asserter(lambda: example_module not in pm.prog_bars)
46 |
47 |
48 | def test_progress_cancel_via_events(imagej_widget, ij, example_module, asserter):
49 | pm.init_progress(example_module)
50 | asserter(lambda: example_module in pm.prog_bars)
51 | pbr = pm.prog_bars[example_module]
52 | asserter(lambda: pbr.n == 0)
53 |
54 | imagej_widget.progress_handler.emit(jc.ModuleExecutingEvent(example_module))
55 | asserter(lambda: pbr.n == 1)
56 |
57 | imagej_widget.progress_handler.emit(jc.ModuleExecutedEvent(example_module))
58 | asserter(lambda: pbr.n == 2)
59 |
60 | imagej_widget.progress_handler.emit(jc.ModuleCanceledEvent(example_module))
61 | asserter(lambda: example_module not in pm.prog_bars)
62 |
63 |
64 | def test_progress_error_via_events(imagej_widget, ij, example_module, asserter, qtbot):
65 | pm.init_progress(example_module)
66 | asserter(lambda: example_module in pm.prog_bars)
67 | pbr = pm.prog_bars[example_module]
68 | asserter(lambda: pbr.n == 0)
69 |
70 | imagej_widget.progress_handler.emit(jc.ModuleExecutingEvent(example_module))
71 | asserter(lambda: pbr.n == 1)
72 |
73 | imagej_widget.progress_handler.emit(jc.ModuleExecutedEvent(example_module))
74 | asserter(lambda: pbr.n == 2)
75 |
76 | # Mock the Constructor for the JavaErrorMessageBox
77 | errored = False
78 | old = JavaErrorMessageBox.exec
79 |
80 | def _new_exec(self):
81 | nonlocal errored
82 | errored = True
83 |
84 | JavaErrorMessageBox.exec = _new_exec
85 |
86 | # Assert emitting a ModuleErroredEvent causes
87 | # JavaErrorMessageBox.exec to be called
88 | exception = jc.IllegalArgumentException("Yay")
89 | imagej_widget.progress_handler.emit(
90 | jc.ModuleErroredEvent(example_module, exception)
91 | )
92 | asserter(lambda: errored)
93 | # Restore the exec method
94 | JavaErrorMessageBox.exec = old
95 |
--------------------------------------------------------------------------------
/tests/widgets/test_info_bar.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.widgets.info_bar
3 | """
4 |
5 | import pytest
6 | from qtpy.QtWidgets import QLabel, QVBoxLayout
7 |
8 | from napari_imagej.widgets.info_bar import InfoBox
9 |
10 |
11 | @pytest.fixture
12 | def info_bar():
13 | return InfoBox()
14 |
15 |
16 | def test_widget_layout(info_bar: InfoBox):
17 | """Tests the number and expected order of InfoBar children"""
18 | subwidgets = info_bar.children()
19 | assert len(subwidgets) == 2
20 | assert isinstance(info_bar.layout(), QVBoxLayout)
21 | assert isinstance(subwidgets[0], QVBoxLayout)
22 |
23 | assert isinstance(subwidgets[1], QLabel)
24 | assert subwidgets[1].text() == ""
25 |
--------------------------------------------------------------------------------
/tests/widgets/test_result_runner.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.widgets.result_runner
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | import pytest
8 | from qtpy.QtWidgets import QVBoxLayout, QWidget
9 | from scyjava import JavaClasses
10 | from superqt import QElidingLabel
11 |
12 | from napari_imagej.widgets.layouts import QFlowLayout
13 | from napari_imagej.widgets.result_runner import ResultRunner
14 |
15 |
16 | class JavaClassesTest(JavaClasses):
17 | @JavaClasses.java_import
18 | def ModuleSearchResult(self):
19 | return "org.scijava.search.module.ModuleSearchResult"
20 |
21 |
22 | jc = JavaClassesTest()
23 |
24 |
25 | @pytest.fixture
26 | def result_runner(imagej_widget):
27 | return imagej_widget.result_runner
28 |
29 |
30 | def test_result_runner(result_runner):
31 | """Tests the number and expected order of ResultRunner widget children"""
32 | subwidgets = result_runner.children()
33 | # Note: This is BEFORE any module is selected.
34 | assert len(subwidgets) == 3
35 | # The layout
36 | assert isinstance(subwidgets[0], QVBoxLayout)
37 | # The label describing the selected module
38 | assert isinstance(subwidgets[1], QElidingLabel)
39 | # The button Container
40 | assert isinstance(subwidgets[2], QWidget)
41 | assert isinstance(subwidgets[2].layout(), QFlowLayout)
42 |
43 |
44 | def test_result_runner_size_hints(result_runner: ResultRunner):
45 | """Ensuring the widget doesn't grow when text is set."""
46 | # The problem we want to safeguard against here is ensuring the minimum
47 | # size hint doesn't change - this is what causes issues like
48 | # https://github.com/imagej/napari-imagej/issues/273
49 |
50 | # Capture size hint
51 | hint = result_runner.minimumSizeHint()
52 | width_hint, height_hint = hint.width(), hint.height()
53 | # Resize result_runner
54 | result_runner._setText("o" * 50)
55 | # Assert size hint did not change
56 | hint = result_runner.minimumSizeHint()
57 | assert width_hint == hint.width()
58 | assert height_hint == hint.height()
59 |
60 |
61 | @pytest.fixture
62 | def example_info(ij):
63 | return ij.module().getModuleById(
64 | "command:net.imagej.ops.commands.filter.FrangiVesselness"
65 | )
66 |
67 |
68 | def test_button_param_regression(
69 | result_runner: ResultRunner, example_info: "jc.ModuleInfo"
70 | ):
71 | """Simple regression test ensuring search action button population"""
72 |
73 | result = jc.ModuleSearchResult(example_info, "")
74 | buttons = result_runner._buttons_for(result)
75 | assert len(buttons) == 5
76 | assert {b.text() for b in buttons} == {"Batch", "Help", "Run", "Source", "Widget"}
77 |
78 |
79 | def test_widget_button_spawns_widget(
80 | result_runner: ResultRunner, example_info: "jc.ModuleInfo", asserter
81 | ):
82 | """Simple regression test ensuring the widget button spawns a new napari widget"""
83 |
84 | result = jc.ModuleSearchResult(example_info, "")
85 | buttons = result_runner._buttons_for(result)
86 | assert buttons[1].text() == "Widget"
87 | assert result.name() not in result_runner.viewer.window._dock_widgets.keys()
88 | buttons[1].action()
89 | assert result.name() in result_runner.viewer.window._dock_widgets.keys()
90 |
--------------------------------------------------------------------------------
/tests/widgets/test_result_tree.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.widgets.results
3 | """
4 |
5 | import pytest
6 | from qtpy.QtCore import QRunnable, Qt, QThreadPool
7 | from qtpy.QtWidgets import QApplication, QMenu
8 |
9 | from napari_imagej import nij
10 | from napari_imagej.widgets.result_tree import (
11 | SearcherItem,
12 | SearcherTreeView,
13 | SearchResultItem,
14 | )
15 | from napari_imagej.widgets.widget_utils import python_actions_for
16 | from tests.utils import DummySearcher, DummySearchEvent, DummySearchResult
17 | from tests.widgets.widget_utils import _populate_tree
18 |
19 |
20 | @pytest.fixture
21 | def results_tree():
22 | return SearcherTreeView(None)
23 |
24 |
25 | @pytest.fixture
26 | def fixed_tree(asserter):
27 | """Creates a "fake" ResultsTree with deterministic results"""
28 | # Create a default SearchResultTree
29 | tree = SearcherTreeView(None)
30 | _populate_tree(tree, asserter)
31 |
32 | return tree
33 |
34 |
35 | def test_results_widget_layout(fixed_tree: SearcherTreeView):
36 | """Tests the number and expected order of results widget children"""
37 | assert fixed_tree.model().columnCount() == 1
38 | assert fixed_tree.model().headerData(0, Qt.Horizontal, 0) == "Search"
39 |
40 |
41 | def test_searchers_persist(fixed_tree: SearcherTreeView, asserter):
42 | # Find the first searcher, and remove its children
43 | item = fixed_tree.model().invisibleRootItem().child(0, 0)
44 | searcher = item.searcher
45 | asserter(lambda: item.rowCount() > 0)
46 | fixed_tree.model().process.emit(DummySearchEvent(searcher, []))
47 | # Ensure that the children disappear, but the searcher remains
48 | asserter(lambda: item.rowCount() == 0)
49 | asserter(lambda: fixed_tree.model().invisibleRootItem().rowCount() == 2)
50 |
51 |
52 | def test_regression():
53 | """Tests SearchResultItems, SearcherItems display as expected."""
54 | # SearchResultItems wrap SciJava SearchResults, so they expect a running JVM
55 | nij.ij
56 |
57 | # Phase 1: Search Results
58 | dummy = DummySearchResult()
59 | item = SearchResultItem(dummy)
60 | assert item.result == dummy
61 | assert item.data(0) == dummy.name()
62 |
63 | dummy = DummySearchResult(properties={"Menu path": "foo > bar > baz"})
64 | item = SearchResultItem(dummy)
65 | assert item.result == dummy
66 | data = f'{dummy.name()} foo > bar > baz'
67 | assert item.data(0) == data
68 |
69 | # Phase 2: Searchers
70 | dummy = DummySearcher("This is not a Searcher")
71 | item = SearcherItem(dummy)
72 | assert item.searcher == dummy
73 | assert (
74 | item.flags()
75 | == Qt.ItemIsUserCheckable
76 | | Qt.ItemIsSelectable
77 | | Qt.ItemIsEnabled
78 | | Qt.ItemIsDragEnabled
79 | | Qt.ItemIsDropEnabled
80 | )
81 | assert item.data(0) == dummy.title()
82 |
83 |
84 | def test_key_return_expansion(fixed_tree: SearcherTreeView, qtbot):
85 | idx = fixed_tree.model().index(0, 0)
86 | fixed_tree.setCurrentIndex(idx)
87 | expanded = fixed_tree.isExpanded(idx)
88 | # Toggle with enter
89 | qtbot.keyPress(fixed_tree, Qt.Key_Return)
90 | assert fixed_tree.isExpanded(idx) is not expanded
91 | qtbot.keyPress(fixed_tree, Qt.Key_Return)
92 | assert fixed_tree.isExpanded(idx) is expanded
93 |
94 |
95 | def test_search_tree_disable(fixed_tree: SearcherTreeView, asserter):
96 | # Grab an arbitratry enabled Searcher
97 | item = fixed_tree.model().invisibleRootItem().child(1, 0)
98 | # Assert GUI start
99 | data = 'Test2 (3)'
100 | asserter(lambda: item.data(0) == data)
101 | asserter(lambda: item.checkState() == Qt.Checked)
102 |
103 | # Disable the searcher, assert the proper GUI response
104 | item.setCheckState(Qt.Unchecked)
105 | asserter(lambda: item.data(0) == "Test2")
106 | asserter(lambda: item.rowCount() == 0)
107 |
108 | # Enable the searcher, assert the proper GUI response
109 | item.setCheckState(Qt.Checked)
110 | asserter(lambda: item.data(0) == "Test2")
111 | asserter(lambda: item.rowCount() == 0)
112 |
113 |
114 | def test_right_click(fixed_tree: SearcherTreeView, asserter):
115 | """
116 | Ensures that SearcherTreeView has a CustomContextMenuPolicy,
117 | creating a menu that has the SciJava Search Actions relevant for
118 | an arbitrary SearchResult
119 | """
120 | # First, assert the policy
121 | assert fixed_tree.contextMenuPolicy() == Qt.CustomContextMenu
122 | # Then, grab an arbitratry Search Result
123 | idx = fixed_tree.model().index(0, 0).child(0, 0)
124 | rect = fixed_tree.visualRect(idx)
125 | item = fixed_tree.model().itemFromIndex(idx)
126 | # Find its SearchActions
127 | expected_action_names = [pair[0] for pair in python_actions_for(item.result, None)]
128 |
129 | # NB when the menu pops, this thread will freeze until the menu is resolved
130 | # To inspect (and close) the menu, we must do so on another thread.
131 | class Handler(QRunnable):
132 | def run(self) -> None:
133 | # Wait for the menu to arise
134 | asserter(lambda: isinstance(QApplication.activePopupWidget(), QMenu))
135 | menu = QApplication.activePopupWidget()
136 | # Assert equality of actions (by name)
137 | for expected, actual in zip(expected_action_names, menu.actions()):
138 | assert expected == actual.text()
139 | # Close the menu (later, on the GUI thread)
140 | menu.deleteLater()
141 | self._passed = True
142 |
143 | def passed(self) -> bool:
144 | return self._passed
145 |
146 | # Start the Runner, so we can evaluate the Menu
147 | runnable = Handler()
148 | QThreadPool.globalInstance().start(runnable)
149 | # Launch the menu
150 | fixed_tree.customContextMenuRequested.emit(rect.center())
151 | # Wait for the the runner to finish evaluating, and ensure assertions passed.
152 | asserter(QThreadPool.globalInstance().waitForDone)
153 | assert runnable.passed()
154 |
--------------------------------------------------------------------------------
/tests/widgets/test_searchbar.py:
--------------------------------------------------------------------------------
1 | """
2 | A module testing napari_imagej.widgets.searchbar
3 | """
4 |
5 | import pytest
6 | from qtpy.QtWidgets import QHBoxLayout, QLineEdit
7 |
8 | from napari_imagej.widgets.napari_imagej import NapariImageJWidget
9 | from napari_imagej.widgets.searchbar import JLineEdit
10 |
11 |
12 | @pytest.fixture
13 | def searchbar(imagej_widget: NapariImageJWidget):
14 | return imagej_widget.search
15 |
16 |
17 | def test_searchbar_widget_layout(searchbar):
18 | """Tests the number and expected order of search widget children"""
19 | subwidgets = searchbar.children()
20 | assert len(subwidgets) == 2
21 | assert isinstance(subwidgets[0], QHBoxLayout)
22 | assert isinstance(subwidgets[1], QLineEdit)
23 |
24 |
25 | def test_searchbar_regression():
26 | bar = JLineEdit()
27 | assert bar.text() == "Initializing ImageJ...Please Wait"
28 | assert not bar.isEnabled()
29 |
--------------------------------------------------------------------------------
/tests/widgets/widget_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | A module containing functionality useful for widget testing
3 | """
4 |
5 | from typing import Optional
6 |
7 | from qtpy.QtCore import Qt
8 |
9 | from napari_imagej import nij
10 | from napari_imagej.widgets.result_tree import SearcherItem, SearcherTreeView
11 | from tests.utils import DummySearcher, DummySearchEvent, jc
12 |
13 |
14 | def _searcher_tree_named(tree: SearcherTreeView, name: str) -> Optional[SearcherItem]:
15 | root = tree.model().invisibleRootItem()
16 | for i in range(root.rowCount()):
17 | if str(root.child(i, 0).searcher.title()).startswith(name):
18 | return root.child(i, 0)
19 | return None
20 |
21 |
22 | def _populate_tree(tree: SearcherTreeView, asserter):
23 | # DummySearchers are SciJava Searchers - we need a JVM
24 | nij.ij
25 |
26 | root = tree.model().invisibleRootItem()
27 | asserter(lambda: root.rowCount() == 0)
28 | # Add two searchers
29 | searcher1 = DummySearcher("Test1")
30 | tree.model().insert_searcher.emit(searcher1)
31 | searcher2 = DummySearcher("Test2")
32 | tree.model().insert_searcher.emit(searcher2)
33 | asserter(lambda: root.rowCount() == 2)
34 | tree.model().item(0).setCheckState(Qt.Checked)
35 | tree.model().item(1).setCheckState(Qt.Checked)
36 |
37 | # Update each searcher with data
38 | tree.model().process.emit(
39 | DummySearchEvent(
40 | searcher1, [jc.ClassSearchResult(c, "") for c in (jc.Float, jc.Double)]
41 | )
42 | )
43 | tree.model().process.emit(
44 | DummySearchEvent(
45 | searcher2,
46 | [jc.ClassSearchResult(c, "") for c in (jc.Short, jc.Integer, jc.Long)],
47 | )
48 | )
49 |
50 | # Wait for the tree to populate
51 | count = 2
52 | asserter(lambda: root.child(0, 0).rowCount() == count)
53 | data = f'Test1 ({count})'
54 | asserter(lambda: root.child(0, 0).data(0) == data)
55 |
56 | count = 3
57 | asserter(lambda: root.child(1, 0).rowCount() == count)
58 | data = f'Test2 ({count})'
59 | asserter(lambda: root.child(1, 0).data(0) == data)
60 |
--------------------------------------------------------------------------------