Lookup tables

Prerequisites

Before starting this lesson, you should be familiar with:

Learning Objectives

After completing this lesson, learners should be able to:
  • Understand how the numerical values in an image are transformed into colourful images.

  • Understand what a lookup table (LUT) is and how to adjust it.

  • Appreciate that choosing the correct LUT is a very serious responsibility when preparing images for a talk or publication.

Motivation

Images are a collection of a lot (millions) of values, which is information that is hard to process for our human brains. Thus, one typically assigns a color to each distinct value, by means of a lookup table (LUT). There is no fix recipe for how to adjust this mapping from numbers to colors. It is easy to chose a mapping that hides certain information in an image, while emphasising other information. Thus, configuring this mapping properly is a great responsibility that scientists have to take on when presenting their image data.

Concept map

graph TD V("Image pixel value") --> L("Lookup table (LUT)") L --> |does not change|V L --> |changes|C("Displayed pixel color & brightness")



Figure


Left: Image displayed with a grey LUT and the color mapping as an inset. Right: Image shown with several different LUTs.



Activities

Explore LUTs


Show activity for:  

ImageJ GUI

  • Open an image
  • Change the contrast settings
    • [ Image › Adjust › Brightness/Contrast… ]
    • Explore different min and max values
    • Appreciate that at certain settings a very dim nucleus becomes visible
    • Check that the pixel values did not change
    • Never click [ Apply ]
  • Explore various single color LUTs, e.g.
    • [ Image › Lookup Tables › Green ]
    • [ Image › Lookup Tables › Blue ]
    • [ Image › Lookup Tables › Red ]
      • Avoid red! 15% of males cannot see anything here!
      • Magenta can be a better alternative.
  • Explore various multi color LUTs, e.g.
    • [ Image › Lookup Tables › Fire ]
      • Good for this high-dynamic range image
    • [ Image › Lookup Tables › HiLo ]
      • Good to see extreme values
  • Show the LUT
    • Especially useful for multi-color LUTs
    • [ Analyze › Tools › Calibration Bar… ]
    • Explore the various settings

ImageJ Macro

// File › Close All
run("Close All");

// File › Open...
open("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__nuclei_high_dynamic_range.tif");

// Image › Rename...
rename("gray_0_255"); 

// Analyze › Tools › Calibration Bar...
run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");

// Image › Duplicate...
run("Duplicate...", "title=gray_0_43");

// Image › Adjust › Brightness/Contrast..
setMinAndMax(0, 43);

run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");
run("Duplicate...", "title=blue_0_255");
run("Blue");
setMinAndMax(0, 255);

run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");
run("Duplicate...", "title=fire_0_255");
run("Fire");

run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");
run("Duplicate...", "title=hilo_0_255");
run("HiLo");

run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");
run("Duplicate...", "title=hilo_20_100");
setMinAndMax(20, 100);

run("Calibration Bar...", "location=[Upper Left] fill=None label=White number=5 decimal=0 font=5 zoom=0.7 overlay");
run("Tile");

skimage napari

# %% [markdown]
# # Explore LUTs

# %%
# Instantiate the napari viewer
import napari
viewer = napari.Viewer()

# %%
# Read the image
from OpenIJTIFF import open_ij_tiff
image, axes, scales, units = open_ij_tiff('https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__nuclei_high_dynamic_range.tif')

# %% [markdown]
# ### Napari GUI alternative to load data
# Drag and drop and rename the layer (alternative for loading data)\
# Change name of layer `viewer.layers[0].name = 'image_grayscale'` \
# Get the data as numpy array `image = viewer.layers['image_grayscale'].data` 

# %%
# Check image type and values
import numpy as np
print(image.dtype, np.min(image), np.max(image))

# %%
# View the intensity image as grayscale
viewer.add_image(image, name='image_grayscale', colormap='gray')

# %%
# Change brightness and contrast
viewer.layers['image_grayscale'].contrast_limits=(100, 175)

# %% [markdown]
# **Napari GUI** explore different contrast limits

# %%
# View the intensity image as grayscale
viewer.add_image(image, name='image_grayscale2', colormap='gray')

# %% [markdown]
# **Napari GUI** visualize images side by side\
# **Napari GUI** change brightness and contrast to visualize dim nuclei

# %%
# Check available colormap
print(list(napari.utils.colormaps.AVAILABLE_COLORMAPS))

# %%
# Change colormap
viewer.add_image(image, name='image_turbo', colormap='turbo')

# %% [markdown]
# **Napari GUI** explore different LUTs

# %%
# Extract image data from the layers
image_grayscale = viewer.layers['image_grayscale'].data
image_grayscale2 = viewer.layers['image_grayscale2'].data

# %%
# Compare raw data
print(image_grayscale[0:5,0:5])
print(image_grayscale2[0:5,0:5])
print((image_grayscale == image_grayscale2).all())



Display several images with same LUT settings

Display image sets with the same gray scale LUT and the same contrast settings. Visualise the LUT as an inset in both images (you may also attempt to visualise the LUT only once outside the images). This is what one typically should do for a presentation or publication for data that were acquired with the same microscope settings.

Example data

Show activity for:  

