Strange behavior of MultiImage loading Fiji-generated RGB Stacks

I am working on large RGB stack .tiff files generated by Fiji : 1000 frames of 32bit RGB 960*730pix. The files are too large to load in the memory. I only want to access them one slice at a time, so I tried using MultiImage :

image_stack=skimage.io.MultiImage(path_of_tiff_file)

However, instead of having a image_stack of the length of my movie, image_stack is of length 1. image_stack[0] contains the whole stack, and is unloadable because it takes to much memory.

I tried doing the same using imread() instead, and it behaves exactly the same.

Does any of you know why it behaves this way and how I could solve the problem ?

I’m not sure what you exactly mean by series of RGB images. Tiff files are composed of series of 2D planes. Those planes can be channels, time points, planes of a z-stack etc. and how the different planes are interpreted by the software depends on if and how the meta-data are read. In any case, if standard solutions don’t work, it means meta-data are not read properly. But you can always read your data as a “raw” Tiff stacks and see if you can recover what you need form there:

from skimage.external.tifffile import TiffFile

data = TiffFile(filepath)
# print some metadata infos
data.info()
# get the plane number 6 from your stack as numpy array
data.pages[6].asarray()

Hi @lorraine_Montel, any chance you can provide us with a short test video to help with debugging the problem?

Hi, thanks for taking time to help me. I tried loading the raw data as you suggested, here is the medatada I got from data.info() :

