Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserving singlet dimensions when loading ImageJ hyperstack tifs #19

Closed
nhthayer opened this issue Aug 3, 2020 · 8 comments
Closed
Labels
question Further information is requested

Comments

@nhthayer
Copy link

nhthayer commented Aug 3, 2020

Noticed a change in behavior between 2019.5.30 -> 2019.6.18 and wanted to make sure it was intended. Hopefully I'm just missing a new way of preserving dimensions of size 1 when loading ImageJ hyperstack tifs.

# works on tifffile==2019.5.30
# assert fails on tifffile==2019.6.18
import numpy as np
import tifffile
a = np.zeros((10, 5, 1, 256, 256), dtype='uint16')
md = {'frames': 10, 'slices': 5, 'channels': 1}
tifffile.imsave('test.tif', a, imagej=True, metadata=md)
b = tifffile.imread('test.tif')
assert b.shape == a.shape, f'Loaded array has shape {b.shape}, not {a.shape}'

In the new version, it appears that dimensions of size 1 are not included in the final stack shape, even if they are specified in the imagej metadata:

tifffile/tifffile/tifffile.py

Lines 2630 to 2648 in 6991c4b

images = ij.get('images', len(pages))
frames = ij.get('frames', 1)
slices = ij.get('slices', 1)
channels = ij.get('channels', 1)
mode = ij.get('mode', None)
hyperstack = ij.get('hyperstack', False)
shape = []
axes = []
if frames > 1:
shape.append(frames)
axes.append('T')
if slices > 1:
shape.append(slices)
axes.append('Z')
if channels > 1 and (page.photometric != 2 or mode != 'composite'):
shape.append(channels)
axes.append('C')

In the older version, the channel dim would be included if it was specified in the metadata:

tifffile/tifffile/tifffile.py

Lines 2526 to 2529 in ffbdf61

if 'channels' in ij and not (page.photometric == 2 and not
ij.get('hyperstack', False)):
shape.append(ij['channels'])
axes.append('C')

@cgohlke
Copy link
Owner

cgohlke commented Aug 3, 2020

That is intended. AFAIK ImageJ does not write channels, slices, or frames if they are 1. Tifffile determines images, channels, slices, frames from the array shape and does not let users overwrite them using metadata. The singlet dimensions can be recovered using the axes property of ImageJ series, e.g with the transpose_axes function.

tifffile/tifffile/tifffile.py

Lines 12547 to 12570 in 9870837

def transpose_axes(image, axes, asaxes=None):
"""Return image with its axes permuted to match specified axes.
A view is returned if possible.
>>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
(5, 2, 1, 3, 4)
"""
for ax in axes:
if ax not in asaxes:
raise ValueError(f'unknown axis {ax}')
# add missing axes to image
if asaxes is None:
asaxes = 'CTZYX'
shape = image.shape
for ax in reversed(asaxes):
if ax not in axes:
axes = ax + axes
shape = (1,) + shape
image = image.reshape(shape)
# transpose axes
image = image.transpose([axes.index(ax) for ax in asaxes])
return image

@nhthayer
Copy link
Author

nhthayer commented Aug 3, 2020

Thank you for the clarification.

I tend to read/write 5D tifs with Tifffile, and keeping the same dimensions when saving/loading is important to me. I know this can be achieved by omitting the imagej=True in tifffile.imsave.

I had been using the imagej option so tifs would open as a hyperstack if I needed to visually inspect any of the data with ImageJ. I guess I had been more tolerant of singlet dimensions being squeezed when interactively viewing the data.

Do you have any suggestion on how this can be achieved now - creating a tif that would load with the "correct dimensions" with only tifffile.imread and still open as a hyperstack in ImageJ? The ability to include other ImageJ metadata (lookup tables, min/max settings, etc.) would be a plus.

Also, feel free to close the issue, as maybe this is getting beyond the scope of your project.

@cgohlke
Copy link
Owner

cgohlke commented Aug 3, 2020

Using transpose_axes should be able to achieve what you want. E.g.:

def imread(filename):
    with TiffFile(filename) as tif:
        axes = tif.series[0].axes
        hyperstack = tif.series[0].asarray()
    return transpose_axes(hyperstack, axes, 'TZCYXS')

@nhthayer
Copy link
Author

nhthayer commented Aug 4, 2020

That would definitely work for me, but I think I will opt for preserving the singlet dimensions by giving up the imagej option.

I was hoping for a .tif that would open as a hyperstack in ImageJ and preserve the dimensions when loaded with the native tifffile.imread function. However, I appreciate that the new behavior replicates what ImageJ does and actually allows you to use frames, slices and channels as keys in the metadata and still open the file!

Thank you for your help and the time that you put into this!

@nhthayer nhthayer closed this as completed Aug 4, 2020
@cgohlke cgohlke added the question Further information is requested label Dec 14, 2020
@AndrewGYork
Copy link

I have the same problem as nhthayer; I want to save and load 5d numpy arrays that open as hyperstacks in imagej, and don't drop singlet dimensions. I guess I can redefine imread in every script I have that uses tifffile, but it seems like the old behavior was strictly superior. What am I missing?

(Disclaimer: I deeply appreciate the work you put into tifffile, and I know you don't owe me your valuable labor for free. I hope my question isn't annoying, and I hope my appreciation and respect for your work is clear.)

@cgohlke
Copy link
Owner

cgohlke commented Feb 10, 2021

IIUC you want an option to include e.g. channels=1 in the ImageJ file and have tifffile return the singlet dimension in the array when reading, even tough ImageJ itself is unable to produce such files and probably no other software will keep the dimension?

@AndrewGYork
Copy link

This was the old behavior, right? Load(save(x)).shape == x.shape, and if you open the file in imagej, it opens as a hyperstack.

I think this use case is fairly common: tif as basically numpy arrays on disk, with easy casual inspection via imagej, but for any serious work you load back to python.

@cgohlke
Copy link
Owner

cgohlke commented Feb 10, 2021

I am -1 on extending the ImageJ format for tifffile. Maybe an option squeeze=False in the reader, which returns axes of length one instead of squeezing them out? That would apply to formats with defined axes order, e.g. ImageJ, OME-TIFF, and LSM files. Basically a convenience to avoid calling transpose_axes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants