Source code for napari.components.dims

from typing import Sequence, Tuple, Union

import numpy as np
from pydantic import root_validator, validator
from typing_extensions import Literal  # Added to typing in 3.8

from ..utils.events import EventedModel


[docs]class Dims(EventedModel): """Dimensions object modeling slicing and displaying. Parameters ---------- ndim : int Number of dimensions. ndisplay : int Number of displayed dimensions. last_used : int Dimension which was last used. range : tuple of 3-tuple of float List of tuples (min, max, step), one for each dimension. In a world coordinates space. current_step : tuple of int Tuple the slider position for each dims slider, in slider coordinates. order : tuple of int Tuple of ordering the dimensions, where the last dimensions are rendered. axis_labels : tuple of str Tuple of labels for each dimension. Attributes ---------- ndim : int Number of dimensions. ndisplay : int Number of displayed dimensions. last_used : int Dimension which was last used. range : tuple of 3-tuple of float List of tuples (min, max, step), one for each dimension. In a world coordinates space. current_step : tuple of int Tuple the slider position for each dims slider, in slider coordinates. order : tuple of int Tuple of ordering the dimensions, where the last dimensions are rendered. axis_labels : tuple of str Tuple of labels for each dimension. nsteps : tuple of int Number of steps available to each slider. These are calculated from the ``range``. point : tuple of float List of floats setting the current value of the range slider when in POINT mode, one for each dimension. In a world coordinates space. These are calculated from the ``current_step`` and ``range``. displayed : tuple of int List of dimensions that are displayed. These are calculated from the ``order`` and ``ndisplay``. not_displayed : tuple of int List of dimensions that are not displayed. These are calculated from the ``order`` and ``ndisplay``. displayed_order : tuple of int Order of only displayed dimensions. These are calculated from the ``displayed`` dimensions. """ # fields ndim: int = 2 ndisplay: Literal[2, 3] = 2 last_used: int = 0 range: Tuple[Tuple[float, float, float], ...] = () current_step: Tuple[int, ...] = () order: Tuple[int, ...] = () axis_labels: Tuple[str, ...] = () # private vars _scroll_progress: int = 0 # validators @validator('axis_labels', pre=True) def _string_to_list(v): if isinstance(v, str): return list(v) return v @root_validator def _check_dims(cls, values): """Check the consitency of dimensionaity for all attributes Parameters ---------- values : dict Values dictionary to update dims model with. """ ndim = values['ndim'] # Check the range tuple has same number of elements as ndim if len(values['range']) < ndim: values['range'] = ((0, 2, 1),) * ( ndim - len(values['range']) ) + values['range'] elif len(values['range']) > ndim: values['range'] = values['range'][-ndim:] # Check the current step tuple has same number of elements as ndim if len(values['current_step']) < ndim: values['current_step'] = (0,) * ( ndim - len(values['current_step']) ) + values['current_step'] elif len(values['current_step']) > ndim: values['current_step'] = values['current_step'][-ndim:] # Check the order tuple has same number of elements as ndim if len(values['order']) < ndim: values['order'] = tuple( range(ndim - len(values['order'])) ) + tuple(o + ndim - len(values['order']) for o in values['order']) elif len(values['order']) > ndim: values['order'] = reorder_after_dim_reduction( values['order'][-ndim:] ) # Check the order is a permutation of 0, ..., ndim - 1 if not set(values['order']) == set(range(ndim)): raise ValueError( f"Invalid ordering {values['order']} for {ndim} dimensions" ) # Check the axis labels tuple has same number of elements as ndim if len(values['axis_labels']) < ndim: # Append new "default" labels to existing ones if values['axis_labels'] == tuple( map(str, range(len(values['axis_labels']))) ): values['axis_labels'] = tuple(map(str, range(ndim))) else: values['axis_labels'] = ( tuple(map(str, range(ndim - len(values['axis_labels'])))) + values['axis_labels'] ) elif len(values['axis_labels']) > ndim: values['axis_labels'] = values['axis_labels'][-ndim:] return values @property def nsteps(self) -> Tuple[int, ...]: """Tuple of int: Number of slider steps for each dimension.""" return tuple( int((max_val - min_val) // step_size) + 1 for min_val, max_val, step_size in self.range ) @property def point(self) -> Tuple[int, ...]: """Tuple of float: Value of each dimension.""" # The point value is computed from the range and current_step point = tuple( min_val + step_size * value for (min_val, max_val, step_size), value in zip( self.range, self.current_step ) ) return point @property def displayed(self) -> Tuple[int, ...]: """Tuple: Dimensions that are displayed.""" return self.order[-self.ndisplay :] @property def not_displayed(self) -> Tuple[int, ...]: """Tuple: Dimensions that are not displayed.""" return self.order[: -self.ndisplay] @property def displayed_order(self) -> Tuple[int, ...]: """Tuple: Order of only displayed dimensions.""" order = np.array(self.displayed) order[np.argsort(order)] = list(range(len(order))) return tuple(order)
[docs] def set_range(self, axis: int, _range: Sequence[Union[int, float]]): """Sets the range (min, max, step) for a given dimension. Parameters ---------- axis : int Dimension index. _range : tuple Range specified as (min, max, step). """ axis = assert_axis_in_bounds(axis, self.ndim) if self.range[axis] != _range: full_range = list(self.range) full_range[axis] = _range self.range = full_range self.last_used = axis
[docs] def set_point(self, axis: int, value: Union[int, float]): """Sets point to slice dimension in world coordinates. The desired point gets transformed into an integer step of the slider and stored in the current_step. Parameters ---------- axis : int Dimension index. value : int or float Value of the point. """ axis = assert_axis_in_bounds(axis, self.ndim) (min_val, max_val, step_size) = self.range[axis] raw_step = (value - min_val) / step_size self.set_current_step(axis, raw_step)
[docs] def set_current_step(self, axis: int, value: int): """Sets the slider step at which to slice this dimension. The position of the slider in world coordinates gets calculated from the current_step of the slider. Parameters ---------- axis : int Dimension index. value : int or float Value of the point. """ axis = assert_axis_in_bounds(axis, self.ndim) step = np.round(np.clip(value, 0, self.nsteps[axis] - 1)).astype(int) if self.current_step[axis] != step: full_current_step = list(self.current_step) full_current_step[axis] = step self.current_step = full_current_step self.last_used = axis
[docs] def set_axis_label(self, axis: int, label: str): """Sets a new axis label for the given axis. Parameters ---------- axis : int Dimension index label : str Given label """ axis = assert_axis_in_bounds(axis, self.ndim) if self.axis_labels[axis] != str(label): full_axis_labels = list(self.axis_labels) full_axis_labels[axis] = str(label) self.axis_labels = full_axis_labels self.last_used = axis
[docs] def reset(self): """Reset dims values to initial states.""" # Don't reset axis labels self.range = ((0, 2, 1),) * self.ndim self.current_step = (0,) * self.ndim self.order = tuple(range(self.ndim))
def _increment_dims_right(self, axis: int = None): """Increment dimensions to the right along given axis, or last used axis if None Parameters ---------- axis : int, optional Axis along which to increment dims, by default None """ if axis is None: axis = self.last_used self.set_current_step(axis, self.current_step[axis] + 1) def _increment_dims_left(self, axis: int = None): """Increment dimensions to the left along given axis, or last used axis if None Parameters ---------- axis : int, optional Axis along which to increment dims, by default None """ if axis is None: axis = self.last_used self.set_current_step(axis, self.current_step[axis] - 1) def _focus_up(self): """Shift focused dimension slider to be the next slider above.""" sliders = [d for d in self.not_displayed if self.nsteps[d] > 1] if len(sliders) == 0: return index = (sliders.index(self.last_used) + 1) % len(sliders) self.last_used = sliders[index] def _focus_down(self): """Shift focused dimension slider to be the next slider bellow.""" sliders = [d for d in self.not_displayed if self.nsteps[d] > 1] if len(sliders) == 0: return index = (sliders.index(self.last_used) - 1) % len(sliders) self.last_used = sliders[index] def _roll(self): """Roll order of dimensions for display.""" order = np.array(self.order) nsteps = np.array(self.nsteps) order[nsteps > 1] = np.roll(order[nsteps > 1], 1) self.order = order.tolist() def _transpose(self): """Transpose displayed dimensions.""" order = list(self.order) order[-2], order[-1] = order[-1], order[-2] self.order = order
def reorder_after_dim_reduction(order): """Ensure current dimension order is preserved after dims are dropped. Parameters ---------- order : tuple The data to reorder. Returns ------- arr : tuple The original array with the unneeded dimension thrown away. """ arr = np.array(order) arr[np.argsort(arr)] = range(len(arr)) return tuple(arr.tolist()) def assert_axis_in_bounds(axis: int, ndim: int) -> int: """Assert a given value is inside the existing axes of the image. Returns ------- axis : int The axis which was checked for validity. ndim : int The dimensionality of the layer. Raises ------ ValueError The given axis index is out of bounds. """ if axis not in range(-ndim, ndim): msg = ( f'Axis {axis} not defined for dimensionality {ndim}. ' f'Must be in [{-ndim}, {ndim}).' ) raise ValueError(msg) return axis % ndim