Skip to content

czitools.export_tools.display #

Image display helpers for the OME-Zarr export tools.

Vendored (with light edits) from czi_omezarr_utils.display in the omezarr_playground repository as part of czitools Stage 5.

Contents
  • compute_pyramid_scale_factors — size-aware multiscale scale factors (Y/X only)
  • get_fieldimage — extract a scene from a 6D array as a multi-scale NgffImage
  • get_display — per-channel display-range settings from CZI metadata
  • create_channel_list — OMERO channel list used by both write backends

Functions:

compute_pyramid_levels #

compute_pyramid_levels(
    size_y: int,
    size_x: int,
    min_size: int = 512,
    max_levels: int = 6,
) -> int

Compute the number of resolution levels based on the 2D plane size.

The pyramid keeps halving the XY plane until the largest XY dimension of the coarsest level is roughly <= min_size pixels (i.e. it fits in about one chunk/tile). Small planes therefore get few (or no) extra levels, avoiding wasted tiny levels, while large planes get enough levels for smooth zoomed-out viewing.

Parameters:

  • size_y #

    (int) –

    Height of the base-resolution plane in pixels.

  • size_x #

    (int) –

    Width of the base-resolution plane in pixels.

  • min_size #

    (int, default: 512 ) –

    Target maximum XY size of the coarsest level. Defaults to 512.

  • max_levels #

    (int, default: 6 ) –

    Hard cap on the number of levels. Defaults to 6.

Returns:

  • int ( int ) –

    Number of resolution levels (>= 1, where 1 means base only, no pyramid).

Source code in czitools/export_tools/display.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def compute_pyramid_levels(size_y: int, size_x: int, min_size: int = 512, max_levels: int = 6) -> int:
    """Compute the number of resolution levels based on the 2D plane size.

    The pyramid keeps halving the XY plane until the largest XY dimension of the
    coarsest level is roughly ``<= min_size`` pixels (i.e. it fits in about one
    chunk/tile). Small planes therefore get few (or no) extra levels, avoiding
    wasted tiny levels, while large planes get enough levels for smooth zoomed-out
    viewing.

    Args:
        size_y (int): Height of the base-resolution plane in pixels.
        size_x (int): Width of the base-resolution plane in pixels.
        min_size (int): Target maximum XY size of the coarsest level. Defaults to 512.
        max_levels (int): Hard cap on the number of levels. Defaults to 6.

    Returns:
        int: Number of resolution levels (>= 1, where 1 means base only, no pyramid).
    """
    largest = int(max(size_y, size_x))
    if largest <= min_size or min_size <= 0:
        return 1
    # Number of halvings needed for the largest XY dim to reach <= min_size,
    # plus the base level. ceil() picks the first level that fits within min_size.
    n_downsamples = math.ceil(math.log2(largest / min_size))
    n_levels = n_downsamples + 1
    return max(1, min(int(n_levels), int(max_levels)))

compute_pyramid_scale_factors #

compute_pyramid_scale_factors(
    size_y: int,
    size_x: int,
    min_size: int = 512,
    max_levels: int = 6,
) -> list

Build size-aware, Y/X-only downscale factors for ngff_zarr.to_multiscales.

Returns cumulative factors [2, 4, 8, ...] applied to the Y and X axes only (Z is not downsampled). The number of factors is levels - 1 where levels comes from :func:compute_pyramid_levels. An empty list means "base only".

Parameters:

  • size_y #

    (int) –

    Height of the base-resolution plane in pixels.

  • size_x #

    (int) –

    Width of the base-resolution plane in pixels.

  • min_size #

    (int, default: 512 ) –

    Target maximum XY size of the coarsest level. Defaults to 512.

  • max_levels #

    (int, default: 6 ) –

    Hard cap on the number of levels. Defaults to 6.

Returns:

  • list ( list ) –

    A list of per-level dicts {"z": 1, "y": 2**i, "x": 2**i} (Y/X only, Z factor fixed at 1 = no Z downsampling), or an empty list when no pyramid is warranted.

