diff --git a/package.json b/package.json index 4858164f..8548fcee 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "pnpm -r --parallel run dev", "build": "pnpm -r build", "lint": "pnpm prettier --check . && pnpm eslint .", + "lint:fix": "pnpm prettier --write . && pnpm eslint --fix .", "test": "pnpm test:downloadData && pnpm cy:component", "test:ci": "pnpm test:downloadData && pnpm cy:component:ci", "test:downloadData": "node test/downloadData.mjs", diff --git a/packages/agave-renderer/README.md b/packages/agave-renderer/README.md new file mode 100644 index 00000000..ca00a3fe --- /dev/null +++ b/packages/agave-renderer/README.md @@ -0,0 +1,21 @@ +# ITK Viewer Agave Renderer + +[![PyPI - Version](https://img.shields.io/pypi/v/itk-viewer-agave-renderer.svg)](https://pypi.org/project/itk-viewer-agave-renderer) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/itk-viewer-agave-renderer.svg)](https://pypi.org/project/itk-viewer-agave-renderer) + +--- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) + +## Installation + +```console +pip install itk-viewer-agave-renderer +``` + +## License + +`itk-viewer-agave-renderer` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license. diff --git a/packages/agave-renderer/itk_viewer_agave_renderer/__about__.py b/packages/agave-renderer/itk_viewer_agave_renderer/__about__.py new file mode 100644 index 00000000..bb244d02 --- /dev/null +++ b/packages/agave-renderer/itk_viewer_agave_renderer/__about__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2023-present NumFOCUS +# +# SPDX-License-Identifier: Apache-2.0 +__version__ = "0.0.1" diff --git a/packages/agave-renderer/itk_viewer_agave_renderer/__init__.py b/packages/agave-renderer/itk_viewer_agave_renderer/__init__.py new file mode 100644 index 00000000..7823ca61 --- /dev/null +++ b/packages/agave-renderer/itk_viewer_agave_renderer/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023-present NumFOCUS +# +# SPDX-License-Identifier: Apache-2.0 + +from .renderer import Renderer diff --git a/packages/agave-renderer/itk_viewer_agave_renderer/renderer.py b/packages/agave-renderer/itk_viewer_agave_renderer/renderer.py new file mode 100644 index 00000000..54c0dfb9 --- /dev/null +++ b/packages/agave-renderer/itk_viewer_agave_renderer/renderer.py @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: 2023-present NumFOCUS +# +# SPDX-License-Identifier: Apache-2.0 + +import time +from enum import Enum, auto +import functools + +import agave_pyclient as agave +from imjoy_rpc.hypha import connect_to_server +from itkwasm_htj2k import encode +from itkwasm import Image, ImageType, PixelTypes, IntTypes +import numpy as np +from PIL import Image as PILImage + +class AgaveRendererMemoryRedraw(agave.AgaveRenderer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def memory_redraw(self): + self.cb.add_command("REDRAW") + buf = self.cb.make_buffer() + # TODO ENSURE CONNECTED + self.ws.send(buf, True) + # and then WAIT for render to be completed + binarydata = self.ws.wait_for_image() + # ready for next frame + self.cb = agave.commandbuffer.CommandBuffer() + img = PILImage.open(binarydata) + rgba = img.convert('RGBA').tobytes() + return rgba + +# how to handle unknown events +class UnknownEventAction(Enum): + ERROR = auto() + WARNING = auto() + IGNORE = auto() + +class Renderer(): + def __init__(self, width=500, height=400, unknown_event_action=UnknownEventAction.WARNING): + self.width = width + self.height = height + self.unknown_event_action = unknown_event_action + + async def setup(self): + # Note: the agave websocket server needs to be running + self.agave = AgaveRendererMemoryRedraw() + self.agave.set_resolution(self.width, self.height) + + async def render(self): + r = self.agave + start_time = time.time() + rgba = r.memory_redraw() + image_type = ImageType(dimension=2, componentType=IntTypes.UInt8, pixelType=PixelTypes.RGBA, components=4) + image = Image(image_type) + image.size = [self.width, self.height] + image.data = np.frombuffer(rgba, dtype=np.uint8) + # lossless + # rgba_encoded = encode(image) + rgba_encoded = encode(image, not_reversible=True, quantization_step=0.02) + elapsed = time.time() - start_time + return { "frame": rgba_encoded, "renderTime": elapsed } + + def update_renderer(self, events): + r = self.agave + + for [event_type, payload] in events: + match event_type: + case 'density': + r.density(payload) + case 'cameraPose': + eye = payload['eye'] + r.eye(eye[0], eye[1], eye[2]) + up = payload['up'] + r.up(up[0], up[1], up[2]) + target = payload['target'] + r.target(target[0], target[1], target[2]) + case 'renderIterations': + r.render_iterations(payload) + case _: + self.handle_unknown_event(event_type) + + def handle_unknown_event(self, event_type): + match self.unknown_event_action: + case UnknownEventAction.ERROR: + raise Exception(f"Unknown event type: {event_type}") + case UnknownEventAction.WARNING: + print(f"Unknown event type: {event_type}", flush=True) + case UnknownEventAction.IGNORE: + pass + + async def connect(self, hypha_server_url, load_image_into_agave_fn, visibility="public", identifier="agave-renderer"): + server = await connect_to_server( + { + "name": "agave-renderer-client", + "server_url": hypha_server_url + } + ) + + await server.register_service({ + "name": "Agave Renderer", + "id": identifier, + "config": { + "visibility": visibility, + "require_context": False, + "run_in_executor": False, + }, + + "setup": self.setup, + + "loadImage": functools.partial(load_image_into_agave_fn, self), + + "render": self.render, + "updateRenderer": self.update_renderer, + }) + print("Renderer is ready to receive request!", server.config, flush=True) \ No newline at end of file diff --git a/packages/agave-renderer/pyproject.toml b/packages/agave-renderer/pyproject.toml new file mode 100644 index 00000000..d1d05a31 --- /dev/null +++ b/packages/agave-renderer/pyproject.toml @@ -0,0 +1,163 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "itk-viewer-agave-renderer" +dynamic = ["version"] +description = '' +readme = "README.md" +requires-python = ">=3.7" +license = "Apache-2.0" +keywords = [] +authors = [ + { name = "Matt McCormick", email = "matt.mccormick@kitware.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "agave-pyclient", + "hypha", + "itkwasm-htj2k", + "numpy", + "pillow", +] + +[project.urls] +Documentation = "https://github.com/InsightSoftwareConsortium/itk-viewer/tree/main/packages/agave-renderer#readme" +Issues = "https://github.com/InsightSoftwareConsortium/itk-viewer/issues" +Source = "https://github.com/InsightSoftwareConsortium/itk-viewer" + +[tool.hatch.version] +path = "itk_viewer_agave_renderer/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.7", "3.8", "3.9", "3.10", "3.11"] + +[tool.hatch.envs.lint] +detached = true +dependencies = [ + "black>=23.1.0", + "mypy>=1.0.0", + "ruff>=0.0.243", +] +[tool.hatch.envs.lint.scripts] +typing = "mypy --install-types --non-interactive {args:itk_viewer_agave_renderer tests}" +style = [ + "ruff {args:.}", + "black --check --diff {args:.}", +] +fmt = [ + "black {args:.}", + "ruff --fix {args:.}", + "style", +] +all = [ + "style", + "typing", +] + +[tool.black] +target-version = ["py37"] +line-length = 120 +skip-string-normalization = true + +[tool.ruff] +target-version = "py37" +line-length = 120 +select = [ + "A", + "ARG", + "B", + "C", + "DTZ", + "E", + "EM", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "Q", + "RUF", + "S", + "T", + "TID", + "UP", + "W", + "YTT", +] +ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +] +unfixable = [ + # Don't touch unused imports + "F401", +] + +[tool.ruff.isort] +known-first-party = ["itk_viewer_agave_renderer"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.coverage.run] +source_pkgs = ["itk_viewer_agave_renderer", "tests"] +branch = true +parallel = true +omit = [ + "itk_viewer_agave_renderer/__about__.py", +] + +[tool.coverage.paths] +itk_viewer_agave_renderer = ["itk_viewer_agave_renderer", "*/itk-viewer-agave-renderer/itk_viewer_agave_renderer"] +tests = ["tests", "*/itk-viewer-agave-renderer/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/packages/agave-renderer/tests/__init__.py b/packages/agave-renderer/tests/__init__.py new file mode 100644 index 00000000..b9ade376 --- /dev/null +++ b/packages/agave-renderer/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2023-present NumFOCUS +# +# SPDX-License-Identifier: Apache 2.0