Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Image processing, pt 1

Martijn Wehrens, Bram van den Broek (version: 0.9)

Libraries

Libraries are packages of code that people have written, and can be loaded.

We’ll be using code for plotting, importing files, and image analysis.

Segmentation and thresholding

A computer doesn’t “know” what biological features are, unless we explicitly program it. For example, if we want to analyze cells, we’ll need to be able to create a computer program that first of all knows which of the pixel of an image belong to a (single) cell.

Creating such regions of interest (ROIs) in an image is called segmentation.

Case 1: segmenting an image with bimodal intensity profile

<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>
<Figure size 393.701x196.85 with 2 Axes>

The intensity distribution of this image is called “bimodal”, since it has two peaks.

Intermezzo: using functions
<Figure size 590.551x196.85 with 2 Axes>
7

Source materials: img_happycell is taken from biobook (fig. 68), Creative Commons Attribution 4.0 International License.

The segmentation

One algorithm that works well on bimodal distributions is Otsu’s method. It will try to find a threshold that minimizes the variance in the intensities that respectively fall above and below that threshold. (For more info, see biobook.)

<Figure size 590.551x196.85 with 2 Axes>
array([[False, False, False, ..., True, True, True], [False, False, False, ..., True, True, True], [False, False, False, ..., True, True, True], ..., [ True, True, True, ..., True, True, True], [ True, True, True, ..., True, True, True], [ True, True, True, ..., True, True, True]], shape=(50, 50))
<Figure size 640x480 with 1 Axes>

Case 2: segmenting an image with a dominant background

<Figure size 590.551x196.85 with 2 Axes>

As can be seen, this image doesn’t have a bimodal distribution, intead, it has a very strong background peak, and a relatively small number of pixels with higher intensity.

In this image, we’ll aim to identify pixels that are above the dominant background peak.

Methodology

A method that’s suitable for the “dominant background” case is the “triangle method”, which was published already by Zack et al. in 1977.

The triangle method.

In the figure above (from Zack et al.), the method is explained. A line is drawn in the histogram between xmode,y(xmode)x_\text{mode}, y(x_\text{mode}) and xmax,0x_\text{max}, 0. Then, a second line is drawn, perpendicular to the first line, which maximizes the distance to the histogram line. The x-value where that second line touches the histogram, will be the threshold. (Originally, a margin of 0.2 was included, but modern implementations leave that out.)

np.int64(121)
<Figure size 590.551x196.85 with 2 Axes>
<Figure size 640x480 with 1 Axes>
More thresholding-finding algorithms

More thresholding-finding algorithms exits. Threshold methods included in skimage.filters are:

threshold_isodata()
threshold_li()
threshold_mean()
threshold_minimum()
threshold_multiotsu()
threshold_niblack()
threshold_otsu()
threshold_sauvola()
threshold_triangle()
threshold_yen()

Local thresholding

What we’ve seen so far are single threshold values that we apply to the whole image, ie “globally”. These are therefor global thresholds.

Let’s look at an example where this goes wrong.

(<Figure size 590.551x196.85 with 2 Axes>, array([<Axes: xlabel='pixels', ylabel='pixels'>, <Axes: xlabel='Intensity', ylabel='Counts'>], dtype=object))
<Figure size 590.551x196.85 with 2 Axes>
<Figure size 590.551x196.85 with 2 Axes>
<Figure size 640x480 with 1 Axes>

However, if we inspect parts of the image separately, we might be able to find thresholds that can be applied locally that do separate back- and foreground.

(191, 384)
(191, 384)
[[130.77211822 130.90207039 131.08406619 131.3037651  131.54411986
  131.78670404 132.01305378 132.20621863 132.35203808]
 [131.05212856 131.19449961 131.39410716 131.63543722 131.89996679
  132.16758333 132.41804769 132.63270539 132.7958992 ]
 [131.46965599 131.6316074  131.85895878 132.13432677 132.43684833
  132.74375233 133.03201741 133.2803335  133.47072804]
 [132.02108962 132.21072415 132.47726401 132.80065482 133.15670758
  133.51889678 133.86029997 134.15590792 134.38455086]
 [132.70090606 132.92728526 133.24579583 133.63280466 134.05969015
  134.49495656 134.90655574 135.2646515  135.54394112]
 [133.50142723 133.77439631 134.15875392 134.62629676 135.14277097
  135.67040636 136.17073221 136.60790608 136.9515313 ]
 [134.41221196 134.74198931 135.2065921  135.77222055 136.39775503
  137.03782359 137.64623024 138.17995689 138.60257775]
 [135.41969318 135.81625928 136.37517213 137.05604269 137.80970077
  138.58191197 139.31753654 139.96530408 140.4819182 ]
 [136.50676645 136.97913384 137.64506655 138.45670846 139.3557997
  140.27815383 141.15865687 141.93689572 142.5620314 ]]
<Figure size 640x480 with 1 Axes>

Exercise: thresholding bacteria