Source code in czitools/export_tools/display.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def compute_pyramid_scale_factors(size_y: int, size_x: int, min_size: int = 512, max_levels: int = 6) -> list:
    """Build size-aware, Y/X-only downscale factors for ``ngff_zarr.to_multiscales``.

    Returns cumulative factors ``[2, 4, 8, ...]`` applied to the Y and X axes only
    (Z is not downsampled). The number of factors is ``levels - 1`` where ``levels``
    comes from :func:`compute_pyramid_levels`. An empty list means "base only".

    Args:
        size_y (int): Height of the base-resolution plane in pixels.
        size_x (int): Width of the base-resolution plane in pixels.
        min_size (int): Target maximum XY size of the coarsest level. Defaults to 512.
        max_levels (int): Hard cap on the number of levels. Defaults to 6.

    Returns:
        list: A list of per-level dicts ``{"z": 1, "y": 2**i, "x": 2**i}`` (Y/X only,
            Z factor fixed at 1 = no Z downsampling), or an empty list when no pyramid
            is warranted.
    """
    n_levels = compute_pyramid_levels(size_y, size_x, min_size=min_size, max_levels=max_levels)
    # z factor is fixed at 1 (no Z downsampling). ngff-zarr requires every spatial
    # dim (z, y, x) to be present in each factor dict, so z=1 must be included.
    return [{"z": 1, "y": 2**i, "x": 2**i} for i in range(1, n_levels)]

create_channel_list #

create_channel_list(metadata: CziMetadata) -> list

Build the OMERO channel list used by both write backends.

Parameters:

  • metadata #

    (CziMetadata) –

    Metadata with channelinfo and maxvalue_list.

Returns:

  • list ( list ) –

    Channel dicts with keys color, label, active, window.

Source code in czitools/export_tools/display.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def create_channel_list(metadata: CziMetadata) -> list:
    """Build the OMERO channel list used by both write backends.

    Args:
        metadata (CziMetadata): Metadata with ``channelinfo`` and ``maxvalue_list``.

    Returns:
        list: Channel dicts with keys ``color``, ``label``, ``active``, ``window``.
    """
    channels_list: list = []

    image = metadata.image
    if image is None:
        return channels_list

    channelinfo = metadata.channelinfo
    if channelinfo is None:
        return channels_list

    for ch_index in range(image.SizeC or 0):
        rgb = channelinfo.colors[ch_index][3:]
        chname = channelinfo.names[ch_index]
        lower, higher, maxvalue = get_display(metadata, ch_index)
        channels_list.append(
            {
                "color": rgb,
                "label": chname,
                "active": True,
                "window": {
                    "min": lower,
                    "start": lower,
                    "end": higher,
                    "max": maxvalue,
                },
            }
        )

    return channels_list

create_ngff_omero_channels #

create_ngff_omero_channels(metadata: CziMetadata) -> list

Build ngff-zarr OmeroChannel objects from CZI metadata.

These are attached to a multiscale image so that OME-NGFF readers (e.g. ngio / napari-ome-zarr-navigator) can resolve per-channel display settings. Without OMERO channel metadata, ngio's channels_meta is None and the navigator ROI loader fails with 'NoneType' object has no attribute 'channels'.

Parameters:

  • metadata #

    (CziMetadata) –

    Metadata with channelinfo and maxvalue_list.

Returns:

  • list ( list ) –

    A list of :class:ngff_zarr.OmeroChannel objects (empty if no channel metadata is available).

Source code in czitools/export_tools/display.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def create_ngff_omero_channels(metadata: CziMetadata) -> list:
    """Build ngff-zarr ``OmeroChannel`` objects from CZI metadata.

    These are attached to a multiscale image so that OME-NGFF readers (e.g. ngio /
    napari-ome-zarr-navigator) can resolve per-channel display settings. Without
    OMERO channel metadata, ngio's ``channels_meta`` is ``None`` and the navigator
    ROI loader fails with ``'NoneType' object has no attribute 'channels'``.

    Args:
        metadata (CziMetadata): Metadata with ``channelinfo`` and ``maxvalue_list``.

    Returns:
        list: A list of :class:`ngff_zarr.OmeroChannel` objects (empty if no
            channel metadata is available).
    """
    channels = []
    for ch in create_channel_list(metadata):
        channels.append(
            nz.OmeroChannel(
                color=ch["color"],
                window=nz.OmeroWindow(
                    min=ch["window"]["min"],
                    max=ch["window"]["max"],
                    start=ch["window"]["start"],
                    end=ch["window"]["end"],
                ),
                label=ch["label"],
            )
        )
    return channels

get_display #

Extract display-range settings for a channel from CZI metadata.

Parameters:

  • metadata #

    (CziMetadata) –

    Metadata with channel display settings.

  • channel_index #

    (int) –

    Zero-based channel index.

Returns:

  • tuple[float, float, float]

    tuple[float, float, float]: (lower, higher, maxvalue). Falls back to (0, maxvalue, maxvalue) when metadata is missing or corrupted.

