Skip to content

Commit

Permalink
3d chunks!
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Jun 11, 2024
1 parent aeb0f4d commit 351e7ad
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 43 deletions.
3 changes: 3 additions & 0 deletions src/ndv/_chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@ def _request_chunk_sync(
# this needs to be aware of nvisible dimensions
try:
offset = tuple(int(getattr(sl, "start", sl)) for sl in idx)[-3:]
offset = (idx[0].start, idx[2].start, idx[3].start)
except TypeError:
offset = (0, 0, 0)
import time

time.sleep(0.05)
return ChunkResponse(
idx=idx, data=data, offset=offset, channel_index=channel_index
)
Expand Down
3 changes: 1 addition & 2 deletions src/ndv/viewer/_backends/_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PImageHandle(Protocol):
def data(self) -> np.ndarray: ...
@data.setter
def data(self, data: np.ndarray) -> None: ...
def set_data(self, data: np.ndarray, offset: tuple) -> None: ...
@property
def visible(self) -> bool: ...
@visible.setter
Expand Down Expand Up @@ -44,11 +45,9 @@ def add_image(
self,
data: np.ndarray | None = ...,
cmap: cmap.Colormap | None = ...,
offset: tuple[float, float] | None = None, # (Y, X)
) -> PImageHandle: ...
def add_volume(
self,
data: np.ndarray | None = ...,
cmap: cmap.Colormap | None = ...,
offset: tuple[float, float, float] | None = ..., # (Z, Y, X)
) -> PImageHandle: ...
10 changes: 9 additions & 1 deletion src/ndv/viewer/_backends/_vispy.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def data(self, data: np.ndarray) -> None:
return
self._visual.set_data(data)

def set_data(self, data: np.ndarray, offset: tuple) -> None:
print("Setting data", data.shape, offset)
self._visual._texture._set_data(data, offset=offset)

@property
def visible(self) -> bool:
return bool(self._visual.visible)
Expand Down Expand Up @@ -161,7 +165,11 @@ def add_volume(
offset: tuple[float, float, float] | None = None, # (Z, Y, X)
) -> VispyImageHandle:
vol = scene.visuals.Volume(
data, parent=self._view.scene, interpolation="nearest"
data,
parent=self._view.scene,
interpolation="nearest",
texture_format="auto",
clim=(0, 40000),
)
vol.set_gl_state("additive", depth_test=False)
vol.interactive = True
Expand Down
40 changes: 32 additions & 8 deletions src/ndv/viewer/_lut_control.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Mapping, cast
from typing import TYPE_CHECKING, Any, Sequence, cast

import numpy as np
from qtpy.QtCore import Qt
Expand Down Expand Up @@ -35,7 +35,7 @@ def showPopup(self) -> None:
class LutControl(QWidget):
def __init__(
self,
channel: Mapping[Any, PImageHandle],
channel: Sequence[PImageHandle],
name: str = "",
parent: QWidget | None = None,
cmaplist: Iterable[Any] = (),
Expand All @@ -51,7 +51,7 @@ def __init__(

self._cmap = CmapCombo()
self._cmap.currentColormapChanged.connect(self._on_cmap_changed)
for handle in channel.values():
for handle in channel:
self._cmap.addColormap(handle.cmap)
for color in cmaplist:
self._cmap.addColormap(color)
Expand Down Expand Up @@ -87,17 +87,17 @@ def autoscaleChecked(self) -> bool:

def _on_clims_changed(self, clims: tuple[float, float]) -> None:
self._auto_clim.setChecked(False)
for handle in self._channel.values():
for handle in self._channel:
handle.clim = clims

def _on_visible_changed(self, visible: bool) -> None:
for handle in self._channel.values():
for handle in self._channel:
handle.visible = visible
if visible:
self.update_autoscale()

def _on_cmap_changed(self, cmap: cmap.Colormap) -> None:
for handle in self._channel.values():
for handle in self._channel:
handle.cmap = cmap

def update_autoscale(self) -> None:
Expand All @@ -110,17 +110,41 @@ def update_autoscale(self) -> None:

# find the min and max values for the current channel
clims = [np.inf, -np.inf]
for handle in self._channel.values():
for handle in self._channel:
clims[0] = min(clims[0], np.nanmin(handle.data))
clims[1] = max(clims[1], np.nanmax(handle.data))

mi, ma = tuple(int(x) for x in clims)
if mi != ma:
for handle in self._channel.values():
for handle in self._channel:
handle.clim = (mi, ma)

# set the slider values to the new clims
with signals_blocked(self._clims):
self._clims.setMinimum(min(mi, self._clims.minimum()))
self._clims.setMaximum(max(ma, self._clims.maximum()))
self._clims.setValue((mi, ma))


def _get_default_clim_from_data(data: np.ndarray) -> tuple[float, float]:
"""Compute a reasonable clim from the min and max, taking nans into account.
If there are no non-finite values (nan, inf, -inf) this is as fast as it can be.
Otherwise, this functions is about 3x slower.
"""
# Fast
min_value = data.min()
max_value = data.max()

# Need more work? The nan-functions are slower
min_finite = np.isfinite(min_value)
max_finite = np.isfinite(max_value)
if not (min_finite and max_finite):
finite_data = data[np.isfinite(data)]
if finite_data.size:
min_value = finite_data.min()
max_value = finite_data.max()
else:
min_value = max_value = 0 # no finite values in the data

return min_value, max_value
84 changes: 52 additions & 32 deletions src/ndv/viewer/_viewer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from __future__ import annotations

from itertools import cycle
from typing import TYPE_CHECKING, Iterator, Literal, Mapping, Sequence, cast
from typing import (
TYPE_CHECKING,
Hashable,
Literal,
MutableSequence,
Sequence,
SupportsIndex,
cast,
overload,
)

import cmap
import numpy as np
from qtpy.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from superqt import QCollapsible, QElidingLabel, QIconifyIcon, ensure_main_thread
from superqt.utils import qthrottled, signals_blocked
Expand All @@ -23,7 +33,7 @@
from ._lut_control import LutControl

if TYPE_CHECKING:
from typing import Any, Hashable, Iterable, TypeAlias
from typing import Any, Iterable, TypeAlias

from qtpy.QtGui import QCloseEvent

Expand All @@ -49,28 +59,40 @@
MONO_CHANNEL = -999999


class Channel(Mapping[tuple, PImageHandle]):
def __init__(
self, ch_key: int, canvas: PCanvas, cmap: cmap.Colormap = GRAYS
) -> None:
class Channel(MutableSequence[PImageHandle]):
def __init__(self, ch_key: int, cmap: cmap.Colormap = GRAYS) -> None:
self.ch_key = ch_key
self._handles: dict[Any, PImageHandle] = {}
self._handles: list[PImageHandle] = []
self.cmap = cmap

def __getitem__(self, key: tuple) -> PImageHandle:
return self._handles[key]

def __setitem__(self, key: tuple, value: PImageHandle) -> None:
self._handles[key] = value
@overload
def __getitem__(self, i: int) -> PImageHandle: ...
@overload
def __getitem__(self, i: slice) -> list[PImageHandle]: ...
def __getitem__(self, i: int | slice) -> PImageHandle | list[PImageHandle]:
return self._handles[i]

@overload
def __setitem__(self, i: SupportsIndex, value: PImageHandle) -> None: ...
@overload
def __setitem__(self, i: slice, value: Iterable[PImageHandle]) -> None: ...
def __setitem__(
self, i: SupportsIndex | slice, value: PImageHandle | Iterable[PImageHandle]
) -> None:
self._handles[i] = value # type: ignore

def __iter__(self) -> Iterator[tuple]:
yield from self._handles
@overload
def __delitem__(self, i: int) -> None: ...
@overload
def __delitem__(self, i: slice) -> None: ...
def __delitem__(self, i: int | slice) -> None:
del self._handles[i]

def __len__(self) -> int:
return len(self._handles)

def __contains__(self, key: object) -> bool:
return key in self._handles
def insert(self, i: int, value: PImageHandle) -> None:
self._handles.insert(i, value)


class NDViewer(QWidget):
Expand Down Expand Up @@ -169,7 +191,7 @@ def __init__(
# IMPORTANT
# chunking here will determine how non-visualized dims are reduced
# so chunkshape will need to change based on the set of visualized dims
chunks=32,
chunks=(20, 100, 32, 32),
on_ready=self._draw_chunk,
)

Expand Down Expand Up @@ -483,33 +505,31 @@ def _draw_chunk(self, chunk: ChunkResponse) -> None:
else:
ch_key = chunk.channel_index

data = chunk.data
if data.ndim == 2:
return
# TODO: Channel object creation could be moved.
# having it here is the laziest... but means that the order of arrival
# of the chunks will determine the order of the channels in the LUTS
# (without additional logic to sort them by index, etc.)
if (channel := self._channels.get(ch_key)) is None:
channel = self._create_channel(ch_key)
if (handles := self._channels.get(ch_key)) is None:
handles = self._create_channel(ch_key)

data = chunk.data
if (offset := chunk.offset) in channel:
channel[offset].data = data
else:
print(f"{data.ndim=}")
if not handles:
if data.ndim == 2:
_offset2 = (offset[-2], offset[-1]) if offset else None
handle = self._canvas.add_image(data, offset=_offset2)
handles.append(self._canvas.add_image(data, cmap=handles.cmap))
elif data.ndim == 3:
_offset3 = (offset[-3], offset[-2], offset[-1]) if offset else None
handle = self._canvas.add_volume(data, offset=_offset3)
handle.cmap = channel.cmap
channel[offset] = handle
empty = np.empty((60, 256, 256), dtype=np.uint16)
handles.append(self._canvas.add_volume(empty, cmap=handles.cmap))

handles[0].set_data(data, chunk.offset)
self._canvas.refresh()

def _create_channel(self, ch_key: int) -> Channel:
# improve this
cmap = GRAYS if ch_key == MONO_CHANNEL else next(self._cmap_cycle)

self._channels[ch_key] = channel = Channel(ch_key, self._canvas, cmap=cmap)
self._channels[ch_key] = channel = Channel(ch_key, cmap=cmap)
self._lut_ctrls[ch_key] = lut = LutControl(
channel,
f"Ch {ch_key}",
Expand All @@ -523,7 +543,7 @@ def _create_channel(self, ch_key: int) -> Channel:
def _clear_images(self) -> None:
"""Remove all images from the canvas."""
for handles in self._channels.values():
for handle in handles.values():
for handle in handles:
handle.remove()
self._channels.clear()

Expand Down
33 changes: 33 additions & 0 deletions xx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import numpy as np
from rich import print
from vispy import app, io, scene

from ndv._chunking import iter_chunk_aligned_slices

vol1 = np.load(io.load_data_file("volume/stent.npz"))["arr_0"].astype(np.uint16)

canvas = scene.SceneCanvas(keys="interactive", size=(800, 600), show=True)
view = canvas.central_widget.add_view()
print("--------- create vol")
volume1 = scene.Volume(
np.empty_like(vol1), parent=view.scene, texture_format="auto", clim=(0, 1200)
)
print("--------- create cam")
view.camera = scene.cameras.ArcballCamera(parent=view.scene, name="Arcball")

# Generate new data to update a subset of the volume


slices = iter_chunk_aligned_slices(
vol1.shape, chunks=(32, 32, 32), slices=(slice(None), slice(None), slice(None))
)

for slice in list(slices)[::1]:
offset = (x.start for x in slice)
chunk = vol1[slice]
# Update the texture with the new data at the calculated offset
print("--------- update vol")
volume1._texture._set_data(chunk, offset=tuple(offset))
canvas.update()
print("--------- run app")
app.run()

0 comments on commit 351e7ad

Please sign in to comment.