Source code for neuralib.scan.czi

from __future__ import annotations

from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Literal, final
from xml.etree.ElementTree import tostring

import aicspylibczi
import numpy as np
import xmltodict
from neuralib.typing import PathLike
from neuralib.util.unstable import unstable

from .core import AbstractScanner, DimCode, SceneIdx

__all__ = ['czi_file', 'CziScanner']


[docs] @contextmanager def czi_file(filepath: PathLike) -> Generator[CziScanner, None, None]: """ context manager for load a czi file :param filepath: czi file path :return: :class:`~CziScanner` """ if Path(filepath).suffix != '.czi': raise ValueError('czi file only') scanner = CziScanner(filepath) try: yield scanner finally: scanner.close()
[docs] @final class CziScanner(AbstractScanner): """CZI confocal image data""" _czi_file: aicspylibczi.CziFile | None _consistent_scan_configs: bool
[docs] def __init__(self, filepath: PathLike): filepath = Path(filepath) self._czi_file = aicspylibczi.CziFile(filepath) super().__init__(filepath)
def _load_metadata(self) -> dict[str, Any]: czi_file = self.czi_file dim = czi_file.get_dims_shape() first_scene_dims = dim[0] if 'S' in first_scene_dims and len(dim) == 1: self._n_scenes = first_scene_dims['S'][1] self._consistent_scan_configs = True elif len(dim) > 1: self._n_scenes = len(dim) self._consistent_scan_configs = len(set(tuple(sorted(d.items())) for d in dim)) == 1 else: self._n_scenes = 1 self._consistent_scan_configs = True xml_string = tostring(czi_file.meta, encoding='utf-8').decode('utf-8') return xmltodict.parse(xml_string)
[docs] def close(self): if self._czi_file is not None: self._czi_file = None
@property def czi_file(self) -> aicspylibczi.CziFile: """get ``aicspylibczi.CziFile`` object""" if self._czi_file is None: raise RuntimeError('CZI file is closed') return self._czi_file @property def consistent_config(self) -> bool: """Checks if the CZI file have consistent scanning configs across scenes""" return self._consistent_scan_configs @property def is_mosaic(self) -> bool: """Checks if the CZI file is marked as mosaic by the reader.""" return self.czi_file.is_mosaic() @property def n_scenes(self) -> int: """Total number of scenes (positions/series) in the file.""" return self._n_scenes @property def dimcode(self) -> DimCode: return self.czi_file.dims
[docs] def get_code(self, scene_idx: SceneIdx, code: Literal['S', 'C', 'Z', 'X', 'Y', 'M']) -> int: """ Retrieves a specific shape value associated with a given scene index and access code from the internal czi_file data structure. :param scene_idx: The index representing a specific scene in the czi_file's dimensions. :param code: A string key for accessing a particular value related to the given scene index. :return: Returns the shape value corresponding to the scene index and ``code`` """ if scene_idx < 0 or scene_idx >= self.n_scenes: raise ValueError(f'invalid scene index:{scene_idx}') dim_shape = self.czi_file.get_dims_shape() if self.consistent_config: return dim_shape[0][code][1] else: return dim_shape[scene_idx][code][1]
[docs] def get_tile_info(self, scene_idx: SceneIdx = 0) -> dict[str, Any] | None: """Extracts tile region information from metadata, if available. Returns None if not a tiled acquisition or info is missing""" tile_region_info = self.metadata \ .get('ImageDocument', {}) \ .get('Metadata', {}) \ .get('Experiment', {}) \ .get('ExperimentBlocks', {}) \ .get('AcquisitionBlock', {}) \ .get('SubDimensionSetups', {}) \ .get('RegionsSetup', {}) \ .get('SampleHolder', {}) \ .get('TileRegions', {}) \ .get('TileRegion') if tile_region_info is None: return None if isinstance(tile_region_info, list): if 0 <= scene_idx < len(tile_region_info): return tile_region_info[scene_idx] # Return specific region dict else: return None elif isinstance(tile_region_info, dict): if scene_idx == 0: return tile_region_info else: return tile_region_info else: return None
[docs] def get_channel_names(self, scene_idx: SceneIdx) -> list[str]: """Get the names of the fluorescence channels for a specific scene. (Implementation copied from czi_scanner_impl_v1 for completeness) """ if not 0 <= scene_idx < self.n_scenes: raise IndexError(f"scene_idx {scene_idx} out of bounds for {self.n_scenes} scenes.") info = self.metadata.get('ImageDocument', {}).get('Metadata', {}) channels_info = info.get('Information', {}).get('Image', {}).get('Dimensions', {}).get('Channels', {}).get('Channel') if isinstance(channels_info, list): names = [ch.get('@Name', f"Channel {i}") for i, ch in enumerate(channels_info)] elif isinstance(channels_info, dict): names = [channels_info.get('@Name', "Channel 0")] else: n_channels = self.get_code(scene_idx, 'C') names = [f"Channel {i}" for i in range(n_channels)] return names
[docs] @unstable(doc=False) def view(self, scene: SceneIdx | None = None, channel: int = 0, depth: int | slice | np.ndarray | None = None, project_type: Literal['avg', 'max', 'min', 'std', 'median'] = 'max', norm: bool = True, **kwargs: Any) -> np.ndarray: """ Generates a view of the image data based on the provided parameters such as scene, channel, depth, projection type, and normalization. The function retrieves image data from either a mosaic file or a standard image file, processes it according to the specified depth, performs projection if required, and applies normalization if requested. :param scene: Scene index to be used for image loading. If None and the file is non-mosaic, defaults to 0. Used only for non-mosaic CZI files. :param channel: Index of the channel to be visualized. Defaults to 0. :param depth: Z-plane depth index, slice, or array. If None, all Z-slices are loaded. Can also be a specific integer index or an array of indices/slice. :param project_type: Type of Z-projection to apply if multiple Z-slices are selected. Options include 'avg', 'max', 'min', 'std', 'median'. Defaults to 'max'. :param norm: Whether to normalize the image intensity values. If True, divides the data by its maximum intensity value. Defaults to True. :return: A NumPy array containing the processed image data, which may be normalized and/or projected based on the input parameters. """ read_kwargs = {} czi_file = self.czi_file if self.is_mosaic: fn = czi_file.read_mosaic nz = np.max([self.get_code(i, 'Z') for i in range(self.n_scenes)]) else: fn = czi_file.read_image scene = 0 if scene is None else scene nz = self.get_code(scene, 'Z') read_kwargs.update({'S': scene}) # if depth is None: depth = np.arange(nz) if isinstance(depth, int): img = fn(C=channel, Z=depth, **read_kwargs).squeeze() elif isinstance(depth, np.ndarray): stacks = np.array([fn(C=channel, Z=z, **read_kwargs)[0].squeeze() for z in depth]) # (C, ) img = self.z_projection(stacks, project_type) elif isinstance(depth, slice): stacks = np.array([fn(C=channel, Z=z, **read_kwargs)[0].squeeze() for z in range(nz)[depth]]) # (C, ) img = self.z_projection(stacks, project_type) else: raise TypeError(f'unknown type {type(depth)}') if norm: img = img / np.max(img) return img