-
Notifications
You must be signed in to change notification settings - Fork 50
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
Writing multi-channel pyramids #502
Comments
Hello @petroslk, TIFF doesn't support multichannel images that well -- for example:
So a 100 channel image is saved as RGB, with 97 extra unassociated alpha channels (!!). This works, but not many systems will be able to read files like this. The usual trick with TIFF is to save these as multi-page images instead, so this would be a 100 page image, with each page holding one channel. OME-TIFF adds some extra XML metadata to the However, this means you can't easily save pyramids, since TIFF usually uses the "pages" dimension to keep pyramid levels. OME-TIFF uses an extra, hidden, sub-IFD dimension hanging off each page to hold the pyramid. The image looks like:
libvips can read and write this layout, but you need to make the many-page image yourself, and write the XML yourself. Something like: #!/usr/bin/python3
import sys
import pyvips
im = pyvips.Image.new_from_file(sys.argv[1])
image_height = im.height
# split to separate image planes and stack vertically ready for OME
im = pyvips.Image.arrayjoin(im.bandsplit(), across=1)
# set minimal OME metadata
# before we can modify an image (set metadata in this case), we must take a
# private copy
im = im.copy()
im.set_type(pyvips.GValue.gint_type, "page-height", image_height)
im.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
<Image ID="Image:0">
<!-- Minimum required fields about image dimensions -->
<Pixels DimensionOrder="XYCZT"
ID="Pixels:0"
SizeC="{im.bands}"
SizeT="1"
SizeX="{im.width}"
SizeY="{image_height}"
SizeZ="1"
Type="uint8">
</Pixels>
</Image>
</OME>""")
im.tiffsave(sys.argv[2], compression="lzw", tile=True,
tile_width=512, tile_height=512,
pyramid=True, subifd=True) I'd use something like QuPath for testing. Perhaps you are using this already. |
Hey @jcupitt Thanks a lot for your feedback! Makes a lot more sense now. I did try your short snippet but still it only wrote one of the channels. Tried something like this aswell but it only writes 2 channels?
Would it be easier to write each channel as a separate tif and then merge them? Thanks a lot! Petros |
Ooop! You're right, it should get #!/usr/bin/env python3
import sys
import pyvips
im = pyvips.Image.new_from_file(sys.argv[1])
num_channels = im.bands
image_height = im.height
# split to separate image planes and stack vertically ready for OME
im = pyvips.Image.arrayjoin(im.bandsplit(), across=1)
# set minimal OME metadata
# before we can modify an image (set metadata in this case), we must take a
# private copy
im = im.copy()
im.set_type(pyvips.GValue.gint_type, "page-height", image_height)
im.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
<Image ID="Image:0">
<!-- Minimum required fields about image dimensions -->
<Pixels DimensionOrder="XYCZT"
ID="Pixels:0"
SizeC="{num_channels}"
SizeT="1"
SizeX="{im.width}"
SizeY="{image_height}"
SizeZ="1"
Type="uint8">
</Pixels>
</Image>
</OME>""")
im.tiffsave(sys.argv[2], compression="lzw", tile=True,
tile_width=512, tile_height=512,
pyramid=True, subifd=True) I made a six-band test image:
Then ran that code and examined the TIFF structure:
You can see it's written a six page TIFF from the six-band image, and each page has a pyramid on it. It loads into QuPath as a 6-channel image: You'd need to adjust the XML if you want QuPath to label your fluorescence images correctly, of course. |
... I meant to say, You can see the six bands in the info bar at the bottom. That's the default "Pages as bands", but if you select "Toilet roll" in the menu on the bottom left you see: So you can examine every band separately. |
Did you update your |
I think I did yes: #!/usr/bin/env python3
import sys
import pyvips
im = pyvips.Image.new_from_file(sys.argv[1])
num_channels = im.bands
image_height = im.height
# split to separate image planes and stack vertically ready for OME
im = pyvips.Image.arrayjoin(im.bandsplit(), across=1)
# set minimal OME metadata
# before we can modify an image (set metadata in this case), we must take a
# private copy
im = im.copy()
im.set_type(pyvips.GValue.gint_type, "page-height", image_height)
im.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
<Image ID="Image:0">
<!-- Minimum required fields about image dimensions -->
<Pixels DimensionOrder="XYCZT"
ID="Pixels:0"
SizeC="{num_channels}"
SizeT="1"
SizeX="{im.width}"
SizeY="{image_height}"
SizeZ="1"
Type="uint8">
</Pixels>
</Image>
</OME>""")
im.tiffsave(sys.argv[2], compression="lzw", tile=True,
tile_width=512, tile_height=512,
pyramid=True, subifd=True) Then tried to convert the ome.tif to essentially another ome.tif just to test:
The vipsheader result for the test2.tif file looks like this, identical to the original ome.tif, but only has a single channel when oppened on vipsdisp or QuPath:
Once again, thank you for your help! If you're not sure what the source of the problem might be, I will try some other solutions, or possibly transfer the test file! Thank you for your patience. |
Wait, is your source image already an OME TIFF? But then why are you trying to convert it? Are you loading it, doing some processing, and trying to save again? Just as you have to do that stuff with bandsplit to save as an OME TIFF, you have to do the opposite conversion when you load an OME TIFF (turn the Y dimension into colour). Or if you are copying one OME TIFF to another, you can skip the bandsplit / pagesplit and just copy the pixels over. Yes, I think a sample image would make everything clearer. |
Yes, pretty much, at this point the only thing I need to do is take the one tif per IF channel that I have performed the modifications on and merge all of those TIFs together into an OME.TIF. How would you go about merging a series of tif files into a multi channel ome.tif file with pyvips? Thanks a lot! |
Could you post a sample image? It'd help me understand exactly what you're working with. |
I have just sent you an email with the example file and the use case! Hope that helps! Thanks a lot!! |
Wow that's a pretty crazy pyramid, I've not seen one like that before. I wish manufactures would follow conventions :(
Assuming OME-TIFF is OK, you need to extract the first 7 pages (looks like those are the full-res scans), process them, then save. Something like: #!/usr/bin/env python3
import sys
import pyvips
filename = "Tonsil_Scan1.er.qptiff"
# you should find this number from examining the XML stored in the
# IMAGEDESCRIPTION
n_pages = 7
# extract full res bands
bands = [pyvips.Image.new_from_file(filename, page=i)
for i in range(n_pages)]
# process each band in some way ... here I just flip the greyscale
bands = [(65535 - band).cast("ushort") for band in bands]
# reassemble into a tall, thin strip
image = pyvips.Image.arrayjoin(bands, across=1)
# set the OME-TIFF metainfo
image = image.copy()
image.set_type(pyvips.GValue.gint_type, "page-height", bands[0].height)
image.set_type(pyvips.GValue.gstr_type, "image-description",
f"""<?xml version="1.0" encoding="UTF-8"?>
<OME xmlns="http://www.openmicroscopy.org/Schemas/OME/2016-06"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd">
<Image ID="Image:0">
<!-- Minimum required fields about image dimensions -->
<Pixels DimensionOrder="XYCZT"
ID="Pixels:0"
SizeC="{n_pages}"
SizeT="1"
SizeX="{bands[0].width}"
SizeY="{bands[0].height}"
SizeZ="1"
Type="uint16">
</Pixels>
</Image>
</OME>""")
print(f"saving to x.tif ...")
image.tiffsave("x.tif", compression="lzw", tile=True,
tile_width=512, tile_height=512,
pyramid=True, subifd=True, bigtiff=True) It seems to load into QuPath OK: |
Dear @jcupitt This works perfectly! Thank you so much for your help! Indeed this file was quite tricky to work with, but at least we figured it out now! Cheers, Petros |
Hey there,
I had a question regarding the writing of pyramids with more than 3 channels. In my case, these are multiplex immunofluorescence images with ~20 channels per magnification layer.
When reading them I can see that all the pages are correctly read:
But when I write this to a tiff file, it only seems to write one channel per pyramid layer:
Can I actually write such images directly?
Thanks a lot!
Petros
The text was updated successfully, but these errors were encountered: