Skip to content

basic image volume #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions examples/notebooks/volume.ipynb
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion fastplotlib/graphics/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
150 changes: 141 additions & 9 deletions fastplotlib/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
65 changes: 61 additions & 4 deletions fastplotlib/layouts/graphic_methods_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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)