From 3fcd7474b133a280cb671ae56ef40b9aaafda063 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 16 Feb 2024 03:06:17 -0500 Subject: [PATCH] basic image volume works --- examples/notebooks/volume.ipynb | 81 ++++++++++ fastplotlib/graphics/__init__.py | 3 +- fastplotlib/graphics/image.py | 150 +++++++++++++++++-- fastplotlib/layouts/graphic_methods_mixin.py | 65 +++++++- 4 files changed, 285 insertions(+), 14 deletions(-) create mode 100644 examples/notebooks/volume.ipynb diff --git a/examples/notebooks/volume.ipynb b/examples/notebooks/volume.ipynb new file mode 100644 index 000000000..60548b31c --- /dev/null +++ b/examples/notebooks/volume.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "737b7aee-8cfb-47f0-b595-bc3749e995ea", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import imageio.v3 as iio\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92d3b41a-2d38-40b7-9156-1755711d4964", + "metadata": {}, + "outputs": [], + "source": [ + "voldata = iio.imread(\"imageio:stent.npz\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef67db3d-484d-40c5-9725-f54b4f99d1d7", + "metadata": {}, + "outputs": [], + "source": [ + "voldata.dtype" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28efdd55-a6fd-4ffd-a934-7a0b759cee96", + "metadata": {}, + "outputs": [], + "source": [ + "plot = fpl.Plot(camera=\"3d\")\n", + "\n", + "plot.add_volume(voldata)\n", + "\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80df1d1c-5ff7-4174-aeb7-4dbb534fd33f", + "metadata": {}, + "outputs": [], + "source": [ + "plot.controller = \"orbit\"" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 2a008015e..ffc108319 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -1,11 +1,12 @@ from .line import LineGraphic from .scatter import ScatterGraphic -from .image import ImageGraphic, HeatmapGraphic +from .image import ImageGraphic, HeatmapGraphic, VolumeGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack __all__ = [ "ImageGraphic", + "VolumeGraphic", "ScatterGraphic", "LineGraphic", "HeatmapGraphic", diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 10f09eefb..029039e11 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -204,7 +204,8 @@ def __init__( vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", + interpolation: str = "nearest", + map_interpolation: str = "linear", isolated_buffer: bool = True, *args, **kwargs, @@ -228,8 +229,11 @@ def __init__( cmap: str, optional, default "plasma" colormap to use to display the image data, ignored if data is RGB - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" + interpolation: str, optional, default "nearest" + interpolation of the data, one of "nearest" or "linear" + + map_interpolation: str, optional, default "nearest" + interpolation of the cmap, one of "nearest" or "linear" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then @@ -282,12 +286,16 @@ def __init__( # if data is RGB or RGBA if data.ndim > 2: material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map_interpolation=filter + clim=(vmin, vmax), + interpolation=interpolation, + map_interpolation=map_interpolation ) # if data is just 2D without color information, use colormap LUT else: material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + clim=(vmin, vmax), map=self.cmap(), + interpolation=interpolation, + map_interpolation=map_interpolation ) world_object = pygfx.Image(geometry, material) @@ -356,7 +364,8 @@ def __init__( vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", + interpolation: str = "nearest", + map_interpolation: str = "linear", chunk_size: int = 8192, isolated_buffer: bool = True, *args, @@ -381,8 +390,11 @@ def __init__( cmap: str, optional, default "plasma" colormap to use to display the data - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" + interpolation: str, optional, default "nearest" + interpolation of the data, one of "nearest" or "linear" + + map_interpolation: str, optional, default "nearest" + interpolation of the cmap, one of "nearest" or "linear" chunk_size: int, default 8192, max 8192 chunk size for each tile used to make up the heatmap texture @@ -446,7 +458,10 @@ def __init__( self.cmap = HeatmapCmapFeature(self, cmap) self._material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + clim=(vmin, vmax), + map=self.cmap(), + interpolation=interpolation, + map_interpolation=map_interpolation, ) for start, stop, chunk in zip(start_ixs, stop_ixs, chunks): @@ -485,3 +500,120 @@ def set_feature(self, feature: str, new_data: Any, indices: Any): def reset_feature(self, feature: str): pass + + +class VolumeGraphic(Graphic, Interaction): + feature_events = ("data", "cmap", "present") + + def __init__( + self, + data: Any, + vmin: float = None, + vmax: float = None, + cmap: str = "plasma", + interpolation: str = "nearest", + map_interpolation: str = "linear", + isolated_buffer: bool = True, + *args, + **kwargs, + ): + """ + Create an Image Volume Graphic + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()`` + Tensorflow Tensors also work **probably**, but not thoroughly tested + | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` + + vmin: int, optional + minimum value for color scaling, calculated from data if not provided + + vmax: int, optional + maximum value for color scaling, calculated from data if not provided + + cmap: str, optional, default "plasma" + colormap to use to display the image data, ignored if data is RGB + + interpolation: str, optional, default "nearest" + interpolation of the data, one of "nearest" or "linear" + + map_interpolation: str, optional, default "nearest" + interpolation of the cmap, one of "nearest" or "linear" + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer. + + args: + additional arguments passed to Graphic + + kwargs: + additional keyword arguments passed to Graphic + + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the data buffer displayed in the ImageGraphic + + **cmap**: :class:`.ImageCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + + """ + + super().__init__(*args, **kwargs) + + data = to_gpu_supported_dtype(data) + + # TODO: we need to organize and do this better + if isolated_buffer: + # initialize a buffer with the same shape as the input data + # we do not directly use the input data array as the buffer + # because if the input array is a read-only type, such as + # numpy memmaps, we would not be able to change the image data + buffer_init = np.zeros(shape=data.shape, dtype=data.dtype) + else: + buffer_init = data + + if (vmin is None) or (vmax is None): + vmin, vmax = quick_min_max(data) + + texture = pygfx.Texture(buffer_init, dim=3) + + geometry = pygfx.Geometry(grid=texture) + + self.cmap = ImageCmapFeature(self, cmap) + + material = pygfx.VolumeRayMaterial( + clim=(vmin, vmax), + map=self.cmap(), + interpolation=interpolation, + map_interpolation=map_interpolation + ) + + world_object = pygfx.Volume( + geometry, + material + ) + + self._set_world_object(world_object) + + self.cmap.vmin = vmin + self.cmap.vmax = vmax + + self.data = ImageDataFeature(self, data) + + if isolated_buffer: + self.data = data + + def set_feature(self, feature: str, new_data: Any, indices: Any): + pass + + def reset_feature(self, feature: str): + pass diff --git a/fastplotlib/layouts/graphic_methods_mixin.py b/fastplotlib/layouts/graphic_methods_mixin.py index b00187df7..0cc358378 100644 --- a/fastplotlib/layouts/graphic_methods_mixin.py +++ b/fastplotlib/layouts/graphic_methods_mixin.py @@ -82,7 +82,7 @@ def add_heatmap(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = """ return self._create_graphic(HeatmapGraphic, data, vmin, vmax, cmap, filter, chunk_size, isolated_buffer, *args, **kwargs) - def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = 'plasma', filter: str = 'nearest', isolated_buffer: bool = True, *args, **kwargs) -> ImageGraphic: + def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = 'plasma', interpolation: str = 'nearest', map_interpolation: str = 'linear', isolated_buffer: bool = True, *args, **kwargs) -> ImageGraphic: """ Create an Image Graphic @@ -103,8 +103,11 @@ def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = ' cmap: str, optional, default "plasma" colormap to use to display the image data, ignored if data is RGB - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" + interpolation: str, optional, default "nearest" + interpolation of the data, one of "nearest" or "linear" + + map_interpolation: str, optional, default "nearest" + interpolation of the cmap, one of "nearest" or "linear" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then @@ -131,7 +134,7 @@ def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = ' """ - return self._create_graphic(ImageGraphic, data, vmin, vmax, cmap, filter, isolated_buffer, *args, **kwargs) + return self._create_graphic(ImageGraphic, data, vmin, vmax, cmap, interpolation, map_interpolation, isolated_buffer, *args, **kwargs) def add_line_collection(self, data: List[numpy.ndarray], z_position: Union[List[float], float] = None, thickness: Union[float, List[float]] = 2.0, colors: Union[List[numpy.ndarray], numpy.ndarray] = 'w', alpha: float = 1.0, cmap: Union[List[str], str] = None, cmap_values: Union[numpy.ndarray, List] = None, name: str = None, metadata: Union[list, tuple, numpy.ndarray] = None, *args, **kwargs) -> LineCollection: """ @@ -409,3 +412,57 @@ def add_text(self, text: str, position: Tuple[int] = (0, 0, 0), size: int = 14, """ return self._create_graphic(TextGraphic, text, position, size, face_color, outline_color, outline_thickness, screen_space, anchor, *args, **kwargs) + def add_volume(self, data: Any, vmin: float = None, vmax: float = None, cmap: str = 'plasma', interpolation: str = 'nearest', map_interpolation: str = 'linear', isolated_buffer: bool = True, *args, **kwargs) -> VolumeGraphic: + """ + + Create an Image Volume Graphic + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()`` + Tensorflow Tensors also work **probably**, but not thoroughly tested + | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` + + vmin: int, optional + minimum value for color scaling, calculated from data if not provided + + vmax: int, optional + maximum value for color scaling, calculated from data if not provided + + cmap: str, optional, default "plasma" + colormap to use to display the image data, ignored if data is RGB + + interpolation: str, optional, default "nearest" + interpolation of the data, one of "nearest" or "linear" + + map_interpolation: str, optional, default "nearest" + interpolation of the cmap, one of "nearest" or "linear" + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer. + + args: + additional arguments passed to Graphic + + kwargs: + additional keyword arguments passed to Graphic + + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the data buffer displayed in the ImageGraphic + + **cmap**: :class:`.ImageCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + + + """ + return self._create_graphic(VolumeGraphic, data, vmin, vmax, cmap, interpolation, map_interpolation, isolated_buffer, *args, **kwargs) +