libvips / pyvips

python binding for libvips using cffi
MIT License
648 stars 50 forks source link

Multi-channel tiffs loaded as a mosaic #250

Open tdmorello opened 3 years ago

tdmorello commented 3 years ago

Hello! My multi-channel ome-tiff images are loaded as a single plane mosaic. Is this the normal behavior? Are there any recommendations on processing multi channel images?

Thank you!

import sys
import pyvips

print(sys.version)
print(pyvips.__version__)

fname = 'my_image.ome.tif'
for i in range(3):  # there are three channels
    im = pyvips.Image.new_from_file(fname, n=i+1, access=pyvips.Access.SEQUENTIAL)
    print(f'channel {i}: {im.width} {im.height}')

image = pyvips.Image.new_from_file(fname, n=-1)
print(f'full image: {im.width} {im.height}')

Output:

3.9.2 | packaged by conda-forge | (default, Feb 21 2021, 05:02:20) 
[Clang 11.0.1 ]
2.1.8
channel 0: 18944 19865
channel 1: 18944 39730
channel 2: 18944 59595
full image: 18944 59595
jcupitt commented 3 years ago

Hi @tdmorello, yes, that's how OME-TIFFs are stored, they are always single plane images. You'll need to chop and recombine if you want to make a regular RGB.

Could you upload a sample somewhere?

tdmorello commented 3 years ago

Hi John, thank you for getting back so quickly!

I get the same result with this image:

https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/bioformats-artificial/multi-channel.ome.tiff

My goal is to flip and rotate multi channel ome-tiffs, and save them back as multi-channel ome-tiffs, keeping the ome metadata.

tdmorello commented 3 years ago

This image, for example, loads like this:

import pyvips
import sys
import matplotlib.pyplot as plt
import numpy as np
print(sys.version)
print(pyvips.__version__)

format_to_dtype = {
    'uchar': np.uint8,
    'char': np.int8,
    'ushort': np.uint16,
    'short': np.int16,
    'uint': np.uint32,
    'int': np.int32,
    'float': np.float32,
    'double': np.float64,
    'complex': np.complex64,
    'dpcomplex': np.complex128,
}

# fname = 'my_image.ome.tif'
fname = "/Users/tim/Downloads/multi-channel.ome.tiff"
for i in range(3):  # there are three channels
    im = pyvips.Image.new_from_file(fname, n=i+1, access=pyvips.Access.SEQUENTIAL)
    print(f'channel {i}: {im.width} {im.height}')

image = pyvips.Image.new_from_file(fname, n=-1)
print(f'full image: {im.width} {im.height}')

img = np.ndarray(buffer=image.write_to_memory(),
                   dtype=format_to_dtype[image.format],
                   shape=[image.height, image.width, image.bands])

plt.imshow(img)
plt.show()
3.9.2 | packaged by conda-forge | (default, Feb 21 2021, 05:02:20) 
[Clang 11.0.1 ]
2.1.8
channel 0: 439 167
channel 1: 439 334
channel 2: 439 501
full image: 439 501

Figure_1

tdmorello commented 3 years ago

Your response on stackoverflow was very helpful.

I think the only additional step for me is to update the OME-XML to reflect the new image dimensions after transformations. The OME writer holds onto dimensions (width and height) from the original pre-transformed image, which leads to a warning when opening the OME-TIFF in ImageJ.

The nascent downsampled pyramid images have the correct dimensions in the OME-XML.

jcupitt commented 3 years ago

I'd do it like this:

#!/usr/bin/python3

import sys
import pyvips

# ome images load as a tall, thin strip, with page-height indicating the breaks
image = pyvips.Image.new_from_file(sys.argv[1], n=-1)
page_height = image.get("page-height")

# chop into pages
pages = [image.crop(0, y, image.width, page_height)
         for y in range(0, image.height, page_height)]

# join pages band-wise to make an interleaved image
image = pages[0].bandjoin(pages[1:])

# set the rgb hint
image = image.copy(interpretation="srgb")

image.write_to_file(sys.argv[2])

Then:

$ ./ome2vips.py ~/pics/ome/multi-channel.ome.tiff x.png

To make:

x

(you can see colour in the 1/2/3)