Skip to content
Merged
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3662ab6
Removes slider_dims and now always generates slides for the first n -…
apasarkar Mar 28, 2024
a95badc
Adds more checks, removing C from the default dims order
apasarkar Mar 29, 2024
d155c27
Removes some more code that allowed specifying dimensions by int and …
apasarkar Mar 29, 2024
a77a41f
Fixes bug in kwarg initialization of frames_apply in imagewidget wher…
apasarkar Mar 29, 2024
18b423d
In the _process_indices function the search for the array index in th…
apasarkar Mar 29, 2024
d1f35fe
Enforces condition that we can only apply window function along T and…
apasarkar Mar 29, 2024
3e539ca
Includes properly formatted updates from previous commit
apasarkar Mar 30, 2024
6bd6191
Updates some global variable names for clarity such as ALLOWED_DIMS a…
apasarkar Mar 30, 2024
26d62c2
Updates doc strings and error messages for frame_apply and for error …
apasarkar Mar 31, 2024
c9d1ade
Adds check that window function windowsize is an int and also simplif…
apasarkar Mar 31, 2024
060161a
Removes some unreachable error statements, renames internal variables…
apasarkar Mar 31, 2024
ba6f5a4
Includes support for mixed data types and RGB for imagewidget
apasarkar Apr 2, 2024
0d217b0
Simplifies the code by removing dims_partition and replacing it with …
apasarkar Apr 3, 2024
1bc7190
Simplifies the imwidget api by replacing color_schemes with rgb_disp,…
apasarkar Apr 3, 2024
5b82074
Removes old global variables and color scheme function
apasarkar Apr 9, 2024
1002857
Renames global variables for dimension counts to be clearer, changes …
apasarkar Apr 9, 2024
3c3f0dc
Adds extra check to make sure rgb input list for imagewidget is same …
apasarkar Apr 9, 2024
54d8552
Updates get_n_scrollable_dims function names and docstrings
apasarkar Apr 9, 2024
cfe4bd6
Updates the function to get number of scrollable dimensions for each …
apasarkar Apr 9, 2024
5ff549e
Updates the set_data code, removing hard to maintain constants, updat…
apasarkar Apr 9, 2024
1d75b9b
Fixes bug that did not update slider dimensions properly and also inc…
apasarkar Apr 9, 2024
cbd47c2
Updates documentation, removes unnecessary public variables
apasarkar Apr 10, 2024
2360142
Merge branch 'main' into refactor_imwidget
apasarkar Apr 10, 2024
5ce336f
fix indexing
kushalkolar Apr 10, 2024
8679910
fix test
kushalkolar Apr 10, 2024
db5f5b4
Update image_widget_test.ipynb
kushalkolar Apr 10, 2024
491aa1f
black
kushalkolar Apr 10, 2024
29ed277
Includes new testing for imagewidget rgb and mixed types
apasarkar Apr 11, 2024
cf3cba1
Overrides synced controllers for gridplot imagewidget test
apasarkar Apr 11, 2024
eba97f7
Includes black after updating the tests
apasarkar Apr 11, 2024
8fa21de
Includes histogram widget in the new tests for rgb and mixed types
apasarkar Apr 11, 2024
aea6d56
Apply suggestions from code review
kushalkolar Apr 12, 2024
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
279 changes: 67 additions & 212 deletions fastplotlib/widgets/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
2: "xy",
3: "txy",
4: "tzxy",
5: "tzcxy",
}

ALLOWED_SLIDER_DIMS = {0: "t", 1: "z"}

ALLOWED_WINDOW_DIMS = {"t", "z"}


def _is_arraylike(obj) -> bool:
"""
Expand Down Expand Up @@ -231,8 +234,6 @@ def current_index(self, index: Dict[str, int]):
def __init__(
self,
data: Union[np.ndarray, List[np.ndarray]],
dims_order: Union[str, Dict[int, str]] = None,
slider_dims: Union[str, int, List[Union[str, int]]] = None,
window_funcs: Union[int, Dict[str, int]] = None,
frame_apply: Union[callable, Dict[int, callable]] = None,
grid_shape: Tuple[int, int] = None,
Expand All @@ -242,12 +243,11 @@ def __init__(
**kwargs,
):
"""
A high level widget for displaying n-dimensional image data in conjunction with automatically generated
sliders for navigating through 1-2 selected dimensions within image data.

Can display a single n-dimensional image array or a grid of n-dimensional images.
This widget facilitates high-level navigation through image stacks, which are arrays containing one or more
images. It includes sliders for key dimensions such as "t" (time) and "z", enabling users to smoothly navigate
through one or multiple image stacks simultaneously.

