Problem with dimension slider when adding array as new layer for OME-TIFF

Hi guys,

I tried to customize the Napari viewer a bit in order to read CZIs (inspired by the webinar). While this works fine for CZIs I stumbled into a strange issue when opening an OME-TIFF the same way.

As one can see the two channels are added as separate layers, where each channel T=10 and Z=15. But in the viewer the Z-Slider shows 0-13 (–> Z=14) while the T-Slider shows correctly 0-9 (–> T=10).

When opening the OME-TIFF in ZEN it correctly shows T=10 and Z=15 and when opening the original CZI in Napari it also shows the correct dimensions.

Has anyone an idea why my mistake could be? The code used to add the individual layers is attached.

Thx, sebi06

Opening the OME-TIFF

Opening the CZI (used to create the OME-TIFF)

Opening both in ZEN

def add_napari(array, metadata,
               blending='additive',
               gamma=0.75,
               verbose=True,
               use_pylibczi=True,
               rename_sliders=False):
    """Show the multidimensional array using the Napari viewer

    :param array: multidimensional NumPy.Array containing the pixeldata
    :type array: NumPy.Array
    :param metadata: dictionary with CZI or OME-TIFF metadata
    :type metadata: dict
    :param blending: NapariViewer option for blending, defaults to 'additive'
    :type blending: str, optional
    :param gamma: NapariViewer value for Gamma, defaults to 0.85
    :type gamma: float, optional
    :param verbose: show additional output, defaults to True
    :type verbose: bool, optional
    :param use_pylibczi: specify if pylibczi was used to read the CZI file, defaults to True
    :type use_pylibczi: bool, optional
    """

    # create scalefcator with all ones
    scalefactors = [1.0] * len(array.shape)

    if metadata['ImageType'] == 'ometiff':

        # find position of dimensions
        dimpos = get_dimpositions(metadata['Axes_aics'])

        # get the scalefactors from the metadata
        scalef = imf.get_scalefactor(metadata)

        # modify the tuple for the scales for napari
        scalefactors[dimpos['Z']] = scalef['zx']

        # remove C dimension from scalefactor
        scalefactors_ch = scalefactors.copy()
        del scalefactors_ch[dimpos['C']]

        # add all channels as layers
        for ch in range(metadata['SizeC']):

            try:
                # get the channel name
                chname = metadata['Channels'][ch]
            except:
                # or use CH1 etc. as string for the name
                chname = 'CH' + str(ch + 1)

            # cutout channel
            channel = array.take(ch, axis=dimpos['C'])
            print('Shape Channel : ', ch, channel.shape)
            new_dimstring = metadata['Axes_aics'].replace('C', '')

            # actually show the image array
            print('Adding Channel  : ', chname)
            print('Shape Channel   : ', ch, channel.shape)
            print('Scaling Factors : ', scalefactors_ch)

            # get min-max values for initial scaling
            clim = calc_scaling(channel,
                                corr_min=1.0,
                                offset_min=0,
                                corr_max=0.85,
                                offset_max=0)

            if verbose:
                print('Scaling: ', clim)

            # add channel to napari viewer
            viewer.add_image(channel,
                             name=chname,
                             scale=scalefactors_ch,
                             contrast_limits=clim,
                             blending=blending,
                             gamma=gamma)

    if metadata['ImageType'] == 'czi':

        # use find position of dimensions
        if not use_pylibczi:
            dimpos = get_dimpositions(metadata['Axes'])

        if use_pylibczi:
            dimpos = get_dimpositions(metadata['Axes_aics'])

        # get the scalefactors from the metadata
        scalef = imf.get_scalefactor(metadata)

        # modify the tuple for the scales for napari
        scalefactors[dimpos['Z']] = scalef['zx']

        # remove C dimension from scalefactor
        scalefactors_ch = scalefactors.copy()
        del scalefactors_ch[dimpos['C']]

        if metadata['SizeC'] > 1:
            # add all channels as layers
            for ch in range(metadata['SizeC']):

                try:
                    # get the channel name
                    chname = metadata['Channels'][ch]
                except:
                    # or use CH1 etc. as string for the name
                    chname = 'CH' + str(ch + 1)

                # cut out channel
                # use dask if array is a dask.array
                if isinstance(array, da.Array):
                    print('Extract Channel using Dask.Array')
                    channel = array.compute().take(ch, axis=dimpos['C'])
                    new_dimstring = metadata['Axes_aics'].replace('C', '')

                else:
                    # use normal numpy if not
                    print('Extract Channel NumPy.Array')
                    channel = array.take(ch, axis=dimpos['C'])
                    if not use_pylibczi:
                        new_dimstring = metadata['Axes'].replace('C', '')
                    if use_pylibczi:
                        new_dimstring = metadata['Axes_aics'].replace('C', '')

                # actually show the image array
                print('Adding Channel  : ', chname)
                print('Shape Channel   : ', ch, channel.shape)
                print('Scaling Factors : ', scalefactors_ch)

                # get min-max values for initial scaling
                clim = calc_scaling(channel,
                                    corr_min=1.0,
                                    offset_min=0,
                                    corr_max=0.85,
                                    offset_max=0)

                # add channel to napari viewer
                viewer.add_image(channel,
                                 name=chname,
                                 scale=scalefactors_ch,
                                 contrast_limits=clim,
                                 blending=blending,
                                 gamma=gamma)

    if rename_sliders:

        print('Renaming the Sliders based on the Dimension String ....')

        # get the position of dimension entries after removing C dimension
        dimpos_viewer = get_dimpositions(new_dimstring)

        # get the label of the sliders
        sliders = viewer.dims.axis_labels

        # update the labels with the correct dimension strings
        slidernames = ['B', 'S', 'T', 'Z']
        for s in slidernames:
            if dimpos_viewer[s] >= 0:
                sliders[dimpos_viewer[s]] = s
        # apply the new labels to the viewer
        viewer.dims.axis_labels = sliders