Can you apply some of the above methods to the image loaded below? We’ll discuss in 5-10 minutes.

<Figure size 590.551x196.85 with 2 Axes>

More advanced image processing

For the example above, and many biological images, features are not straightforward to extract. We need more advanced methods than simple thresholding.

We’ll introduce you to some advanced image processing methods, and apply them later to biological examples.

Convolution

Convolutational image operations are at the heart of many image processing steps. You’ll also see them a load in machine learning.

Image source: via discuss.pytorch.org.

In convolution, a new image of equal size is created. The procedure goes over each pixel (i,j)(i, j) in the original image, and sums its neighborhood pixels multiplied with neighborhood location specific weights, to get the intensity value for the new pixel at location (i,j)(i ,j).

This allows you to manipulate the image in very useful ways.

Examples (1)

<Figure size 640x480 with 2 Axes>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 2 Axes>

Do you understand why the results looks like it does? Both how the shapes have changed, and also how the intensities have changed?

Examples (2)

<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>

The seismic colormap, when centered around 0, shows positive values in red, and negative values in blue.

The first image looks blurred, and in the second image edge structures remain.

Do you understand why the 1st image is blurred? Why edges remain in the second image are a bit more complicated. Can you take a guess as to why this is?

The laplacian

We’ll skip the math here, but the “laplacian” kernel equals a pixel’s 2nd derivative. The image below illustrates why that helps to detect sudden intensity changes.

The left panel shows an intensity change in one dimension, here yy would be the intensity and xx the location along the image. The middle panel shows that the derivative peaks at the “edge” (note that the peak would be negative if the change goes in the other direction). The right panel shows that around the edge, the double derivative first shows a positive peak and then a negative peak.

Thus, in the second derivative image, pixels that are near-zero and which are in the vicinity of both a negative and positive value, are close to an edge.

Technical note

Both the derivative and laplacian kernel can be used to find edges. The advantage of the Laplacian is that it better identifies the exact location of the edge, as the zero-crossing (pixels with values close to zero) are precisely where the edge is. First-order derivatives can also be used, but they are dimension-specific (either the derivative in the row-direction, or the derivative in the column-direction), so the output of two convolutions needs to be combined. Additionally, both can be sensitive to noise, why they are often combined with a de-noising convolution (e.g. blur), as we will see below.

Dilation and erosion

As we’ll see, it is often convenient to manipulate binary masks.

Morphological operations, similar to the convolution, also construct a new picture on a pixel-by-pixel basis. They look at properties of the set of neighborhood intensity values.

The main operations are:

  • Dilation, which takes the neighborhood maximum.

  • Erosion, which takes the neighborhood minimum.

<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>

Combining operations

These operations can also be applied to grayscale images.

There exist multiple combinations of morphological operations that are often used together, such as:

  • Opening (erode → dilate): removes small features, keeps coarse features.

  • Closing (dilate → erode): fills small holes/gaps.

  • Top-hat (image − opening): isolates small bright features (background-subtraction, spot-detection)

  • Morphological gradient (dilation − erosion): edges.

Exercise: Apply opening and closing

  • Apply sk.morphology.closing and sk.morphology.opening to the “pikachu” image. Do you understand the result?

  • Use the argument footprint=sk.morphology.disk(7). Does this change the outcome? Why?

We’ll discuss in 5-10 minutes.

Built-in filtering and edge detection

With the above tools, we could make custom functions that perform edge detection or blurring.

There are however also some optimized built-in functions that perform these tasks.

<Figure size 640x480 with 1 Axes>
<Figure size 640x480 with 1 Axes>

Edge detection methods

There exist multiple built-in edge detection methods, common ones include: sk.filters.sobel(), sk.filters.prewitt(), sk.filters.laplace(), sk.feature.canny(). See also the documentation.

<Figure size 640x480 with 1 Axes>

Neighborhood-based filtering

Neighborhood-based filtering methods include ndimage.minimum_filter(), ndimage.maximum_filter, ndimage.median_filter().

The median filter is for example relevant to remove noise from images.

(<Figure size 590.551x196.85 with 2 Axes>, array([<Axes: xlabel='pixels', ylabel='pixels'>, <Axes: xlabel='Intensity', ylabel='Counts'>], dtype=object))
<Figure size 590.551x196.85 with 2 Axes>
<Figure size 590.551x196.85 with 2 Axes>

Exercise: thresholding

  • Can you create a binary mask for the points in img_noisy?

  • Can you create a binary mask that contains the edges of img_nuclei?

Optional help: hints for the nuclear edges

One solution involves using a combination of erosion(), the original nuclei_mask, and combining different masks using np.logical_..() (documentation).

If you want a polished result, perhaps you can also throw in an opening() operation.

References
  1. Zack, G. W., Rogers, W. E., & Latt, S. A. (1977). Automatic measurement of sister chromatid exchange frequency. Journal of Histochemistry & Cytochemistry, 25(7), 741–753. 10.1177/25.7.70454