Thresholding

Prerequisites

Before starting this lesson, you should be familiar with:

Learning Objectives

After completing this lesson, learners should be able to:
  • Describe the relationship between an intensity image and a derived binary image

  • Apply a threshold to segment an image into foreground and background regions

Motivation

One strategy to detect objects or specific regions in images is to first distinguish so-called background pixels, which do not contain objects or interesting regions from foreground pixels, which mark the areas of interest. This process is called two class semantic segmentation and is often referred to as image binarization. The foreground regions can then be further processed, e.g. to detect objects or perform intensity measurements.

Concept map

graph TD I("Image") --> T("Threshold") T --> BI("Binary image / Binary mask") BI --- BG("Background pixels (false, 0)") BI --- FG("Foreground pixels (true, 1, 255)")



Figure


Image before and after applying a threshold of 73 gray values.



Activities

Threshold bright and dim cells


Show activity for:  

ImageJ GUI

  • Find and apply a threshold
    • Open xy_8bit__two_cells.tif
    • Set binary options: [Process > Binary > Options ..]
      • Black background
    • Inspect pixel values to find a threshold separating fore- and background
      • Hover over the image and observe the pixel values in ImageJ status bar
      • Draw line profile and [Analyze > Plot Profile ] or [Ctrl-K]
      • Inspect histogram using [Analyze > Histogram] or [Ctrl-H]
        • If you do this for the whole image the background peak dominates the histogram
        • Draw a ROI on the image such that there is 50/50 foreground and background
    • Interactively find and apply a threshold
      • [Image > Adjust > Threshold…] or [Ctrl-Shift-T]
        • Dark Background
        • Upper slider is lower threshold level, which is the value that you observed in the aforementioned step that would separate foreground and background
        • Lower slide is the upper threshold level. This can be set to the maximum value of the bit depth (in this case 255)
        • Press Apply to create a binary image
    • Appreciate that the binary image only has two values: 0 and 255
    • Repeat the operation so that only the brightest cell remains visible

ImageJ Macro

// Parameters
threshold1 = 49;
threshold2 = 100;

run("Close All");

// Code
open("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__two_cells.tif");
rename("input");

// apply threshold 1
selectWindow("input");
run("Duplicate...", "title=threshold1");
setThreshold(threshold1, 65535);
setOption("BlackBackground", true);
run("Convert to Mask");

// apply threshold 2
selectWindow("input");
run("Duplicate...", "title=threshold2");
setThreshold(threshold2, 65535);
setOption("BlackBackground", true);
run("Convert to Mask");

ImageJ Jython

from ij import IJ, ImagePlus
from ij.plugin import Thresholder

inputImage = IJ.openImage("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__two_cells.tif")
IJ.setRawThreshold(inputImage, 44, 255, None)
binaryImage = ImagePlus('Binary image 2 nuclei', Thresholder.createMask(inputImage))
binaryImage.show()
IJ.setRawThreshold(inputImage, 88, 255, None)
binaryImage_1 = ImagePlus('Binary image 1 nuclei', Thresholder.createMask(inputImage))
binaryImage_1.show()

skimage napari

# %% 
# Thresholding bright and dim cells

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

# %%
# Load the image
image, *_ = open_ij_tiff('https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__two_cells.tif')

# %%
# Check the datatype and view the image
print(image.dtype)
viewer.add_image(image)

# %%
# Napari: Inspect the pixel values to identify a threshold that segments both cells 

# %%
# Inspect the image histogram to confirm the above threshold
import matplotlib.pyplot as plt
import numpy as np
plt.hist(image.flatten(), bins=np.arange(image.min(), image.max() + 1)); 
plt.yscale('log') # the background peak is so dominat that without the log scale it is hard to see the threshold

# %%
# Threshold the image and inspect the resulting values and data type
binary_image_two_cells = image > 49
import numpy as np
print(np.unique(binary_image_two_cells))
print(binary_image_two_cells.dtype)

# %%
# Overlay the binary image
viewer.add_image(binary_image_two_cells, opacity=0.8)

# %%
# Apply a higher threshold
# to only select the brighter cell
# and also add this to the viewer
binary_image_one_cell = image > 100
viewer.add_image(binary_image_one_cell, opacity=0.8)



Apply various thresholds to one image


Show activity for:  

ImageJ GUI

  • [File > Open…] xy_8bit__PCNA.tif
  • Use [Analyze > Plot Profile ]/[Ctrl-K] to identify possible threshold values
  • Use [Image > Adjust > Threshold…] to create binary masks. Suggestion: Duplicate the image first , do not forget to press Apply

Example solutions:

  1. Lower threshold level ~ 5
  2. Lower threshold level ~ 44
  3. Lower threshold level ~ 4 and Upper threshold level ~ 4-5

ImageJ Macro

