diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml new file mode 100644 index 0000000..0480091 --- /dev/null +++ b/.github/workflows/test_and_deploy.yml @@ -0,0 +1,90 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: tests + +on: + push: + branches: + - main + - npe2 + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + pull_request: + branches: + - main + - npe2 + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.platform }} py${{ matrix.python-version }} + runs-on: ${{ matrix.platform }} + strategy: + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # these libraries enable testing on Qt on linux + - uses: tlambert03/setup-qt-libs@v1 + + # strategy borrowed from vispy for installing opengl libs on windows + - name: Install Windows OpenGL + if: runner.os == 'Windows' + run: | + git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git + powershell gl-ci-helpers/appveyor/install_opengl.ps1 + + # note: if you need dependencies from conda, considering using + # setup-miniconda: https://github.com/conda-incubator/setup-miniconda + # and + # tox-conda: https://github.com/tox-dev/tox-conda + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install setuptools tox tox-gh-actions + + # this runs the platform-specific tests declared in tox.ini + - name: Test with tox + uses: aganders3/headless-gui@v1 + with: + run: python -m tox + env: + PLATFORM: ${{ matrix.platform }} + + - name: Coverage + uses: codecov/codecov-action@v3 + + deploy: + # this will run when you have tagged a commit, starting with "v*" + # and requires that you have put your twine API key in your + # github secrets (see readme for details) + needs: [test] + runs-on: ubuntu-latest + if: contains(github.ref, 'tags') + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools setuptools_scm wheel twine build + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} + run: | + git tag + python -m build . + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73d56d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.napari_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask instance folder +instance/ + +# Sphinx documentation +docs/_build/ + +# MkDocs documentation +/site/ + +# PyBuilder +target/ + +# Pycharm and VSCode +.idea/ +venv/ +.vscode/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# OS +.DS_Store + +# written by setuptools_scm +**/_version.py diff --git a/.napari-hub/DESCRIPTION.md b/.napari-hub/DESCRIPTION.md new file mode 100644 index 0000000..59a66ca --- /dev/null +++ b/.napari-hub/DESCRIPTION.md @@ -0,0 +1,9 @@ + + +This plugin uses bfio Python package to read and write OMETIFF and OMEZarr files. diff --git a/.napari-hub/config.yml b/.napari-hub/config.yml new file mode 100644 index 0000000..904c76f --- /dev/null +++ b/.napari-hub/config.yml @@ -0,0 +1,9 @@ +# You may use this file to customize how your plugin page appears +# on the napari hub: https://www.napari-hub.org/ +# See their wiki for details https://github.com/chanzuckerberg/napari-hub/wiki + +# Please note that this file should only be used IN ADDITION to entering +# metadata fields (such as summary, description, authors, and various URLS) +# in your standard python package metadata (e.g. setup.cfg, setup.py, or +# pyproject.toml), when you would like those fields to be displayed +# differently on the hub than in the napari application. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ce6cc96 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-docstring-first + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: ^\.napari-hub/.* + - id: check-yaml # checks for correct yaml syntax for github actions ex. + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.256 + hooks: + - id: ruff + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + - repo: https://github.com/tlambert03/napari-plugin-checks + rev: v0.3.0 + hooks: + - id: napari-plugin-checks + # https://mypy.readthedocs.io/en/stable/introduction.html + # you may wish to add this as well! + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v0.910-1 + # hooks: + # - id: mypy diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f3155af --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include README.md + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1c395b --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# napari-bfio + +[![License MIT](https://img.shields.io/pypi/l/napari-bfio.svg?color=green)](https://github.com/PolusAI/napari-bfio/raw/main/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/napari-bfio.svg?color=green)](https://pypi.org/project/napari-bfio) +[![Python Version](https://img.shields.io/pypi/pyversions/napari-bfio.svg?color=green)](https://python.org) +[![tests](https://github.com/PolusAI/napari-bfio/workflows/tests/badge.svg)](https://github.com/PolusAI/napari-bfio/actions) +[![codecov](https://codecov.io/gh/PolusAI/napari-bfio/branch/main/graph/badge.svg)](https://codecov.io/gh/PolusAI/napari-bfio) +[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-bfio)](https://napari-hub.org/plugins/napari-bfio) + +A plugin to read and write images using bfio within napari + +---------------------------------- + +This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template. + + + +## Installation + +You can install `napari-bfio` via [pip]: + + pip install napari-bfio + + + +To install latest development version : + + pip install git+https://github.com/PolusAI/napari-bfio.git + + +## Contributing + +Contributions are very welcome. Tests can be run with [tox], please ensure +the coverage at least stays the same before you submit a pull request. + +## License + +Distributed under the terms of the [MIT] license, +"napari-bfio" is free and open source software + +## Issues + +If you encounter any problems, please [file an issue] along with a detailed description. + +[napari]: https://github.com/napari/napari +[Cookiecutter]: https://github.com/audreyr/cookiecutter +[@napari]: https://github.com/napari +[MIT]: http://opensource.org/licenses/MIT +[BSD-3]: http://opensource.org/licenses/BSD-3-Clause +[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt +[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt +[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0 +[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt +[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin + +[file an issue]: https://github.com/PolusAI/napari-bfio/issues + +[napari]: https://github.com/napari/napari +[tox]: https://tox.readthedocs.io/en/latest/ +[pip]: https://pypi.org/project/pip/ +[PyPI]: https://pypi.org/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..422dc2d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=42.0.0", "wheel"] +build-backend = "setuptools.build_meta" + + + +[tool.black] +line-length = 79 +target-version = ['py38', 'py39', 'py310'] + + +[tool.ruff] +line-length = 79 +select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long. let black handle this + "UP006", "UP007", # type annotation. As using magicgui require runtime type annotation then we disable this. + "SIM117", # flake8-simplify - some of merged with statements are not looking great with black, reanble after drop python 3.9 +] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".mypy_cache", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*vendored*", + "*_vendor*", +] + +target-version = "py38" +fix = true diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9d93f5a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,59 @@ +[metadata] +name = napari-bfio +version = attr: napari_bfio.__version__ +description = A plugin to read and write images using bfio within napari +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/PolusAI/napari-bfio +author = Sameeul B Samee, Nick Schaub +author_email = sameeul.samee@nih.gov, nick.schaub@nih.gov +license = MIT +license_files = LICENSE +classifiers = + Development Status :: 2 - Pre-Alpha + Framework :: napari + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Topic :: Scientific/Engineering :: Image Processing +project_urls = + Bug Tracker = https://github.com/PolusAI/napari-bfio/issues + Documentation = https://github.com/PolusAI/napari-bfio#README.md + Source Code = https://github.com/PolusAI/napari-bfio + User Support = https://github.com/PolusAI/napari-bfio/issues + +[options] +packages = find: +install_requires = + numpy + bfio + +python_requires = >=3.8 +include_package_data = True +package_dir = + =src + +# add your package requirements here + +[options.packages.find] +where = src + +[options.entry_points] +napari.manifest = + napari-bfio = napari_bfio:napari.yaml + +[options.extras_require] +testing = + tox + pytest # https://docs.pytest.org/en/latest/contents.html + pytest-cov # https://pytest-cov.readthedocs.io/en/latest/ + + +[options.package_data] +* = *.yaml diff --git a/src/napari_bfio/__init__.py b/src/napari_bfio/__init__.py new file mode 100644 index 0000000..f158ec7 --- /dev/null +++ b/src/napari_bfio/__init__.py @@ -0,0 +1,10 @@ +__version__ = "0.0.1" + +from ._reader import napari_get_reader +from ._writer import write_multiple, write_single_image + +__all__ = ( + "napari_get_reader", + "write_single_image", + "write_multiple", + ) diff --git a/src/napari_bfio/_reader.py b/src/napari_bfio/_reader.py new file mode 100644 index 0000000..3e338ed --- /dev/null +++ b/src/napari_bfio/_reader.py @@ -0,0 +1,79 @@ +""" +This module is an example of a barebones numpy reader plugin for napari. + +It implements the Reader specification, but your plugin may choose to +implement multiple readers or even other plugin contributions. see: +https://napari.org/stable/plugins/guides.html?#readers +""" +import numpy as np + +def is_type_supported(path): + if path.endswith(".ome.tiff"): + return True + elif path.endswith(".ome.tif"): + return True + elif path.endswith(".ome.zarr"): + return True + else: + return False + +def napari_get_reader(path): + """A basic implementation of a Reader contribution. + + Parameters + ---------- + path : str or list of str + Path to file, or list of paths. + + Returns + ------- + function or None + If the path is a recognized format, return a function that accepts the + same path or list of paths, and returns a list of layer data tuples. + """ + if isinstance(path, list): + # reader plugins may be handed single path, or a list of paths. + # if it is a list, it is assumed to be an image stack... + # so we are only going to look at the first file. + path = path[0] + + # # if we know we cannot read the file, we immediately return None. + if not is_type_supported(path): + return None + + # otherwise we return the *function* that can read ``path``. + return reader_function + + +def reader_function(path): + """Take a path or list of paths and return a list of LayerData tuples. + + Readers are expected to return data as a list of tuples, where each tuple + is (data, [add_kwargs, [layer_type]]), "add_kwargs" and "layer_type" are + both optional. + + Parameters + ---------- + path : str or list of str + Path to file, or list of paths. + + Returns + ------- + layer_data : list of tuples + A list of LayerData tuples where each tuple in the list contains + (data, metadata, layer_type), where data is a numpy array, metadata is + a dict of keyword arguments for the corresponding viewer.add_* method + in napari, and layer_type is a lower-case string naming the type of + layer. Both "meta", and "layer_type" are optional. napari will + default to layer_type=="image" if not provided + """ + # handle both a string and a list of strings + paths = [path] if isinstance(path, str) else path + # load all files into array + layer_data = [] + from bfio import BioReader # import as late as possible + for _path in paths: + br = BioReader(_path) + layer_data.append((np.squeeze(br.read()), {"metadata":br.metadata})) + + return layer_data diff --git a/src/napari_bfio/_tests/__init__.py b/src/napari_bfio/_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/napari_bfio/_tests/test_reader.py b/src/napari_bfio/_tests/test_reader.py new file mode 100644 index 0000000..43f52bd --- /dev/null +++ b/src/napari_bfio/_tests/test_reader.py @@ -0,0 +1,16 @@ +import numpy as np + +from napari_bfio import napari_get_reader + + +# tmp_path is a pytest fixture +def test_reader(tmp_path): + """An example of how you might test your plugin.""" + + # write some fake data using your supported file format + pass + + +def test_get_reader_pass(): + reader = napari_get_reader("fake.file") + assert reader is None \ No newline at end of file diff --git a/src/napari_bfio/_tests/test_writer.py b/src/napari_bfio/_tests/test_writer.py new file mode 100644 index 0000000..d3cc0f2 --- /dev/null +++ b/src/napari_bfio/_tests/test_writer.py @@ -0,0 +1,7 @@ +# from napari_bfio import write_single_image, write_multiple + +# add your tests here... + + +def test_something(): + pass diff --git a/src/napari_bfio/_writer.py b/src/napari_bfio/_writer.py new file mode 100644 index 0000000..152055a --- /dev/null +++ b/src/napari_bfio/_writer.py @@ -0,0 +1,64 @@ +""" +This module is an example of a barebones writer plugin for napari. + +It implements the Writer specification. +see: https://napari.org/stable/plugins/guides.html?#writers + +Replace code below according to your needs. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, List, Sequence, Tuple, Union + +import numpy as np +import ome_types + +if TYPE_CHECKING: + DataType = Union[Any, Sequence[Any]] + FullLayerData = Tuple[DataType, dict, str] + + +def write_single_image(path: str, data: Any, meta: dict) -> List[str]: + + """Writes a single image layer""" + # need to check if we need to run in a seperate thread to stop GUI from freezing + + from bfio import BioWriter + # if this image is loaded via bfio BioReader, we will have a non-empty metadata + if (meta["metadata"] != {}): + try: + ome_data = ome_types.model.OME(**meta["metadata"]) + except ome_types.ValidationError: + ome_data = None + + if ome_data != None: # a high chance we read this using bfio + with BioWriter(path, metadata=ome_data) as bw: + while data.ndim < 5: + data = data[..., np.newaxis] + + bw[:] = data + + return [path] + else: # we are reading some other data that is not OME Tiff/ Zarr and also not loaded via bfio + if meta["rgb"]: + BioWriter.logger.info("The BioWriter cannot write color images.") + return None + with BioWriter(path) as bw: + bw.shape = data.shape + bw.dtype = data.dtype + while data.ndim < 5: + data = data[..., np.newaxis] + + bw[:] = data + + return [path] + +def write_multiple(path: str, data: List[FullLayerData]) -> List[str]: + """Writes multiple layers of different types.""" + + # implement your writer logic here ... + from bfio import BioWriter + BioWriter.logger.info("Bfio: Multi-file writing not yet supported.") + return None + # return path to any file(s) that were successfully written + # return [path] diff --git a/src/napari_bfio/napari.yaml b/src/napari_bfio/napari.yaml new file mode 100644 index 0000000..16f0209 --- /dev/null +++ b/src/napari_bfio/napari.yaml @@ -0,0 +1,22 @@ +name: napari-bfio +display_name: Bfio +contributions: + commands: + - id: napari-bfio.get_reader + python_name: napari_bfio._reader:napari_get_reader + title: Open data with Bfio + - id: napari-bfio.write_single_image + python_name: napari_bfio._writer:write_single_image + title: Save image data with Bfio + readers: + - command: napari-bfio.get_reader + accepts_directories: true + filename_patterns: ['*.ome.tiff', '*.ome.tif', '*.ome.zarr'] + writers: + filename_extensions: ['*.ome.tiff', '*.ome.tif', '*.ome.zarr'] + - command: napari-bfio.write_single_image + layer_types: ['image'] + filename_extensions: ['*.ome.tiff', '*.ome.tif', '*.ome.zarr'] + - command: napari-bfio.write_single_image + layer_types: ['labels'] + filename_extensions: ['*.ome.tiff', '*.ome.tif', '*.ome.zarr'] \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6f3cfad --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +# For more information about tox, see https://tox.readthedocs.io/en/latest/ +[tox] +envlist = py{38,39,310}-{linux,macos,windows} +isolated_build=true + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows + +[testenv] +platform = + macos: darwin + linux: linux + windows: win32 +passenv = + CI + GITHUB_ACTIONS + DISPLAY + XAUTHORITY + NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +extras = + testing +commands = pytest -v --color=yes --cov=napari_bfio --cov-report=xml