How to specify colormap when saving tiff stack
Asked Answered
B

3

6

I'm using tifffile in python to save out 3-channel tiff stacks, which I then want to read into ImageJ or FIJI. These tiff stacks open as composites in ImageJ with each channel assigned a (presumably default) colormap/LUT. However, the colors that are assigned aren't the colors that make sense for my images. My problem is that I can't figure out how to specify the colormap for each channel when saving the image using tifffile.

For example, I'd like to have the following colormap assignments:

  • ch 0: grays
  • ch 1: green
  • ch 2: red

Here's the code that I'm using to save the files:

# save hyperstack
with tifffile.TiffWriter(filename, bigtiff=False, imagej=True) as tif:
    for i in range(t_stack.shape[0]):
        tif.save(t_stack[i], metadata={'Composite mode': 'composite'})

There must be metadata that's saved with the tiff that holds the channel colormap info because I can manually edit the color assignment in ImageJ and then save it, close it, and then when I open the file up again it retains my manual colormap assignments. So I'm guessing there must be a metadata tag (maybe colormap?) that can be used to specify channel colors, but I can't find any info on what tag or syntax to use.

Butterscotch answered 9/5, 2018 at 16:41 Comment(0)
H
9

Create the private IJMetadata (50839) and IJMetadataByteCounts (50838) TIFF tags on your own and pass them to tifffile.imsave as extratags. IJMetadata contains application internal metadata in a binary format. The color information is in the luts metadata:

import struct
import numpy
import tifffile