“TIFF file: Zone7.tiff, 2005 MiB, big endian, imagej\n\nSeries 0: 1000x730x960x3, uint8, ZYXS, 1 pages, memmap-offset=90015\n\nPage 0: 1000x730x960x3, uint8, 8 bit, rgb, raw, imagej|contiguous\n* 254 new_subfile_type (1I) 0\n* 256 image_width (1I) 960\n* 257 image_length (1I) 730\n* 258 bits_per_sample (3H) (8, 8, 8)\n* 262 photometric (1H) 2\n* 270 image_description (49s) b’ImageJ=1.52p\nimages=1000\nslices=1000\nloop=fa\n* 273 strip_offsets (1I) (90015,)\n* 277 samples_per_pixel (1H) 3\n* 278 rows_per_strip (1H) 730\n* 279 strip_byte_counts (1I) (2102400000,)\n* 50838 imagej_byte_counts (1001I) (12, 82, 82, 82, 82, 82, 82, 82, 82, 82, 84,\n* 50839 imagej_metadata (85798B) b’IJIJlabl\x00\x00\x03\xe8\x00T\x00h\x00i\x00n\n\nIMAGEJ_TAGS\n* ImageJ: 1.52p\n* images: 1000\n* labels: [‘Thin4_15mbar10_t1_Simple Segmentation.tif’, 'Thin4_15mbar10_t2_Simp\n* loop: False\n* slices: 1000”

So it looks like it reads 1 page of 1000 *730 960 3, and not 1000 pages of 730 960 3 as expected. I will have to look how to make readable tiff files from Fiji.

I uploaded a short 5 frames video :
Zone7-1.tif (12.0 MB)

Yes apparently something is not entirely correct at export time, and I’m not sure how to fix it. For a quick solution you can set fastij = False when importing, which gives you access to each “page” separately. With your dataset of 6 frames that gives:

from skimage.external.tifffile import TiffFile
data = TiffFile('Zone7-1.tif', fastij=False)
data.info()

“TIFF file: Zone7-1.tif, 12 MiB, big endian, imagej, 6 pages\n\nSeries 0: 6x730x960x3, uint8, ZYXS, 6 pages, memmap-offset=751\n\nPage 0: 730x960x3, uint8, 8 bit, rgb, raw, imagej|contiguous\n* 254 new_subfile_type (1I) 0\n* 256 image_width (1I) 960\n* 257 image_length (1I) 730\n* 258 bits_per_sample (3H) (8, 8, 8)\n* 262 photometric (1H) 2\n* 270 image_description (43s) b’ImageJ=1.51u\nimages=6\nslices=6\nloop=false\n’\n* 273 strip_offsets (1I) (751,)\n* 277 samples_per_pixel (1H) 3\n* 278 rows_per_strip (1H) 730\n* 279 strip_byte_counts (1I) (2102400,)\n* 50838 imagej_byte_counts (7I) (12, 84, 84, 84, 84, 84, 84)\n* 50839 imagej_metadata (516B) b’IJIJlabl\x00\x00\x00\x06\x00T\x00h\x00i\x00n\x\n\nIMAGEJ_TAGS\n* ImageJ: 1.51u\n* images: 6\n* labels: [‘Thin4_15mbar10_t10_Simple Segmentation.tif’, 'Thin4_15mbar10_t11_Si\n* loop: False\n* slices: 6”

data.pages[0].shape

(730, 960, 3)

len(data.pages)

6

With the example data, I see:

In [1]: from skimage import io                                                      

In [2]: image = io.MultiImage('Zone7-1.tif')                                        

In [3]: len(image)                                                                  
Out[3]: 6

In [4]: image[0].shape                                                              
Out[4]: (730, 960, 3)

This is strange @stefanv. I get the same problem as @lorraine_Montel:

 In [1]: from skimage import io                                                  

In [2]: image = io.MultiImage('Zone7-1.tif')                                    

In [3]: len(image)                                                              
Out[3]: 1

In [4]: image[0].shape                                                          
Out[4]: (6, 730, 960, 3)

In [5]: import skimage                                                          

In [6]: skimage.__version__                                                     
Out[6]: '0.16.2'

@guiwitz note that we have recently (ie on the master branch, not yet released) ripped out our vendored version of tifffile and now depend on the released version. That accounts for the difference between your code and @stefanv’s. (Incidentally, this means you will soon no longer be able to use tifffile from skimage.external.)

@lorraine_Montel depending on your use case, I might recommend dask_image. It is a pure-python library that reads in your images as dask arrays. In the case of your file, it does the right thing and reads planes lazily:

In [12]: image4 = imread('Zone7-1.tif')                                                                                       

In [13]: type(image4)                                                                                                         
Out[13]: dask.array.core.Array

In [14]: image4.shape                                                                                                         
Out[14]: (6, 730, 960, 3)

In [15]: image4.chunks                                                                                                        
Out[15]: ((1, 1, 1, 1, 1, 1), (730,), (960,), (3,))

You can get individual slices (or groups of slices!) with:

In [16]: timepoint3 = np.array(image4[3])                                                                                     
2 Likes

link to dask_image:

Hi @jni,

I’m trying now to import another dataset (from here http://www.cellimagelibrary.org/images/30567 for a course) and I’m running into similar problems, so I thought I would just post here for the sake of continuity. I understand that soon skimage.external won’t be available anymore so I tried to find an alternative, but I have problems “easily” recovering information. I can import my dataset using skimage.io.imread, and the 5D dataset gets imported correctly as a 5D numpy array. Now let’s assume I don’t want to import the entire dataset but single pages. Previously I would use skimage.external.tifffile.Tifffile to do that e.g.:

from skimage.external.tifffile import TiffFile
data = TiffFile('mytif_stack.tif')

And to know how the dimensions are organised, I would use data.info() (as done in a previous post) to see the channel order and the size of each dimension.

Since skimage.external won’t be available anymore, I looked for another solution. As done in this thread I tried MultiImage:

data = skimage.io.MultiImage('mytif_stack.tif')

However I couldn’t find anywhere a way to access any sort of metadata from this. Is there a way? Or is there any other “skimage-way” to read metadata? I have read this issue https://github.com/scikit-image/scikit-image/issues/2605 and I understand (and agree) that there’s no point in re-implement a full metadata-handler in skimage. But I guess there should still be a way to at least read raw metadata or some basic tif tags?

From what I understand, now tifffile is a skimage dependency and gets installed automatically. So I know that I can access metadata through there. But it’s still a bit cumbersome. For example just to recover the size of the dimensions I have to do:

from tifffile import TiffFile
data = TiffFile('mytif_stack.tif')
data.series[0].shape

Here again, I couldn’t find a direct way of accessing metadata trough something like data.info().

I hope there’s a solution I overlooked! If not it would probably good to somehow put back the info() function that was available through skimage.external. Even beginners often try to import multiD datasets and it would be nice that they don’t have to rely on loading tifffile or dask just to have an idea of metadata.

I think I agree with @jni, dask_image or aicsimageio is probably the way to go if you want to load single planes with ease.

Bias: I am a core developer of the aicsimageio library. But in short if you want to load specific chunks of an image you can do things like:

from aicsimageio import AICSImage

# Get an AICSImage object
img = AICSImage("my_file.tiff")
img.get_image_data("CZYX", S=0, T=0)  # returns 4D CZYX numpy array
img.get_image_data("YX", S=0, T=0, C=0, Z=20) # returns 2D YX numpy array

Metadata is returned as an ElementTree or with utility functions:

from aicsimageio import AICSImage

# Get an AICSImage object
img = AICSImage("my_file.tiff")
img.metadata  # returns the metadata object for this image type
img.get_channel_names()  # returns a list of string channel names if found in the metadata
img.get_physical_pixel_size()  # returns a tuple of pixel sizes in XYZ order

The aicsimageio README has many more examples of metadata interaction and such but that is the basics.

3 Likes

Thanks for the info, this is a very cool package, and it worked “as advertised” on my dataset. I’m very impressed by the view_napari() function which worked perfectly in my notebook without having to import anything else. That’s a very nice user-friendly feature!

2 Likes

Thanks! Be on the look out for release 3.2 sometime next week hopefully that adds some more metadata handling, some reader optimizations, and supports reading the LIF file format.

Unfortunately, we have already decided we are removing the view_napari function in aicsimageio>=4.0.0 but we did release napari-aicsimageio which is a plugin for napari for it to do largely the same thing.

1 Like

The view_napari function, even though it will be going away, will be added to documentation as something like “easy napari viewing”. We just didn’t want to manage more dependencies and changing APIs other than the core reading and metadata handling. So that function will be very documented and available just not part of the core library.

Not sure what is “cumbersome” or not “direct” “here”:

TiffFile('file.tif').series[0].shape

vs

TiffFile('file.tif').info()['series'][0]['shape']
2 Likes

@guiwitz scikit-image was not providing anything at all with .info(). It all came from tifffile. Since I think tifffile is as good as it gets at grabbing metadata, I would suggest that you write a utility package to extra the metadata in a format that makes sense to you. (And share it with everyone!)

Regarding standard minimal metadata, there’s lots of discussions scattered all over the place (some of it can be found following links from scikit-image/scikit-image#2605), with folks from zarr, imageio, itk, xarray, … and I expect that it will take a long time to first come to a concensus and then implement that consensus. At skimage, I don’t think we want to spend cycles rolling our own only to have to deprecate and modify it once other libraries coordinate. So, again, rather than rely on us to implement a new .info(), I suggest you build your own tifffile_info() function that extracts the data in a nice format.

Or, just rely on aicsimageio, it does seem great and now lets you drag and drop directly into napari. Which frankly still blows my mind. =D

btw @JacksonMaxfield is the napari plugin lazy?

Simply installing the plugin gives you both a fully in-memory and lazy-load option. When 0.3.0 is released you will see these options in the plugin manager as aicsimageio and aicsimageio-delayed :smiley:

1 Like