diff --git a/napari_czifile2/_reader.py b/napari_czifile2/_reader.py index 754dee1..fb33d62 100644 --- a/napari_czifile2/_reader.py +++ b/napari_czifile2/_reader.py @@ -24,7 +24,7 @@ def reader_function(paths): num_scenes = CZISceneFile.get_num_scenes(path) for scene_index in range(num_scenes): with CZISceneFile(path, scene_index) as f: - data = f.as_tzcyx0_array(max_workers=cpu_count()) + data = f.as_tzcyx0_array(maxworkers=cpu_count()) # https://github.com/BodenmillerGroup/napari-czifile2/issues/5 contrast_limits = None if data.dtype == np.uint16: diff --git a/napari_czifile2/io.py b/napari_czifile2/io.py index 5ef2345..18d3406 100644 --- a/napari_czifile2/io.py +++ b/napari_czifile2/io.py @@ -1,77 +1,74 @@ +from functools import cached_property from pathlib import Path -from typing import Iterable, List, Optional, Union +from typing import List, Optional, Union from xml.etree import ElementTree import numpy as np -from czifile import CziFile, DimensionEntryDV1, DirectoryEntryDV -from tifffile import lazyattr +from czifile import CziDirectoryEntryDV, CziFile class CZISceneFile(CziFile): @staticmethod def get_num_scenes(path: Union[str, Path], *args, **kwargs) -> int: with CziFile(path, *args, **kwargs) as czi_file: - if "S" in czi_file.axes: - return czi_file.shape[czi_file.axes.index("S")] - return 1 + return len(czi_file.scenes) def __init__(self, path: Union[str, Path], scene_index: int, *args, **kwargs): super(CZISceneFile, self).__init__(str(path), *args, **kwargs) self.scene_index = scene_index + self.axes = list(self.scenes[self.scene_index].sizes.keys()) - @lazyattr + def _get_start(self, dim: str) -> int: + try: + return self.scenes[self.scene_index].start[self.axes.index(dim)] + except ValueError: + return 0 + + @cached_property def pos_x_um(self) -> float: - return self.scale_x_um * min( - (dim_entry.start for dim_entry in self._iter_dim_entries("X")), default=0.0 - ) + return self.scale_x_um * self._get_start("X") - @lazyattr + @cached_property def pos_y_um(self) -> float: - return self.scale_y_um * min( - (dim_entry.start for dim_entry in self._iter_dim_entries("Y")), default=0.0 - ) + return self.scale_y_um * self._get_start("Y") - @lazyattr + @cached_property def pos_z_um(self) -> float: - return self.scale_z_um * min( - (dim_entry.start for dim_entry in self._iter_dim_entries("Z")), default=0.0 - ) + return self.scale_z_um * self._get_start("Z") - @lazyattr + @cached_property def pos_t_seconds(self) -> float: - return self.scale_t_seconds * min( - (dim_entry.start for dim_entry in self._iter_dim_entries("T")), default=0.0 - ) + return self.scale_t_seconds * self._get_start("T") - @lazyattr + @cached_property def scale_x_um(self) -> float: return self._get_scale("X", multiplier=10.0**6) - @lazyattr + @cached_property def scale_y_um(self) -> float: return self._get_scale("Y", multiplier=10.0**6) - @lazyattr + @cached_property def scale_z_um(self) -> float: return self._get_scale("Z", multiplier=10.0**6) - @lazyattr + @cached_property def scale_t_seconds(self) -> float: return self._get_scale("T") - @lazyattr + @cached_property def channel_names(self) -> Optional[List[str]]: if "C" in self.axes: channel_elements = self._metadata_xml.findall( ".//Metadata/Information/Image/Dimensions/Channels/Channel" ) - if len(channel_elements) == self.shape[self.axes.index("C")]: + if len(channel_elements) == self.scenes[self.scene_index].sizes["C"]: return [c.attrib.get("Name", c.attrib["Id"]) for c in channel_elements] return None - @lazyattr + @cached_property def is_rgb(self) -> bool: - return "0" in self.axes and self.shape[self.axes.index("0")] > 1 + return "0" in self.axes and self.scenes[self.scene_index].sizes["0"] > 1 def as_tzcyx0_array(self, *args, **kwargs) -> np.ndarray: data = self.asarray(*args, **kwargs) @@ -105,12 +102,6 @@ def as_tzcyx0_array(self, *args, **kwargs) -> np.ndarray: data.shape = data.shape[:6] return data - def _iter_dim_entries(self, dimension: str) -> Iterable[DimensionEntryDV1]: - for dir_entry in self.filtered_subblock_directory: - for dim_entry in dir_entry.dimension_entries: - if dim_entry.dimension == dimension: - yield dim_entry - def _get_scale(self, dimension: str, multiplier: float = 1.0): scale_element = self._metadata_xml.find( f'.//Metadata/Scaling/Items/Distance[@Id="{dimension}"]/Value' @@ -121,12 +112,12 @@ def _get_scale(self, dimension: str, multiplier: float = 1.0): return scale * multiplier return 1.0 - @lazyattr + @cached_property def _metadata_xml(self) -> ElementTree.Element: return ElementTree.fromstring(self.metadata()) - @lazyattr - def filtered_subblock_directory(self) -> List[DirectoryEntryDV]: + @cached_property + def filtered_subblock_directory(self) -> List[CziDirectoryEntryDV]: dir_entries = super(CZISceneFile, self).filtered_subblock_directory return list( filter( @@ -136,13 +127,6 @@ def filtered_subblock_directory(self) -> List[DirectoryEntryDV]: ) @staticmethod - def _get_scene_index(dir_entry: DirectoryEntryDV) -> int: - scene_indices = { - dim_entry.start - for dim_entry in dir_entry.dimension_entries - if dim_entry.dimension == "S" - } - if len(scene_indices) == 0: - return 0 - assert len(scene_indices) == 1 - return scene_indices.pop() + def _get_scene_index(dir_entry: CziDirectoryEntryDV) -> int: + scene_index = dir_entry.scene_index + return 0 if scene_index == -1 else scene_index diff --git a/setup.cfg b/setup.cfg index db4871f..4181ea0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ project_urls = [options] include_package_data = True install_requires = - czifile + czifile >= 2026 imagecodecs numpy tifffile