Hi @sebi06!

I don’t think you’ve made a mistake, but rather discovered a bug in our sliders when used with scales. Thanks for printing out the scales of your images, that was the key piece of information. I can reproduce the issue with just (in ipython --gui=qt):

import numpy as np
import napari

image = np.random.random((15, 256, 256))
# slider goes to /13
_ = napari.view_image(image, scale=(3.533, 1, 1))
# slider goes to /14
_ = napari.view_image(image, scale=(3.516, 1, 1))

In both cases, however, you are actually seeing the full dataset. You can see this with:

image2 = np.zeros((15, 256, 256))
image2[-1] = 1
_ = napari.view_image(image2, scale=(3.533, 1, 1))

and sliding all the way to the end — you’ll see an all white image. Also in displaying the 3D image you should be able to see the white plane.

The sliders issue is known. In this specific case I think it’s an issue with floating point arithmetic, but more generally the issue is that we don’t have a robust world coordinates model. See this project, which is at the top of our priority list — see this PR from @sofroniewn for the latest, though I think it’ll take at least one more PR, probably #1276, to fix this.

1 Like

Yeah, thanks for reporting this. I feel like we could add a test right now that we mark as a “skip” until those two other PRs are in at which point hopefully it will be fixed!!

Thx for the fast response. Any idea why it works when reading the data from a CZI but not working when reading from OME-TIFF?

This sounds like the issue I was having at Napari non-integer step size

Is it simply that with your CZI, you don’t have Z-scaling metadata that you do have with OME-TIFF?

Will

@sebi06 if you look at the z scale in the CZI it is slightly different from the OME-TIFF (3.516 vs 3.533). Only you can tell us why that would be the case… :blush:

@jni, @will-moore and @sofroniewn

As always the issue is the person infront of the system (aka me … :-). I found the answer to the question why it works for the CZI but not for the OME-TIFF.

  • the original image is an CZI (acquired by me) where the the scaling is XY=0.090577 (0.0905667) Z=0.320000
  • the OME-TIFF is just the exported CZI and also has XY=0.090577 (0.0905667) Z=0.320000

When I read the CZI in Python I did round the scaling values for convenience reasons to 3 digits, but I did not do the same when reading the scaling data from the exported OME-TIFF. So the reason is just the “rounding”. Obviously this is independent from the actual glitch in Napari and interestingly the more “precise” value from the OME-TIFF (no rounding of XYZScale) leads to the slider issue while when using rounding for the CZI it works. So maybe I was just lucky.

---------- CZI ----------
XScale 0.091
YScale 0.091
ZScale 0.32
SF CZI {'xy': 1.0, 'zx': 3.516}

---------- OME-TIFF ----------
XScale 0.09057667415221031
YScale 0.09057667415221031
ZScale 0.32
SF OME-TIFF {'xy': 1.0, 'zx': 3.533}
3 Likes