def imagej_metadata_tags(metadata, byteorder):
    """Return IJMetadata and IJMetadataByteCounts tags from metadata dict.

    The tags can be passed to the TiffWriter.save function as extratags.

    """
    header = [{'>': b'IJIJ', '<': b'JIJI'}[byteorder]]
    bytecounts = [0]
    body = []

    def writestring(data, byteorder):
        return data.encode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])

    def writedoubles(data, byteorder):
        return struct.pack(byteorder+('d' * len(data)), *data)

    def writebytes(data, byteorder):
        return data.tobytes()

    metadata_types = (
        ('Info', b'info', 1, writestring),
        ('Labels', b'labl', None, writestring),
        ('Ranges', b'rang', 1, writedoubles),
        ('LUTs', b'luts', None, writebytes),
        ('Plot', b'plot', 1, writebytes),
        ('ROI', b'roi ', 1, writebytes),
        ('Overlays', b'over', None, writebytes))

    for key, mtype, count, func in metadata_types:
        if key not in metadata:
            continue
        if byteorder == '<':
            mtype = mtype[::-1]
        values = metadata[key]
        if count is None:
            count = len(values)
        else:
            values = [values]
        header.append(mtype + struct.pack(byteorder+'I', count))
        for value in values:
            data = func(value, byteorder)
            body.append(data)
            bytecounts.append(len(data))

    body = b''.join(body)
    header = b''.join(header)
    data = header + body
    bytecounts[0] = len(header)
    bytecounts = struct.pack(byteorder+('I' * len(bytecounts)), *bytecounts)
    return ((50839, 'B', len(data), data, True),
            (50838, 'I', len(bytecounts)//4, bytecounts, True))


filename = 'FluorescentCells.tif'
image = tifffile.imread(filename)

grays = numpy.tile(numpy.arange(256, dtype='uint8'), (3, 1))
red = numpy.zeros((3, 256), dtype='uint8')
red[0] = numpy.arange(256, dtype='uint8')
green = numpy.zeros((3, 256), dtype='uint8')
green[1] = numpy.arange(256, dtype='uint8')
ijtags = imagej_metadata_tags({'LUTs': [grays, green, red]}, '>')

tifffile.imsave('test_ijmetadata.tif', image, byteorder='>', imagej=True,
                metadata={'mode': 'composite'}, extratags=ijtags)
Headwork answered 9/5, 2018 at 23:15 Comment(2)
Thank you!! About 4 hours of googling and trying stuff didn't even get me close to something like this... is there documentation somewhere that explains this??Butterscotch
Can you please explain what exactly what part of this tells ImageJ to use a green pseudocolor, for example? Or, what I'm really after is how would you extend this to add additional colors (e.g. blue, pink, yellow) for an image with more than 3 channels?Butterscotch
S
5

You can pass a number of keyword arguments to tifffile's imsave function. It's not very well documented, so what I found most helpful was to read the docstring for the save function in the TiffWriter class:

https://github.com/blink1073/tifffile/blob/master/tifffile/tifffile.py#L750

For ImageJ metadata specifications, TiffWriter.save then refers to imagej_metadata_tags where you can see what types of data you can store in the variable metadata_types (line 7749):

https://github.com/blink1073/tifffile/blob/master/tifffile/tifffile.py#L7710

metadata_types = (
    ('Info', b'info', 1, _string),
    ('Labels', b'labl', None, _string),
    ('Ranges', b'rang', 1, _doubles),
    ('LUTs', b'luts', None, _ndarray),
    ('Plot', b'plot', 1, _bytes),
    ('ROI', b'roi ', 1, _bytes),
    ('Overlays', b'over', None, _bytes))

You can create LUTs to visualize your data using different colormaps. Presumably your data is uint8, then the LUTs you will need have the shape (3, 256) for the 3 color channels and 256 intensity values. So for gray, green and red LUTs you will need to something along the lines:

import numpy as np
import tifffile

# Create a random test image
im_3frame = np.random.randint(0, 255, size=(3, 150, 250), dtype=np.uint8)
# Intensity value range
val_range = np.arange(256, dtype=np.uint8)
# Gray LUT
lut_gray = np.stack([val_range, val_range, val_range])
# Red LUT
lut_red = np.zeros((3, 256), dtype=np.uint8)
lut_red[0, :] = val_range
# Green LUT
lut_green = np.zeros((3, 256), dtype=np.uint8)
lut_green[1, :] = val_range
# Create ijmetadata kwarg
ijmeta = {'LUTs': [lut_gray, lut_red, lut_green]}
# Save image
tifffile.imsave(
    save_name,
    im_rgb,
    imagej=True,
    metadata={'mode': 'composite'},
    ijmetadata=ijmeta,
) 
Sirkin answered 12/10, 2018 at 21:39 Comment(2)
Thanks, @Jenny Folkesson. Can you explain what exactly what part of this tells ImageJ to use a green pseudocolor, for example? Or, what I'm really after is how would you extend this to add additional colors (e.g. blue, pink, yellow)?Butterscotch
Just as you create lut_red and lut_green by filling the the first vs second of three columns with an intensity range, you can create an lut_blue by lut_blue[2,:] = val_range, and add intensity value ranges to a combination of columns for other colors. Hope that helps.Sirkin
M
2

I recently came across this thread while looking for a solution to save tiff files with ImageJ metadata for more than 3 color channels in addition to a gray channel. The solutions described above were very helpful and I extended the example for additional channels.

In ImageJ one can use up to 7 different color channels in composite mode based on the RGB color scheme - the three primary colors red, green and blue, mixtures of 2 primary colors resulting in yellow, magenta and cyan as well as a gray channel.

To add a blue LUT you would simply define a ndarray as shown in the example above for the red or green LUT but assign the intensity values ranging from 0 to 255 to the third array while the other two arrays (red and green) are filled with zeros.

lut_blue = np.zeros((3, 256), dtype=np.uint8)
lut_blue[2, :] = val_range

By 'mixing' of e.g. the primary colors red and green one could now generate a yellow LUT.

lut_yellow= np.zeros((3, 256), dtype='uint8')
lut_yellow[[0,1],:] = np.arange(256, dtype='uint8')

The example given below will result in the generation of a tiff file with 7 channels. The color assignment to the images in the tiff stack is defined by:

ijmeta = {'LUTs': [lut_gray, lut_red, lut_green, lut_blue, lut_yellow, lut_magenta, lut_cyan]}

and can be adjusted as required. The complete code based on the example by Jenny Folkesson looks as follows:

import numpy as np
from tifffile import imread, imsave

# Create a random test image
im_3frame = np.random.randint(0, 255, size=(7, 150, 250), dtype=np.uint8)
# Intensity value range
val_range = np.arange(256, dtype=np.uint8)
# Gray LUT
lut_gray = np.stack([val_range, val_range, val_range])
# Red LUT
lut_red = np.zeros((3, 256), dtype=np.uint8)
lut_red[0, :] = val_range
# Green LUT
lut_green = np.zeros((3, 256), dtype=np.uint8)
lut_green[1, :] = val_range
# Blue LUT
lut_blue = np.zeros((3, 256), dtype=np.uint8)
lut_blue[2, :] = val_range
# Yellow LUT
lut_yellow= np.zeros((3, 256), dtype='uint8')
lut_yellow[[0,1],:] = np.arange(256, dtype='uint8')
# Magenta LUT
lut_magenta= np.zeros((3, 256), dtype='uint8')
lut_magenta[[0,2],:] = np.arange(256, dtype='uint8')
# Cyan LUT
lut_cyan= np.zeros((3, 256), dtype='uint8')
lut_cyan[[1,2],:] = np.arange(256, dtype='uint8')


# Create ijmetadata kwarg
ijmeta = {'LUTs': [lut_gray, lut_red, lut_green, lut_blue, lut_yellow, lut_magenta, lut_cyan]}
# Save image
imsave(
    'test.tif',
    im_3frame,
    imagej=True,
    metadata={'mode': 'composite'},
    ijmetadata=ijmeta,
) 
Meath answered 23/4, 2019 at 9:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.