Default dimension orders:
Allowed dimensions orders for each image stack:

======= ==========
n_dims dims order
Expand All @@ -262,31 +262,18 @@ def __init__(
data: Union[np.ndarray, List[np.ndarray]
array-like or a list of array-like

dims_order: Optional[Union[str, Dict[np.ndarray, str]]]
| ``str`` or a dict mapping to indicate dimension order
| a single ``str`` if ``data`` is a single array, or a list of arrays with the same dimension order
| examples: ``"xyt"``, ``"tzxy"``
| ``dict`` mapping of ``{array_index: axis_order}`` if specific arrays have a non-default axes order.
| "array_index" is the position of the corresponding array in the data list.
| examples: ``{array_index: "tzxy", another_array_index: "xytz"}``

slider_dims: Optional[Union[str, int, List[Union[str, int]]]]
| The dimensions for which to create a slider
| can be a single ``str`` such as **"t"**, **"z"** or a numerical ``int`` that indexes the desired dimension
| can also be a list of ``str`` or ``int`` if multiple sliders are desired for multiple dimensions
| examples: ``"t"``, ``["t", "z"]``

window_funcs: Dict[Union[int, str], int]
| average one or more dimensions using a given window
| if a slider exists for only one dimension this can be an ``int``.
| if multiple sliders exist, then it must be a `dict`` mapping in the form of: ``{dimension: window_size}``
| dimension/axes can be specified using ``str`` such as "t", "z" etc. or ``int`` that indexes the dimension
| if window_size is not an odd number, adds 1
| use ``None`` to disable averaging for a dimension, example: ``{"t": 5, "z": None}``
| Apply function(s) with rolling windows along "t" and/or "z" dimensions of the `data` arrays.
| Pass a dict in the form: {dimension: (func, window_size)}, `func` must take a slice of the data array as the
| first argument and must take `axis` as a kwarg.
| Ex: mean along "t" dimension: {"t": (np.mean, 11)}, if `current_index` of "t" is 50, it will pass frames
| 45 to 55 to `np.mean` with `axis = 0`.
| Ex2: max along z dim: {"z": (np.max, 3)}, passes current, previous and next frame to `np.max` with `axis = 1`

frame_apply: Union[callable, Dict[int, callable]]
| apply a function to slices of the array before displaying the frame
| pass a single function or a dict of functions to apply to each array individually
| Apply function(s) to `data` arrays before to generate final 2D image that is displayed.
| Ex: apply a spatial Gaussian filter, image rescaling
Comment thread
apasarkar marked this conversation as resolved.
Outdated
| Pass a single function or a dict of functions to apply to each array individually
| examples: ``{array_index: to_grayscale}``, ``{0: to_grayscale, 2: threshold_img}``
Comment thread
apasarkar marked this conversation as resolved.
| "array_index" is the position of the corresponding array in the data list.
| if `window_funcs` is used, then this function is applied after `window_funcs`
Expand All @@ -309,7 +296,6 @@ def __init__(
passed to fastplotlib.graphics.Image

"""

self._names = None

# output context
Expand Down Expand Up @@ -372,149 +358,30 @@ def __init__(
f"You have passed the following type {type(data)}"
)

# default dims order if not passed
# updated later if passed
if self.ndim not in DEFAULT_DIMS_ORDER.keys():
raise ValueError(
f"{self.ndim} dimensions not supported "
f"only xy, txy, and tzxy data with or without RGB(A) is supported"
)
self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len(self.data)

if dims_order is not None:
if isinstance(dims_order, str):
dims_order = dims_order.lower()
if len(dims_order) != self.ndim:
raise ValueError(
f"number of dims '{len(dims_order)} passed to `dims_order` "
f"does not match ndim '{self.ndim}' of data"
)
self._dims_order: List[str] = [dims_order] * len(self.data)
elif isinstance(dims_order, dict):
self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len(
self.data
)

# dict of {array_ix: dims_order_str}
for data_ix in list(dims_order.keys()):
if not isinstance(data_ix, int):
raise TypeError("`dims_order` dict keys must be <int>")
if len(dims_order[data_ix]) != self.ndim:
raise ValueError(
f"number of dims '{len(dims_order)} passed to `dims_order` "
f"does not match ndim '{self.ndim}' of data"
)
_do = dims_order[data_ix].lower()
# make sure the same dims are present
if not set(_do) == set(DEFAULT_DIMS_ORDER[self.ndim]):
raise ValueError(
f"Invalid `dims_order` passed for one of your arrays, "
f"valid `dims_order` for given number of dimensions "
f"can only contain the following characters: "
f"{DEFAULT_DIMS_ORDER[self.ndim]}"
)
try:
self.dims_order[data_ix] = _do
except Exception:
raise IndexError(
f"index {data_ix} out of bounds for `dims_order`, the bounds are 0 - {len(self.data)}"
)
else:
raise TypeError(
f"`dims_order` must be a <str> or <Dict[int: str]>, you have passed a: <{type(dims_order)}>"
)

if not len(self.dims_order[0]) == self.ndim:
raise ValueError(
f"Number of dims specified by `dims_order`: {len(self.dims_order[0])} does not"
f" match number of dimensions in the `data`: {self.ndim}"
)

ao = np.array([sorted(v) for v in self.dims_order])

if not np.all(ao == ao[0]):
raise ValueError(
f"`dims_order` for all arrays must contain the same combination of dimensions, your `dims_order` are: "
f"{self.dims_order}"
)

Comment thread
apasarkar marked this conversation as resolved.
# if slider_dims not provided
if slider_dims is None:
# by default sliders are made for all dimensions except the last 2
default_dim_names = {0: "t", 1: "z", 2: "c"}
slider_dims = list()
for dim in range(self.ndim - 2):
if dim in default_dim_names.keys():
slider_dims.append(default_dim_names[dim])
else:
slider_dims.append(f"{dim}")

# slider for only one of the dimensions
if isinstance(slider_dims, (int, str)):
# if numerical dimension is specified
if isinstance(slider_dims, int):
ao = np.array([v for v in self.dims_order])
if not np.all(ao == ao[0]):
raise ValueError(
f"`dims_order` for all arrays must be identical if passing in a <int> `slider_dims` argument. "
f"Pass in a <str> argument if the `dims_order` are different for each array."
)
self._slider_dims: List[str] = [self.dims_order[0][slider_dims]]

# if dimension specified by str
elif isinstance(slider_dims, str):
if slider_dims not in self.dims_order[0]:
raise ValueError(
f"if `slider_dims` is a <str>, it must be a character found in `dims_order`. "
f"Your `dims_order` characters are: {set(self.dims_order[0])}."
)
self._slider_dims: List[str] = [slider_dims]

# multiple sliders, one for each dimension
elif isinstance(slider_dims, list):
self._slider_dims: List[str] = list()

# make sure window_funcs and frame_apply are dicts if multiple sliders are desired
if (not isinstance(window_funcs, dict)) and (window_funcs is not None):
raise TypeError(
f"`window_funcs` must be a <dict> if multiple `slider_dims` are provided. You must specify the "
f"window for each dimension."
)
if (not isinstance(frame_apply, dict)) and (frame_apply is not None):
raise TypeError(
f"`frame_apply` must be a <dict> if multiple `slider_dims` are provided. You must specify a "
f"function for each dimension."
)

for sdm in slider_dims:
if isinstance(sdm, int):
ao = np.array([v for v in self.dims_order])
if not np.all(ao == ao[0]):
raise ValueError(
f"`dims_order` for all arrays must be identical if passing in a <int> `slider_dims` argument. "
f"Pass in a <str> argument if the `dims_order` are different for each array."
)
# parse int to a str
self.slider_dims.append(self.dims_order[0][sdm])

elif isinstance(sdm, str):
if sdm not in self.dims_order[0]:
raise ValueError(
f"if `slider_dims` is a <str>, it must be a character found in `dims_order`. "
f"Your `dims_order` characters are: {set(self.dims_order[0])}."
)
self.slider_dims.append(sdm)

else:
raise TypeError(
"If passing a list for `slider_dims` each element must be either an <int> or <str>"
)

else:
raise TypeError(
f"`slider_dims` must a <int>, <str> or <list>, you have passed a: {type(slider_dims)}"
)
# Sliders are made for all dimensions except the last 2
self._slider_dims = list()
for dim in range(self.ndim):
if dim in ALLOWED_SLIDER_DIMS.keys():
self.slider_dims.append(ALLOWED_SLIDER_DIMS[dim])

self._frame_apply: Dict[int, callable] = dict()

if frame_apply is not None:
if callable(frame_apply):
self._frame_apply = {0: frame_apply}
self._frame_apply = frame_apply

elif isinstance(frame_apply, dict):
self._frame_apply: Dict[int, callable] = dict.fromkeys(
Expand Down Expand Up @@ -615,62 +482,54 @@ def window_funcs(self) -> Dict[str, _WindowFunctions]:
return self._window_funcs

@window_funcs.setter
def window_funcs(self, sa: Union[int, Dict[str, int]]):
if sa is None:
def window_funcs(self, callable_dict: Dict[str, int]):
Comment thread
apasarkar marked this conversation as resolved.
if callable_dict is None:
self._window_funcs = None
# force frame to update
self.current_index = self.current_index
return

# for a single dim
elif isinstance(sa, tuple):
if len(self.slider_dims) > 1:
raise TypeError(
"Must pass dict argument to window_funcs if using multiple sliders. See the docstring."
)
if not callable(sa[0]) or not isinstance(sa[1], int):
raise TypeError(
"Tuple argument to `window_funcs` must be in the form of (func, window_size). See the docstring."
elif isinstance(callable_dict, dict):
if not set(callable_dict.keys()).issubset(ALLOWED_WINDOW_DIMS):
raise ValueError(
f"The only allowed keys to window funcs are {list(ALLOWED_WINDOW_DIMS)} "
f"Your window func passed in these keys: {list(callable_dict.keys())}"
)

dim_str = self.slider_dims[0]
self._window_funcs = dict()
self._window_funcs[dim_str] = _WindowFunctions(self, *sa)

# for multiple dims
elif isinstance(sa, dict):
if not all(
[isinstance(_sa, tuple) or (_sa is None) for _sa in sa.values()]
[
isinstance(_callable_dict, tuple)
for _callable_dict in callable_dict.values()
]
):
raise TypeError(
"dict argument to `window_funcs` must be in the form of: "
"`{dimension: (func, window_size)}`. "
"See the docstring."
)
for v in sa.values():
if v is not None:
if not callable(v[0]) or not (
isinstance(v[1], int) or v[1] is None
):
raise TypeError(
"dict argument to `window_funcs` must be in the form of: "
"`{dimension: (func, window_size)}`. "
"See the docstring."
)
for v in callable_dict.values():
if not callable(v[0]):
Comment thread
apasarkar marked this conversation as resolved.
raise TypeError(
"dict argument to `window_funcs` must be in the form of: "
"`{dimension: (func, window_size)}`. "
"See the docstring."
)
if not isinstance(v[1], int):
raise TypeError(
f"dict argument to `window_funcs` must be in the form of: "
"`{dimension: (func, window_size)}`. "
f"where window_size is integer. you passed in {v[1]} for window_size"
)

if not isinstance(self._window_funcs, dict):
self._window_funcs = dict()

for k in list(sa.keys()):
if sa[k] is None:
self._window_funcs[k] = None
else:
self._window_funcs[k] = _WindowFunctions(self, *sa[k])
for k in list(callable_dict.keys()):
self._window_funcs[k] = _WindowFunctions(self, *callable_dict[k])

else:
raise TypeError(
f"`window_funcs` must be of type `int` if using a single slider or a dict if using multiple sliders. "
f"You have passed a {type(sa)}. See the docstring."
f"`window_funcs` must be either Nonetype or dict."
f"You have passed a {type(callable_dict)}. See the docstring."
)

# force frame to update
Expand All @@ -692,7 +551,7 @@ def _process_indices(
dict in form of {dimension_index: slice_index}
For example if an array has shape [1000, 30, 512, 512] corresponding to [t, z, x, y]:
To get the 100th timepoint and 3rd z-plane pass:
{"t": 100, "z": 3}, or {0: 100, 1: 3}
{"t": 100, "z": 3}

Returns
-------
Expand All @@ -703,23 +562,20 @@ def _process_indices(
indexer = [slice(None)] * self.ndim

numerical_dims = list()

data_ix = None
for i in range(len(self.data)):
if self.data[i] is array:
data_ix = i
break

for dim in list(slice_indices.keys()):
if isinstance(dim, str):
data_ix = None
for i in range(len(self.data)):
if self.data[i] is array:
data_ix = i
break
if data_ix is None:
raise ValueError(f"Given `array` not found in `self.data`")
# get axes order for that specific array
numerical_dim = self.dims_order[data_ix].index(dim)
else:
numerical_dim = dim
# get axes order for that specific array
numerical_dim = self.dims_order[data_ix].index(dim)

indices_dim = slice_indices[dim]

# takes care of averaging if it was specified
# takes care of index selection (window slicing) for this specific axis
indices_dim = self._get_window_indices(data_ix, numerical_dim, indices_dim)

# set the indices for this dimension
Expand All @@ -745,7 +601,6 @@ def _process_indices(
func = self.window_funcs[dim_str].func
window = a[tuple(_indexer)]
a = func(window, axis=dim)
# a = np.mean(a[tuple(_indexer)], axis=dim)
return a
else:
return array[tuple(indexer)]
Expand Down