libvips / pyvips

python binding for libvips using cffi
MIT License
642 stars 49 forks source link

Save multiband tiff file #262

Closed petoor closed 3 years ago

petoor commented 3 years ago

Hello John. I have 3 RGB images i have merged in the color channel. It should be possible to save it as a 9 channel image. However, when i save it and reload it, it only saves the first 3 channels.

<pyvips.Image 3360x3456 uchar, 9 bands, srgb>
img.tiffsave("img.tiff", compression="jpeg")
img.tiffload("img.tiff")
<pyvips.Image 3360x3456 uchar, 3 bands, srgb>

I know i can use img.join(img2, "vertical") and specify the page height of the image "toilet paper roll" style. However, it will be more clean to be able to save the images combined in the band channels not changing height or width. Is this possible?

Best regards.

petoor commented 3 years ago

Turns out you have already answered this question : https://github.com/libvips/pyvips/issues/222

jcupitt commented 3 years ago

In case anyone else comes across this question, here's some demo code:

#!/usr/bin/python3

import sys
import pyvips

im = pyvips.Image.new_from_file(sys.argv[1])
print(f"image has {im.bands} bands")

# put the image together with itself three times, so for example an RGB 
# source will become a 9-band image
manyband = im.bandjoin([im, im])

manyband.write_to_file(sys.argv[2])

im2 = pyvips.Image.new_from_file(sys.argv[2])
print(f"after save and reload, image has {im2.bands} bands")

I can run it like this:

john@banana ~/try $ ./manyband.py ~/pics/k2.jpg x.tif
image has 3 bands
after save and reload, image has 9 bands
john@banana ~/try $ !tiff
tiffinfo x.tif
TIFF Directory at offset 0x197d008 (26726408)
  Image Width: 1450 Image Length: 2048
  Resolution: 72.009, 72.009 pixels/inch
  Bits/Sample: 8
  Sample Format: unsigned integer
  Compression Scheme: None
  Photometric Interpretation: RGB color
  Extra Samples: 6<unassoc-alpha, unassoc-alpha, unassoc-alpha, unassoc-alpha, unassoc-alpha, unassoc-alpha>
  Orientation: row 0 top, col 0 lhs
  Samples/Pixel: 9
  Rows/Strip: 128
  Planar Configuration: single image plane

So the three band RGB image becomes a 9 band RGB TIFF, with the extra six bands tagged as unassoc-alpha (ie. extra image bands with no alpha premultiplication). This might or might not load into imagej etc.

Some tile formats have strict limits on the number of image bands they allow, for example:

john@banana ~/try $ ./manyband.py ~/pics/k2.jpg x.ppm
image has 3 bands
after save and reload, image has 3 bands

PPM only allows 1 or 3 bands, so the extra 6 are dropped on save.

petoor commented 3 years ago

Thank you John.

While https://github.com/libvips/pyvips/issues/222#issuecomment-748509935 gives the correct image, when loading it pyvips displays. <pyvips.Image 3360x3456 uchar, 1 bands, b-w> Would it be possible to get pyvips to state the correct metadata directly? so SizeC, SizeT, SizeX, SizeY, SizeZ from the ome.tiff format. Idearly with the pyramid information as well. Or at least at which page the different C, T and Z are located?

This is simply a feature request as newcommers to vips might look at the above and think it is strange that their ome.tiff image looks "flat"

jcupitt commented 3 years ago

Ah, this is an OME TIFF? They are rather different (and complicated).

You need to load all the page of the TIFF, then bandjoin them back into an N-band interleaved image. You can use the subifd parameter to select the pyramid layer.

There's some sample OME save code here:

https://forum.image.sc/t/writing-qupath-bio-formats-compatible-pyramidal-image-with-libvips/51223/6

You'd need to do the inverse for OME load. Do you need sample code?

petoor commented 3 years ago

Actually in this case it is 3 ndpi files (different staining) I need to combine into one image. The format of the combined image is not important, however ome.tiff seems to do the job rather nicely.

I guess the ome.tiff metadata provided in https://forum.image.sc/t/writing-qupath-bio-formats-compatible-pyramidal-image-with-libvips/51223/6 makes it an ome.tiff file?

It would be very nice with some ome load sample code. Ideally where one specifies t, z and c and the corresponding "toilet paper sheet" is returned.

Also, is there a way to see how deep the pyramid is? That is, number of subifd's per sheet.

jcupitt commented 3 years ago

Yes, use image.get("n-subifds") to get he number of subifds (pyramid layers). You can use vipsheader -a to see all the fields, eg.:

$ vips copy CMU-1.svs x.tif[tile,pyramid,compression=jpeg,subifd]
$ vipsheader -a x.tif
x.tif: 46000x32914 uchar, 3 bands, srgb, tiffload
width: 46000
height: 32914
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 2004.01
yres: 2004.01
filename: x.tif
vips-loader: tiffload
n-subifds: 9
n-pages: 1
resolution-unit: cm
orientation: 1

Are you planning to load the OME TIFF into QuPath, or bioformats?

Post some (small) sample images and I'll make you a converter.

petoor commented 3 years ago

Perfect. Thanks.

I have a tile server connected to some JavaScript that serves the images, so neither in my case. Is there a difference of how QuPath and bioformats treats the metadata though?

If you haven't any ome.tiff load code already it is fine, I don't want you to spend time on it.

As a feature request it would be nice to have tiffload where you specify the Time and Z (if applicable by the metadata) and maybe channel (I guess you want all by default), this makes as much sense as having page as a argument, at least to me. However I also respect if it doesn't fit into the design of pyvips.

jcupitt commented 3 years ago

I found an ome load example:

#!/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])

You can pick a pyr layer with subifd=, for example:

$ vipsheader -a x.tif
x.tif: 24960x34560 uchar, 1 band, b-w, tiffload
width: 24960
height: 34560
bands: 1
format: uchar
coding: none
interpretation: b-w
xoffset: 0
yoffset: 0
xres: 2008.05
yres: 2008.05
filename: x.tif
vips-loader: tiffload
n-subifds: 9
n-pages: 5
image-description: <?xml version="1.0" encoding="UTF-8"?><!-- Warning: this comment is an OME-XML metadata block, which contains crucial dimensional parameters and other important metadata. Please edit cautiously (if at all), and back up the original data before doing so...
resolution-unit: cm
orientation: 1

Then:

$ ~/try/ome2vips.py x.tif[subifd=6] layer6.tif[compression=jpeg,tile]
$ vipsheader layer6.tif
layer6.tif: 195x270 uchar, 3 bands, srgb, tiffload
petoor commented 3 years ago

Hi John.

Thank you for the code.

Isn't it a problem to set the interpretation to standard RGB in the case of 5 pages (5 color channels)? If i understand your x.tif image correctly that is of course. One could argue that for images where bands != 3 RGB is doesn't make much sense. Wouldn't it be better to just keep it b-w or multiband ?

jcupitt commented 3 years ago

I think it doesn't really matter. TIFF only supports 1, 3 and 4 bands, and any others have to be tagged as alpha.

RGB is useful because it'll probably look better in viewers.