ImageJ GUI

  • Open one of the above pairs of images
  • Choose a suitable LUT using Image › Lookup Tables › ...
  • Adjust brightness and contrast in one image using Image › Adjust › Brightness/Contrast...
    • To avoid intensity clipping one typically sets the contrast on the brightest image (this may depend on your scientific question though…)
    • To find out which image is brighter you can try to use Analyze > Histogram
  • Use the Set button in Image › Adjust › Brightness/Contrast... and check [X] Propagate to all other open images
  • Visualise the current LUT as an inset in both images using Analyze › Tools › Calibration Bar...
    • Export the image using Plugins › BioVoxxel Figure Tools › Export SVG (requires BioVoxxel update site)
      • SVG preserves the rendering of the scale bar at different zoom levels.

ImageJ Macro

run("Close All");
// File › Open...
open("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_calibrated_16bit__nuclear_protein_control.tif");
// [Image › Lookup Tables › Grays ]
run("Grays");
// [ Image › Adjust › Brightness/Contrast... ]
setMinAndMax(8, 3804);
// Analyze › Tools › Calibration Bar...
run("Calibration Bar...", "location=[Upper Left] fill=[Dark Gray] label=White number=5 decimal=0 font=10 zoom=2 overlay");

open("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_calibrated_16bit__nuclear_protein_treated.tif");
run("Grays");
setMinAndMax(8, 3804);
run("Calibration Bar...", "location=[Upper Left] fill=[Dark Gray] label=White number=5 decimal=0 font=10 zoom=2 overlay");
// Window › Tile
run("Tile");

skimage napari

# %% [markdown]
# # Configure LUTs

# %%
# Instantiate the napari viewer
import napari
viewer = napari.Viewer()

# %%
# Read the image
from OpenIJTIFF import open_ij_tiff
image_control, axes_control, scales_control, units_control = open_ij_tiff('https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_calibrated_16bit__nuclear_protein_control.tif')
image_treated, axes_treated, scales_treated, units_treated = open_ij_tiff('https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_calibrated_16bit__nuclear_protein_treated.tif')

# %% 
# View the intensity image as grayscale
viewer.add_image(image_control, name='control', colormap='gray')
viewer.add_image(image_treated, name='treated', colormap='gray')

# %% [markdown]
# **Napari GUI** Show images side by side \
# **Napari GUI** Inspect possible consistent limits for both images

# %% 
# Apply limits to both images
viewer.layers['control'].contrast_limits=(0, 1024)
viewer.layers['treated'].contrast_limits=(0, 1024)






Assessment

Compute how the contrast limits affect the rendered pixel brightness

Read the below section “Explanations: Single color lookup tables” and use the formula that is given there to compute the rendered pixel brightness for the following scenarios:

  1. value = 49, min = 10, max = 50, brightness = ?
  2. value = 100, min = 0, max = 65, brightness = ?
  3. value = 10, min = 20, max = 65, brightness = ?

Solution

  1. 0.975
  2. 1.538 -> 1.0
  3. -0.22 -> 0.0

Fill in the blanks

Fill in the blanks using those words: larger than, smaller than

  1. Pixels with values _____ max will appear saturated.
  2. Pixels with values _____ the min will appear black (using a single color LUT).

Solution

  1. larger than
  2. smaller than

Key points

  • A LUT has configurable contrast limits that determine the pixel value range that is rendered linearly.

  • LUT settings must be responsibly chosen to convey the intended scientific message and not to hide relevant information.

  • A gray scale LUT is usually preferable over a colour LUT, especially blue and red are not well visible for many people.

  • For high dynamic range images multi-color LUTs may be useful to visualise a wider range of pixel values.

Explanations

Lookup tables do the mapping from a numeric pixel value to a color. This is the main mechanism how we visualise microscopy image data. In case of doubt, it is always a good idea to show the mapping as an inset in the image (or next to the image).

Single color lookup tables

Single color lookup tables are typically configured by chosing one color such as, e.g., grey or green, and choosing a min and max value that determine the brightness of this color depending on the value of the respective pixel in the following way:

brightness( value ) = ( value - min ) / ( max - min )

In this formula, 1 corresponds to the maximal brightness and 0 corresponds to the minimal brightness that, e.g., your computer monitor can produce.

Depending on the values of value, min and max it can be that the formula yields values that are less than 0 or larger than 1. This is handled by assinging a brightness of 0 even if the formula yields values < 0 and assigning a brightness of 1 even if the formula yields values larger than 1. In such cases one speaks of “clipping”, because one looses (“clips”) information about the pixel value (see below for an example).

Clipping example

min = 20, max = 100, v1 = 100, v2 = 200

brightness( v1 ) = ( 100 - 20 ) / ( 100 - 20 ) = 1

brightness( v2 ) = ( 200 - 20 ) / ( 100 - 20 ) = 2.25

Both pixel values will be painted with the same brightness as a brightness larger than 1 is not possible (see above).

Multi color lookup tables

As the name suggestes multi color lookup tables map pixel gray values to different colors.

For example:

0 -> black

1 -> green

2 -> blue

3 -> ...

Typical use cases for multi color LUTs are images of a high dynamic range (large differences in gray values) and label mask images (where the pixel values encode object IDs).

Sometimes, also multi color LUTs can be configured in terms of a min and max value. The reason is that multi colors LUTs only have a limited amount of colors, e.g. 256 different colors. For instance, if you have an image that contains a pixel with a value of 300 it is not immediately obvious which color it should get; the min and max settings allow you to configure how to map your larger value range into a limited amount of colors.




Follow-up material

Recommended follow-up modules:

Learn more: