├── .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 | [![License](https://img.shields.io/pypi/l/napari-imagej.svg?color=green)](https://github.com/imagej/napari-imagej/raw/main/LICENSE) 6 | [![PyPI](https://img.shields.io/pypi/v/napari-imagej.svg?color=green)](https://pypi.org/project/napari-imagej) 7 | [![Python Version](https://img.shields.io/pypi/pyversions/napari-imagej.svg?color=green)](https://python.org) 8 | [![tests](https://github.com/imagej/napari-imagej/workflows/tests/badge.svg)](https://github.com/imagej/napari-imagej/actions) 9 | [![codecov](https://codecov.io/gh/imagej/napari-imagej/branch/main/graph/badge.svg)](https://codecov.io/gh/imagej/napari-imagej) 10 | [![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-imagej)](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 | 5 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/napari_imagej/resources/export_detailed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 40 | 45 | 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 | 5 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /src/napari_imagej/resources/repl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 25 | 27 | 32 | 36 | 40 | 44 | 45 | 46 | 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 | --------------------------------------------------------------------------------