Source code in czitools/export_tools/display.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def get_display(metadata: CziMetadata, channel_index: int) -> tuple[float, float, float]:
    """Extract display-range settings for a channel from CZI metadata.

    Args:
        metadata (CziMetadata): Metadata with channel display settings.
        channel_index (int): Zero-based channel index.

    Returns:
        tuple[float, float, float]: ``(lower, higher, maxvalue)``. Falls back to
            ``(0, maxvalue, maxvalue)`` when metadata is missing or corrupted.
    """
    channelinfo = metadata.channelinfo
    maxvalue_list = metadata.maxvalue_list
    try:
        assert channelinfo is not None, "channelinfo is None"
        assert maxvalue_list is not None, "maxvalue_list is None"
        lower = np.round(channelinfo.clims[channel_index][0] * maxvalue_list[channel_index], 0)
        higher = np.round(channelinfo.clims[channel_index][1] * maxvalue_list[channel_index], 0)
        maxvalue = maxvalue_list[channel_index]
    except (IndexError, AssertionError):
        logger.warning("Display setting from CZI unavailable. Using 0-Max instead.")
        lower = 0.0
        higher = float(maxvalue_list[channel_index]) if maxvalue_list is not None else 0.0
        maxvalue = higher

    return lower, higher, maxvalue

get_fieldimage #

get_fieldimage(
    array6d: DataArray | ndarray | Array,
    scene_index: int,
    metadata: CziMetadata,
    min_size: int = 512,
    max_levels: int = 6,
) -> Multiscales

Extract a field image from a 6D array as a multi-scale representation.

The number of resolution levels is derived from the 2D (Y, X) plane size via :func:compute_pyramid_scale_factors (downsampling Y/X only, never Z), so small fields get few/no extra levels and large fields get enough for smooth zoomed-out viewing.

Parameters:

  • array6d #

    (Union[DataArray, ndarray, Array]) –

    6D array with dimensions [scene, t, c, z, y, x].

  • scene_index #

    (int) –

    Index of the scene to extract.

  • metadata #

    (CziMetadata) –

    Metadata with scale information and filename.

  • min_size #

    (int, default: 512 ) –

    Target maximum XY size of the coarsest level. Defaults to 512.

  • max_levels #

    (int, default: 6 ) –

    Hard cap on the number of levels. Defaults to 6.

Returns:

  • Multiscales

    nz.Multiscales: Multi-scale representation using Gaussian downsampling with size-aware, Y/X-only scale factors.

Source code in czitools/export_tools/display.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def get_fieldimage(
    array6d: xr.DataArray | np.ndarray | da.Array,
    scene_index: int,
    metadata: CziMetadata,
    min_size: int = 512,
    max_levels: int = 6,
) -> "nz.Multiscales":
    """Extract a field image from a 6D array as a multi-scale representation.

    The number of resolution levels is derived from the 2D (Y, X) plane size via
    :func:`compute_pyramid_scale_factors` (downsampling Y/X only, never Z), so
    small fields get few/no extra levels and large fields get enough for smooth
    zoomed-out viewing.

    Args:
        array6d (Union[xr.DataArray, np.ndarray, da.Array]): 6D array with dimensions
            ``[scene, t, c, z, y, x]``.
        scene_index (int): Index of the scene to extract.
        metadata (CziMetadata): Metadata with scale information and filename.
        min_size (int): Target maximum XY size of the coarsest level. Defaults to 512.
        max_levels (int): Hard cap on the number of levels. Defaults to 6.

    Returns:
        nz.Multiscales: Multi-scale representation using Gaussian downsampling with
            size-aware, Y/X-only scale factors.
    """
    if isinstance(array6d, xr.DataArray):
        data = array6d[scene_index, ...].data
    else:
        data = array6d[scene_index, ...]

    size_y, size_x = int(data.shape[-2]), int(data.shape[-1])
    scale_factors = compute_pyramid_scale_factors(size_y, size_x, min_size=min_size, max_levels=max_levels)

    _scale = metadata.scale
    current_field_image = nz.NgffImage(
        data=data,  # type: ignore[arg-type]
        dims=["t", "c", "z", "y", "x"],
        scale={
            "t": 1.0,
            "c": 1.0,
            "z": float(_scale.Z) if (_scale is not None and _scale.Z is not None) else 1.0,
            "y": float(_scale.Y) if (_scale is not None and _scale.Y is not None) else 1.0,
            "x": float(_scale.X) if (_scale is not None and _scale.X is not None) else 1.0,
        },
        axes_units={
            "t": "second",
            "z": "micrometer",
            "y": "micrometer",
            "x": "micrometer",
        },
        translation={"t": 0.0, "c": 0.0, "z": 0.0, "y": 0.0, "x": 0.0},
        name=metadata.filename if metadata.filename is not None else "image.czi",
    )

    return nz.to_multiscales(  # type: ignore[attr-defined]
        current_field_image,
        scale_factors=scale_factors,
        method=nz.Methods.DASK_IMAGE_GAUSSIAN,  # type: ignore[attr-defined]
    )