Source code for ClearMap.gui.params_mixins

"""
Mixins for params classes
The `OrthoviewerSlicingMixin` provides orthoviewer-related functionality.
"""
from typing import Protocol, runtime_checkable, Literal, Any, Optional, Mapping, List

import numpy as np

from ClearMap.IO.assets_constants import CONTENT_TYPE_TO_PIPELINE
from ClearMap.gui.params_interfaces import ParamLink


[docs] @runtime_checkable class HasSlicingFields(Protocol): """Implemented by params classes that expose crop_* fields.""" crop_x: List[int] crop_y: List[int] crop_z: List[int]
[docs] class OrthoviewerSlicingMixin: """ Provides: - ratios -> np.ndarray([rx, ry, rz]) - scale_axis(val, axis='x') / reverse_scale_axis(val, axis='x') - slice_tuples / slicing (if HasSlicingFields) Defaults to ratios = resampled_resolution / raw_resolution. Host must provide `_get_view()`. If the host has `name`, that channel is used. If not, set `pipeline = 'TubeMap'|'CellMap'|...` OR set `reference_channel` explicitly. The first channel matching the pipeline (via CONTENT_TYPE_TO_PIPELINE) is used as fallback. Tunables: - _target_space: 'resampled' | 'native' (default 'resampled') - set_target_space('resampled'|'native') """ pipeline: Optional[str] = None reference_channel: Optional[str] = None _target_space: Literal['resampled', 'native'] = 'resampled'
[docs] def set_target_space(self, space: Literal['resampled', 'native']): if space not in ('resampled', 'native'): raise ValueError("space must be 'resampled' or 'native'") self._target_space = space
def _sample_channels_cfg(self) -> Mapping[str, Any]: view = self._get_view() return view['sample'].get('channels', {}) def _raw_resolution(self, channel_name: str) -> np.ndarray: res = self._sample_channels_cfg().get(channel_name, {}).get('resolution', [1.0, 1.0, 1.0]) return np.array(res, dtype=float) def _resampled_res(self, channel_name: str) -> np.ndarray[Any, np.dtype[Any]]: view = self._get_view() res = view['registration']['channels'][channel_name]['resampled_resolution'] return np.array(res, dtype=float) def _target_resolution(self, channel_name: str) -> np.ndarray: if self._target_space == 'resampled': return self._resampled_res(channel_name) else: return self._raw_resolution(channel_name) def _pick_reference_channel(self) -> Optional[str]: # 1) explicit override if isinstance(self.reference_channel, str): return self.reference_channel # 2) per-channel objects (ChannelUiParameters) channel_name = getattr(self, 'name', None) if channel_name is not None and channel_name: return channel_name # 3) pipeline-level objects -> derive from relevant channels pipeline_name = getattr(self, 'pipeline', None) if pipeline_name is not None: for ch, cfg in self._sample_channels_cfg().items(): dt = cfg.get('data_type') if dt and CONTENT_TYPE_TO_PIPELINE.get(dt) == pipeline_name: # FIXME: use self.relevant_channels return ch # 4) last resort chans = list(self._sample_channels_cfg().keys()) return chans[0] if chans else None @property def ratios(self) -> np.ndarray: channel = self._pick_reference_channel() raw_res = self._raw_resolution(channel) tgt_res = self._target_resolution(channel) with np.errstate(divide='ignore', invalid='ignore'): r = tgt_res / raw_res r[~np.isfinite(r)] = 1.0 return r
[docs] def scale_axis(self, val: float | int, axis: str = 'x') -> int: axis_ratio = float(self.ratios['xyz'.index(axis)]) scaled = round(val * axis_ratio) return int(scaled)
[docs] def reverse_scale_axis(self, val: float | int, axis: str = 'x') -> int: axis_ratio = float(self.ratios['xyz'.index(axis)]) scaled = round(val / axis_ratio) if axis_ratio else val return int(scaled)
@property def crop_x_min(self) -> int: return self.crop_x[0] @crop_x_min.setter def crop_x_min(self, value): self.crop_x = [value, self.crop_x_max] @property def crop_x_max(self) -> int: return self.crop_x[1] @crop_x_max.setter def crop_x_max(self, value): self.crop_x = [self.crop_x_min, value] @property def crop_y_min(self) -> int: return self.crop_y[0] @crop_y_min.setter def crop_y_min(self, value): self.crop_y = [value, self.crop_y_max] @property def crop_y_max(self) -> int: return self.crop_y[1] @crop_y_max.setter def crop_y_max(self, value): self.crop_y = [self.crop_y_min, value] @property def crop_z_min(self) -> int: return self.crop_z[0] @crop_z_min.setter def crop_z_min(self, value): self.crop_z = [value, self.crop_z_max] @property def crop_z_max(self) -> int: return self.crop_z[1] @crop_z_max.setter def crop_z_max(self, value): self.crop_z = [self.crop_z_min, value] @property def slice_tuples(self): if not isinstance(self, HasSlicingFields): raise AttributeError('slice_tuples requires crop_* fields.') return (tuple(ax) for ax in (self.crop_x, self.crop_y, self.crop_z)) @property def slicing(self): return tuple(slice(rng_min, rng_max) for rng_min, rng_max in self.slice_tuples)