open("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__PCNA.tif")
selectWindow("xy_8bit__PCNA.tif");
run("Duplicate...", "title=[2 nuclei]");
selectWindow("xy_8bit__PCNA.tif");
run("Duplicate...", "title=[boundary]");
selectWindow("xy_8bit__PCNA.tif");
run("Duplicate...", "title=[dots]");
selectWindow("2 nuclei");
setThreshold(5, 255);
setOption("BlackBackground", true);
run("Convert to Mask");
selectWindow("boundary");
setThreshold(4, 4);
setOption("BlackBackground", true);
run("Convert to Mask");
selectWindow("dots");
setThreshold(44, 255);
setOption("BlackBackground", true);
run("Convert to Mask");

ImageJ Jython

from ij import IJ, ImagePlus
from ij.plugin import Thresholder
# image is xy_8bit_PCNA.tif
inputImage = IJ.openImage("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__PCNA.tif")
IJ.setRawThreshold(inputImage, 5, 255, None)
binaryImage = ImagePlus('Binary image 2 nuclei', Thresholder.createMask(inputImage))
binaryImage.show()
IJ.setRawThreshold(inputImage, 4, 4, None)
binaryImage_boundary = ImagePlus('Binary boundary', Thresholder.createMask(inputImage))
binaryImage_boundary.show()
IJ.setRawThreshold(inputImage, 44, 255, None)
binaryImage_brightdots = ImagePlus('Binary bright dots', Thresholder.createMask(inputImage))
binaryImage_brightdots.show()

ImageJ Jython + input parameters

#@ Integer (label="Lower threshold") thr1
#@ Integer (label="Upper threshold") thr2

from ij import IJ, ImagePlus
from ij.plugin import Thresholder
nputImage = IJ.openImage("https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__PCNA.tif")
IJ.setRawThreshold(inputImage, thr1, thr2, None)
binaryImage = ImagePlus('Thresholded image', Thresholder.createMask(inputImage))
binaryImage.show()

skimage napari

# %% 
# Nuclei, spots and nuclei boundary segmentation

# %%
# Import libraries and instantiate the viewer
import napari
import numpy as np 
import matplotlib.pyplot as plt
viewer = napari.Viewer()

# %%
# Read the intensity image
from OpenIJTIFF import open_ij_tiff
image, *_ = open_ij_tiff('https://github.com/NEUBIAS/training-resources/raw/master/image_data/xy_8bit__PCNA.tif')

# %%
# Check the image's data type and 
# view the image
print(image.dtype)
viewer.add_image(image)

# %% 
# Napari: Inspect the pixel values in order to identify a threshold 

# %%
# Also check the image histogram for a threshold
# Observe that interestingly there are several local minima in the histogram
plt.hist(image.flatten(), bins=np.arange(image.min(), image.max() + 1));
plt.yscale('log')

# %%
# Threshold both nuclei
# check the resulting datatype and content 
# and view the binary image
binary_image_two_nuclei = image > 5
print(binary_image_two_nuclei.dtype)
print(np.unique(binary_image_two_nuclei))
viewer.add_image(binary_image_two_nuclei, opacity=0.8)

# %%
# Apply a higher threshold
# to only segment the brighter nucleus
binary_image_one_nucleus = image > 15
viewer.add_image(binary_image_one_nucleus, opacity=0.8)

# %%
# Apply an even higher threshold
# to only select the intranuclear speckles 
binary_image_speckles = image > 44
viewer.add_image(binary_image_speckles, opacity=0.8)

# %%
# Apply two thresholds (aka "gating") 
# to only select the boundary of cells
binary_image_boundary = (image < 5) & (image >= 4)
viewer.add_image(binary_image_boundary, opacity=0.8)



Threshold based on noise measurement

Here we set the threshold value t such that it is higher than the background intensity plus some noise level.


Show activity for:  

ImageJ GUI

  • Open xy_8bit__two_cells.tif
  • Draw a squared ROI
  • [Analyze > Measure] or [Ctrl-M}
  • Eventually add [x] Mean gray value and [x] Standard deviation to your result table, [Results > Set Measurements]
  • Note the value of Mean and StdDev and compute their sum
  • Choose a multiple of this sum for the threshold
  • Test your choice [Image > Adjust > Threshold…]
  • Press Apply to create a binary image






Assessment

Fill in the blanks

Solution

  • Pixels in a binary image can have maximally 2 different values.
  • If the threshold is larger than the maximal pixel value in the intensity image, all pixels in the binary image have a value of 0.

True or False

Solution

  • There is only one correct threshold value in order to convert an intensity image into a binary image. False
  • Binary images are always unsigned 8-bit where the foreground is 255. False

Explanations

A common algorithm for binarization is thresholding. A threshold value t is chosen, either manually or automatically, and all pixels with intensities below t are set to 0, whereas pixels with intensities >= t are set to the value for the foreground. Depending on the software the foreground value can be different (e.g. 1 in MATLAB or 255 in ImageJ). At any pixel (x,y):

p_im(x,y) < t -> p_bin(x,y) = 0

p_im(x,y) >= t -> p_bin(x,y) = 1

where, p_im and p_bin are the intensity and binary images respectively.

It is also possible to define an interval of threshold values, i.e. a lower and upper threshold value. Pixels with intensity values within this interval belong to the foreground and vice versa.




Follow-up material

Recommended follow-up modules:

Learn more: