Skip to content

feat: add circular/ring buffer and data wrapper, that can be used to facilitate a streaming viewer #186

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
33 changes: 33 additions & 0 deletions examples/streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# /// script
# dependencies = [
# "ndv[pyqt,pygfx]",
# "imageio[tifffile]",
# ]
# ///
import ndv
from ndv.models import RingBuffer

# some data we're going to stream (as if it was coming from a camera)
data = ndv.data.cells3d()[:, 1]

# a ring buffer to hold the data as it comes in
# the ring buffer is a circular buffer that holds the last N frames
rb = RingBuffer(max_capacity=20, dtype=(data.dtype, data.shape[-2:]))

# pass the ring buffer to the viewer
viewer = ndv.ArrayViewer(rb)
viewer.show()


# function that will be called after the app is running
def stream() -> None:
# iterate over the data, add it to the ring buffer, and update the viewer index
for n, plane in enumerate(data):
rb.append(plane)
viewer.display_model.current_index.update({0: max(n, 20 - 1)})

ndv.process_events() # force viewer updates for this example


ndv.call_later(200, stream)
ndv.run_app()
10 changes: 9 additions & 1 deletion src/ndv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@
from .controllers import ArrayViewer
from .models import DataWrapper
from .util import imshow
from .views import run_app, set_canvas_backend, set_gui_backend
from .views import (
call_later,
process_events,
run_app,
set_canvas_backend,
set_gui_backend,
)

__all__ = [
"ArrayViewer",
"DataWrapper",
"call_later",
"data",
"imshow",
"process_events",
"run_app",
"set_canvas_backend",
"set_gui_backend",
Expand Down
3 changes: 3 additions & 0 deletions src/ndv/controllers/_array_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,9 @@

display_model = self._data_model.display
for key, data in response.data.items():
if data.size == 0:
# no data for this channel
continue

Check warning on line 562 in src/ndv/controllers/_array_viewer.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/controllers/_array_viewer.py#L562

Added line #L562 was not covered by tests
if (lut_ctrl := self._lut_controllers.get(key)) is None:
if key is None:
model = display_model.default_lut
Expand Down
5 changes: 4 additions & 1 deletion src/ndv/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from ._array_display_model import ArrayDisplayModel, ChannelMode
from ._base_model import NDVModel
from ._data_wrapper import DataWrapper
from ._data_wrapper import DataWrapper, RingBufferWrapper
from ._lut_model import (
ClimPolicy,
ClimsManual,
Expand All @@ -11,6 +11,7 @@
ClimsStdDev,
LUTModel,
)
from ._ring_buffer import RingBuffer
from ._roi_model import RectangularROIModel

__all__ = [
Expand All @@ -25,4 +26,6 @@
"LUTModel",
"NDVModel",
"RectangularROIModel",
"RingBuffer",
"RingBufferWrapper",
]
61 changes: 61 additions & 0 deletions src/ndv/models/_data_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import numpy.typing as npt
from psygnal import Signal

from ._ring_buffer import RingBuffer

if TYPE_CHECKING:
from collections.abc import Container, Iterator
from typing import Any, TypeAlias, TypeGuard
Expand Down Expand Up @@ -498,3 +500,62 @@
if (torch := sys.modules.get("torch")) and isinstance(obj, torch.Tensor):
return True
return False


class RingBufferWrapper(DataWrapper[RingBuffer]):
"""Wrapper for ring buffer objects."""

def __init__(
self,
max_capacity: int | RingBuffer,
dtype: npt.DTypeLike = None,
*,
allow_overwrite: bool = True,
):
if isinstance(max_capacity, RingBuffer):

Check warning on line 515 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L515

Added line #L515 was not covered by tests
if dtype is not None: # pragma: no cover
raise ValueError(
"Cannot specify dtype when passing an existing RingBuffer."
)
self._ring = max_capacity

Check warning on line 520 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L520

Added line #L520 was not covered by tests
else:
if dtype is None:
dtype = float
self._ring = RingBuffer(

Check warning on line 524 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L522-L524

Added lines #L522 - L524 were not covered by tests
max_capacity=max_capacity, dtype=dtype, allow_overwrite=allow_overwrite
)
self._ring.resized.connect(self.dims_changed)
super().__init__(self._ring)

Check warning on line 528 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L527-L528

Added lines #L527 - L528 were not covered by tests

@property
def dims(self) -> tuple[int, ...]:
"""Return the dimensions of the data."""
return tuple(range(len(self._ring.shape)))

@property
def coords(self) -> Mapping:
"""Return the coordinates for the data."""
shape = self._ring.shape
return {i: range(s) for i, s in enumerate(shape)}

Check warning on line 539 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L538-L539

Added lines #L538 - L539 were not covered by tests

@classmethod
def supports(cls, obj: Any) -> TypeGuard[np.ndarray]:
if isinstance(obj, RingBuffer):
return True

Check warning on line 544 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L544

Added line #L544 was not covered by tests
return False

def append(self, value: npt.ArrayLike) -> None:
"""Append a value to the right end of the buffer."""
self._ring.append(value)

Check warning on line 549 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L549

Added line #L549 was not covered by tests

def appendleft(self, value: npt.ArrayLike) -> None:
"""Append a value to the left end of the buffer."""
self._ring.appendleft(value)

Check warning on line 553 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L553

Added line #L553 was not covered by tests

def pop(self) -> np.ndarray:
"""Pop a value from the right end of the buffer."""
return self._ring.pop()

Check warning on line 557 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L557

Added line #L557 was not covered by tests

def popleft(self) -> np.ndarray:
"""Pop a value from the left end of the buffer."""
return self._ring.popleft()

Check warning on line 561 in src/ndv/models/_data_wrapper.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/models/_data_wrapper.py#L561

Added line #L561 was not covered by tests
Loading
Loading