From b821ca32a2f1f574f75123df6bd4416e518c6680 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 23 Sep 2024 00:21:27 +0200 Subject: [PATCH 1/4] read channel colors from czi metadata --- package/PartSegImage/image.py | 4 ++-- package/PartSegImage/image_reader.py | 23 +++++++++++-------- .../test_PartSegImage/test_image_reader.py | 1 + 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/package/PartSegImage/image.py b/package/PartSegImage/image.py index 5bbd94926..e16a49d7e 100644 --- a/package/PartSegImage/image.py +++ b/package/PartSegImage/image.py @@ -916,8 +916,8 @@ def get_imagej_colors(self): res.append(color) return res - def get_colors(self): - res = [] + def get_colors(self) -> list[str | list[int]]: + res: list[str | list[int]] = [] for color in self.default_coloring: if isinstance(color, str): res.append(color) diff --git a/package/PartSegImage/image_reader.py b/package/PartSegImage/image_reader.py index 08cde4cce..9be19cf71 100644 --- a/package/PartSegImage/image_reader.py +++ b/package/PartSegImage/image_reader.py @@ -133,11 +133,21 @@ def __init__(self, callback_function=None): self.default_spacing = 10**-6, 10**-6, 10**-6 self.spacing = self.default_spacing self.channel_names = None + self.colors = None + self.ranges = None if callback_function is None: self.callback_function = _empty else: self.callback_function = callback_function + def _get_channel_info(self): + return [ + ChannelInfo(name=name, color_map=color, contrast_limits=contrast_limits) + for name, color, contrast_limits in zip_longest( + li_if_no(self.channel_names), li_if_no(self.colors), li_if_no(self.ranges) + ) + ] + def set_default_spacing(self, spacing): spacing = tuple(spacing) if len(spacing) == 2: @@ -368,11 +378,13 @@ def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext scale_info.get("Y", self.default_spacing[1]), scale_info.get("X", self.default_spacing[2]), ) + with suppress(KeyError): channel_meta = metadata["ImageDocument"]["Metadata"]["DisplaySetting"]["Channels"]["Channel"] if isinstance(channel_meta, dict): # single channel saved in czifile channel_meta = [channel_meta] self.channel_names = [x["Name"] for x in channel_meta] + self.colors = [x["Color"] for x in channel_meta] # TODO add mask reading if isinstance(image_path, BytesIO): image_path = "" @@ -383,7 +395,7 @@ def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext file_path=image_path, axes_order=self.return_order(), metadata_dict=metadata, - channel_info=[ChannelInfo(name=name) for name in self.channel_names or []], + channel_info=self._get_channel_info(), ) @classmethod @@ -462,7 +474,6 @@ class TiffImageReader(BaseImageReaderBuffer): def __init__(self, callback_function=None): super().__init__(callback_function) - self.colors = None self.ranges = None self.shift = (0, 0, 0) self.name = "" @@ -522,17 +533,11 @@ def report_func(): if not isinstance(image_path, (str, Path)): image_path = "" - channel_info = [ - ChannelInfo(name=name, color_map=color, contrast_limits=contrast_limits) - for name, color, contrast_limits in zip_longest( - li_if_no(self.channel_names), li_if_no(self.colors), li_if_no(self.ranges) - ) - ] return self.image_class( image_data, spacing=self.spacing, mask=mask_data, - channel_info=channel_info, + channel_info=self._get_channel_info(), file_path=os.path.abspath(image_path), axes_order=self.return_order(), shift=self.shift, diff --git a/package/tests/test_PartSegImage/test_image_reader.py b/package/tests/test_PartSegImage/test_image_reader.py index aa3808da1..0ca6ab980 100644 --- a/package/tests/test_PartSegImage/test_image_reader.py +++ b/package/tests/test_PartSegImage/test_image_reader.py @@ -39,6 +39,7 @@ def test_czi_file_read(self, data_test_dir): assert np.count_nonzero(image.get_channel(0)) assert image.channels == 4 assert image.layers == 1 + assert image.get_colors() == ["#FFFFFF", "#FF0000", "#00FF00", "#0000FF"] assert image.file_path == os.path.join(data_test_dir, "test_czi.czi") From 51ef1da9fe8078cb36a28b8c57a85db978be81d9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 23 Sep 2024 10:28:01 +0200 Subject: [PATCH 2/4] apply suggestions from PR --- .github/project_dict.pws | 1 + package/PartSegImage/image_reader.py | 18 +++++++++--------- .../test_PartSegImage/test_image_reader.py | 1 + 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/project_dict.pws b/.github/project_dict.pws index d6179e52b..ef5c223d5 100644 --- a/.github/project_dict.pws +++ b/.github/project_dict.pws @@ -11,3 +11,4 @@ numpy pre bool changelog +czi diff --git a/package/PartSegImage/image_reader.py b/package/PartSegImage/image_reader.py index 9be19cf71..88b1bf205 100644 --- a/package/PartSegImage/image_reader.py +++ b/package/PartSegImage/image_reader.py @@ -29,7 +29,7 @@ CZI_MAX_WORKERS = None -def li_if_no(value): +def empty_list_if_none(value: typing.Optional[typing.Any]) -> typing.List[typing.Any]: if value is None: return [] return value @@ -129,22 +129,22 @@ def return_order(cls) -> str: """ return cls.image_class.axis_order - def __init__(self, callback_function=None): + def __init__(self, callback_function: typing.Optional[typing.Callable[[str, int], typing.Any]] = None) -> None: self.default_spacing = 10**-6, 10**-6, 10**-6 self.spacing = self.default_spacing - self.channel_names = None - self.colors = None - self.ranges = None + self.channel_names: typing.Optional[typing.List[str]] = None + self.colors: typing.Optional[typing.List[typing.Optional[typing.Any]]] = None + self.ranges: typing.Optional[typing.List[typing.Tuple[float, float]]] = None if callback_function is None: self.callback_function = _empty else: self.callback_function = callback_function - def _get_channel_info(self): + def _get_channel_info(self) -> typing.List[ChannelInfo]: return [ ChannelInfo(name=name, color_map=color, contrast_limits=contrast_limits) for name, color, contrast_limits in zip_longest( - li_if_no(self.channel_names), li_if_no(self.colors), li_if_no(self.ranges) + empty_list_if_none(self.channel_names), empty_list_if_none(self.colors), empty_list_if_none(self.ranges) ) ] @@ -383,8 +383,8 @@ def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext if isinstance(channel_meta, dict): # single channel saved in czifile channel_meta = [channel_meta] - self.channel_names = [x["Name"] for x in channel_meta] - self.colors = [x["Color"] for x in channel_meta] + self.channel_names = [x.get("Name", f"Channel_{i}") for i, x in enumerate(channel_meta, start=1)] + self.colors = [x.get("Color") for x in channel_meta] # TODO add mask reading if isinstance(image_path, BytesIO): image_path = "" diff --git a/package/tests/test_PartSegImage/test_image_reader.py b/package/tests/test_PartSegImage/test_image_reader.py index 0ca6ab980..ceb3636d0 100644 --- a/package/tests/test_PartSegImage/test_image_reader.py +++ b/package/tests/test_PartSegImage/test_image_reader.py @@ -35,6 +35,7 @@ def test_tiff_image_read_buffer(self): assert np.all(np.isclose(image.spacing, (7.752248561753867e-08,) * 2)) def test_czi_file_read(self, data_test_dir): + """Check if czi file is read correctly.""" image = CziImageReader.read_image(os.path.join(data_test_dir, "test_czi.czi")) assert np.count_nonzero(image.get_channel(0)) assert image.channels == 4 From fafd7a9ce901085c3e054978d558fb0027f29ccf Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 23 Sep 2024 11:14:14 +0200 Subject: [PATCH 3/4] improve code based on automated PR --- package/PartSegCore/napari_plugins/loader.py | 1 + package/PartSegImage/image_reader.py | 23 ++++++-------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/package/PartSegCore/napari_plugins/loader.py b/package/PartSegCore/napari_plugins/loader.py index 05ef6c225..22285df2b 100644 --- a/package/PartSegCore/napari_plugins/loader.py +++ b/package/PartSegCore/napari_plugins/loader.py @@ -27,6 +27,7 @@ def _image_to_layers(project_info, scale, translate): "blending": "additive", "translate": translate, "metadata": project_info.image.metadata, + "colormap": project_info.image.get_colors()[i], }, "image", ) diff --git a/package/PartSegImage/image_reader.py b/package/PartSegImage/image_reader.py index 88b1bf205..6681202fd 100644 --- a/package/PartSegImage/image_reader.py +++ b/package/PartSegImage/image_reader.py @@ -29,12 +29,6 @@ CZI_MAX_WORKERS = None -def empty_list_if_none(value: typing.Optional[typing.Any]) -> typing.List[typing.Any]: - if value is None: - return [] - return value - - class ZSTD1Header(typing.NamedTuple): """ ZSTD1 header structure @@ -132,9 +126,9 @@ def return_order(cls) -> str: def __init__(self, callback_function: typing.Optional[typing.Callable[[str, int], typing.Any]] = None) -> None: self.default_spacing = 10**-6, 10**-6, 10**-6 self.spacing = self.default_spacing - self.channel_names: typing.Optional[typing.List[str]] = None - self.colors: typing.Optional[typing.List[typing.Optional[typing.Any]]] = None - self.ranges: typing.Optional[typing.List[typing.Tuple[float, float]]] = None + self.channel_names: typing.List[str] = [] + self.colors: typing.List[typing.Optional[typing.Any]] = [] + self.ranges: typing.List[typing.Tuple[float, float]] = [] if callback_function is None: self.callback_function = _empty else: @@ -143,9 +137,7 @@ def __init__(self, callback_function: typing.Optional[typing.Callable[[str, int] def _get_channel_info(self) -> typing.List[ChannelInfo]: return [ ChannelInfo(name=name, color_map=color, contrast_limits=contrast_limits) - for name, color, contrast_limits in zip_longest( - empty_list_if_none(self.channel_names), empty_list_if_none(self.colors), empty_list_if_none(self.ranges) - ) + for name, color, contrast_limits in zip_longest(self.channel_names, self.colors, self.ranges) ] def set_default_spacing(self, spacing): @@ -474,7 +466,6 @@ class TiffImageReader(BaseImageReaderBuffer): def __init__(self, callback_function=None): super().__init__(callback_function) - self.ranges = None self.shift = (0, 0, 0) self.name = "" self.metadata = {} @@ -483,7 +474,7 @@ def read(self, image_path: typing.Union[str, BytesIO, Path], mask_path=None, ext """ Read tiff image from tiff_file """ - self.spacing, self.colors, self.channel_names, self.ranges = self.default_spacing, None, None, None + self.spacing, self.colors, self.channel_names, self.ranges = self.default_spacing, [], [], [] with tifffile.TiffFile(image_path) as image_file: total_pages_num = len(image_file.series[0]) @@ -604,8 +595,8 @@ def read_imagej_metadata(self, image_file): z_spacing = self.default_spacing[0] x_spacing, y_spacing = self.read_resolution_from_tags(image_file) self.spacing = z_spacing, y_spacing, x_spacing - self.colors = image_file.imagej_metadata.get("LUTs") - self.channel_names = image_file.imagej_metadata.get("Labels") + self.colors = image_file.imagej_metadata.get("LUTs", []) + self.channel_names = image_file.imagej_metadata.get("Labels", []) if "Ranges" in image_file.imagej_metadata: ranges = image_file.imagej_metadata["Ranges"] self.ranges = list(zip(ranges[::2], ranges[1::2])) From cf5f666cd70541f2c670a0c05fbd9ce19fa495d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Mon, 23 Sep 2024 11:15:10 +0200 Subject: [PATCH 4/4] remove obsolete change --- package/PartSegCore/napari_plugins/loader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/package/PartSegCore/napari_plugins/loader.py b/package/PartSegCore/napari_plugins/loader.py index 22285df2b..05ef6c225 100644 --- a/package/PartSegCore/napari_plugins/loader.py +++ b/package/PartSegCore/napari_plugins/loader.py @@ -27,7 +27,6 @@ def _image_to_layers(project_info, scale, translate): "blending": "additive", "translate": translate, "metadata": project_info.image.metadata, - "colormap": project_info.image.get_colors()[i], }, "image", )