corneretageres.com

Nuclei Detection and Fluorescence Quantification in Python

Written on

In the realm of biological research, particularly in cell biology, cancer studies, and drug discovery, bioimage segmentation and fluorescence quantification are vital techniques. This initial segment will delve into nuclei segmentation within fluorescence microscopy images, including essential steps such as image preprocessing, Gaussian smoothing, thresholding, and advanced techniques to enhance segmentation accuracy.

> For optimal results from this guide, a fundamental understanding of Python and some familiarity with fluorescence microscopy principles, including the use of fluorescent markers, is advantageous.

Nuclei Detection in Fluorescence Microscopy

Loading and Preprocessing the Data

This tutorial will focus on fibroblast cells, where the nuclei are stained with DAPI, a fluorescent dye that binds to DNA, alongside a protein of interest detected in the green channel. Our aim is to accurately segment the nuclei and quantify the fluorescence intensity of the protein specifically within these nuclei, facilitating a focus on nuclear-specific fluorescence, which aids in understanding the localization and expression levels of the target protein.

> In this section, we will demonstrate how to load microscopy images using the scikit-image library and perform necessary preprocessing steps, including channel separation.

Microscopy images typically come in formats like TIFF (or proprietary formats from microscope manufacturers), which can handle multi-channel and multi-dimensional data, such as Z-stacks. We will utilize the io.imread() function from scikit-image to load our image into a NumPy array for subsequent processing.

> The image used in this tutorial was captured with a Leica TCS SP8 MP Multiphoton Microscope.

from skimage import io import numpy as np import matplotlib.pyplot as plt import pandas as pd

# Set option to display all columns pd.set_option('display.max_columns', None) # Set option to display all rows pd.set_option('display.max_rows', None) from skimage import io, filters, morphology, measure, segmentation, color

# Load the multi-channel TIFF image image = io.imread('fibro_nuclei.tif')

# Print the shape of the loaded image to understand its dimensions print("Image shape:", image.shape)

The shape of the image is (17, 2, 1024, 1024), providing a detailed overview of the microscopy image's structure. Each number corresponds to a specific dimension of the image. Here's a breakdown:

#### 1. First Dimension: 17 - Meaning: Represents the number of Z-slices in our Z-stack. - Explanation: In microscopy, especially confocal microscopy, a Z-stack comprises a series of images captured at different focal planes along the Z-axis (depth). Here, 17 slices indicate the image data captures 17 distinct layers through the specimen's depth, which can be combined to form a 3D representation of the sample.

#### 2. Second Dimension: 2 - Meaning: Indicates the number of channels in the image. - Explanation: Microscopy images frequently include multiple channels, with each channel corresponding to different fluorescent markers or stains. In this instance:

  • Channel 1 corresponds to DAPI staining, used for labeling nuclei.
  • Channel 2 corresponds to a fluorescent marker for the target protein.

#### 3. Third and Fourth Dimensions: 1024, 1024 - Meaning: Represent the height and width of each 2D image in pixels. - Explanation: Each Z-slice and channel is represented as a 2D image with a resolution of 1024x1024 pixels. The pixel size dictates the spatial resolution, with more pixels yielding higher resolution and more detailed visual information.

Next, we will separate the channels (DAPI and the green fluorescent channel) to visualize, process, and analyze each independently. The following code extracts the middle Z-slice from our 3D microscopy image stack for both fluorescence channels, specifically selecting the 9th slice (index 8) from each channel. The code then creates a figure with two side-by-side subplots, sharing the x-axis, to display these middle slices, visualized in grayscale.

> Grayscale images are commonly used in scientific imaging, such as microscopy, to represent intensity variations across the image. This is particularly beneficial for detecting objects like nuclei based on their brightness.

# Extract the middle Z-slice for each channel channel_1_middle = image[7, 0, :, :] # Middle slice for GFP channel channel_2_middle = image[7, 1, :, :] # Middle slice for DAPI channel

# Create subplots with shared x-axis fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True, figsize=(10, 10))

# Visualize the middle Z-slice for the first channel (DAPI) ax1.imshow(channel_2_middle, cmap='gray') ax1.set_title('Channel 1: DAPI (Nuclei) - Middle Z-slice') ax1.axis('off') # Hide the axis for better visualization

# Visualize the middle Z-slice for the second channel (GFP) ax2.imshow(channel_1_middle, cmap='gray') ax2.set_title('Channel 2: GFP (Protein of Interest) - Middle Z-slice') ax2.axis('off') # Hide the axis for better visualization

# Adjust layout to prevent overlap plt.tight_layout() # Display the plots plt.show()

Middle Z-slice images from a multi-channel dataset

As shown in the image above, the signal intensity in a single Z-slice is relatively low, complicating accurate segmentation. To enhance clarity and robustness in nuclei segmentation, we will employ a maximum intensity projection (MIP) technique. This method involves collapsing the entire Z-stack into a single 2D image by taking the maximum intensity value of each pixel across all Z-slices, effectively capturing the brightest points from each slice, which provides a clearer representation of the nuclei and facilitates more accurate segmentation.

The following code separates the GFP (protein of interest) and DAPI (nuclei) channels, performing a maximum intensity projection (MIP) on each channel to collapse the Z-stack into a single 2D image by taking the maximum pixel value across all slices. The code then visualizes the MIPs side by side.

# Separate the GFP channel channe1 = image[:, 0, :, :] # GFP channel # Perform Maximum Intensity Projection (MIP) channe1_max_projection = np.max(channe1, axis=0)

# Separate the DAPI channel channe2 = image[:, 1, :, :] # DAPI channel # Perform Maximum Intensity Projection (MIP) channe2_max_projection = np.max(channe2, axis=0)

# Create subplots with shared x-axis fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True, figsize=(10, 10))

# Visualize the maximum intensity projection for the DAPI channel ax1.imshow(channe2_max_projection, cmap='gray') ax1.set_title('Maximum Intensity Projection (DAPI Channel)')

# Visualize the maximum intensity projection for the GFP channel ax2.imshow(channe1_max_projection, cmap='gray') ax2.set_title('Maximum Intensity Projection (Protein of Interest)') ax2.axis('off') # Hide the axis for better visualization

# Adjust layout to prevent overlap plt.tight_layout() # Display the plots plt.show()

Maximum intensity projections of fluorescence channels

The left side of the image below features a zoomed-in section of our image in grayscale. Each square represents a single pixel in this region, with varying shades of gray indicating pixel intensity. The right side displays the corresponding grid of pixel intensity values from the zoomed-in area. Our image is 8-bit, meaning each pixel’s intensity is represented using 8 bits, allowing for 256 different values ranging from 0 (black) to 255 (white).

Pixel intensity values in a zoomed-in region

Nuclei Segmentation

Nuclei segmentation is a critical step in numerous biological image analysis tasks, including cell counting, fluorescence intensity measurement, and cell movement tracking. This section's primary goal is to accurately identify and delineate individual nuclei within our image.

Preprocessing: Prior to segmentation, it is crucial to preprocess the image to enhance segmentation accuracy. Biological images often contain noise that can hinder the segmentation process. To mitigate this, we typically apply Gaussian smoothing, which reduces noise while maintaining essential features, thus enhancing areas of interest like nuclei.

Thresholding: After preprocessing, we apply thresholding to the filtered image. Thresholding converts the image into a binary format, classifying pixels as either foreground (nuclei) or background. In simpler terms, each pixel in a grayscale image, which has an intensity value from 0 (black) to 255 (white), is converted to True (or 1) if its intensity exceeds a specific threshold. This threshold can be set manually or determined automatically via an algorithm, with pixels below the threshold assigned to False (or 0), representing the background.

Otsu’s Method: For this tutorial, we will use Otsu’s Method for thresholding. This widely used automatic technique determines the optimal threshold value by minimizing intra-class variance, which refers to variance within the foreground and background pixel intensities. This effectively separates the nuclei from the background, enhancing the reliability of the segmentation process.

> This approach ensures accurate identification and separation of the nuclei from the surrounding tissue or background, enabling precise measurements and analyses in subsequent workflow stages.

The following code performs image segmentation on the MIP (Maximum Intensity Projection) image by applying Gaussian smoothing with various sigma values to find optimal parameters. It then employs Otsu’s method to calculate a suitable threshold for converting the smoothed image into a binary mask, where pixels are marked as True (foreground) or False (background) based on the threshold.

# Try different sigma values for Gaussian smoothing images_sigma = [] sigma_values = [1, 3, 5]

for sigma in sigma_values:

smoothed_image = filters.gaussian(channe2_max_projection, sigma=sigma)

threshold_value = filters.threshold_otsu(smoothed_image)

binary_mask = smoothed_image > threshold_value

images_sigma.append(binary_mask)

# Create subplots fig, axes = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(15, 5))

for i, (ax, image_sigma) in enumerate(zip(axes, images_sigma)):

ax.imshow(image_sigma[400:800, 400:800], cmap='gray')

ax.set_title(f'Smoothed image with sigma = {sigma_values[i]}')

ax.axis('off') # Hide the axis for better visualization

plt.tight_layout() plt.show()

Impact of Gaussian smoothing on nuclei segmentation

The images above illustrate the effects of Gaussian smoothing with varying sigma values on nuclei segmentation. Higher sigma values in Gaussian smoothing enhance segmentation clarity for this specific image by reducing noise, allowing for clearer delineation of the nuclei. If Gaussian filtering does not yield satisfactory segmentation results, other techniques, such as median filtering or bilateral filtering, may be considered.

The mask produced by the aforementioned code is a binary image highlighting specific areas of interest from our original image. A binary mask contains only two values: True (or 1) and False (or 0).

  • True pixels signify objects of interest that are highlighted or "masked in."
  • False pixels indicate areas that are excluded or "masked out."

The image below showcases the binary mask obtained after thresholding the original image. The left side displays the binary mask for a zoomed-in region, while the right side presents the grid of binary values (True or False) for each pixel in that region, indicating whether they were above or below the threshold during the thresholding process.

Binary mask applied to a zoomed-in region

Improving Segmentation Accuracy

During image segmentation, particularly for nuclei detection, the resulting binary mask may include small, unwanted regions that do not correspond to the actual objects of interest. These regions can encompass debris, noise, or artifacts incorrectly identified as part of the objects (e.g., nuclei). To refine segmentation and ensure only relevant objects are retained, we can utilize the remove_small_objects function from the skimage.morphology module.

The remove_small_objects function is specifically designed to filter out small, irrelevant components from a binary mask. Connected components in a binary mask are clusters of adjacent True pixels representing the objects of interest, such as nuclei. Each connected component is treated as a distinct object. This function iterates through each connected component, counting the number of pixels it contains. If a component has fewer pixels than the specified min_size, it is deemed too small and removed from the binary mask.

For instance, the following line of code:

cleaned_mask = morphology.remove_small_objects(binary_mask, min_size=100)

will eliminate any objects with fewer than 100 pixels from the binary mask. It's important to note that the choice of 100 as the threshold is somewhat arbitrary and should be tailored based on the specific characteristics of the image and the objects being segmented. This threshold should be adjusted to align with the expected size of the objects of interest, ensuring that smaller noise elements are removed without discarding relevant structures.

In this tutorial, the nuclei in our image are well-separated, simplifying segmentation. However, if the nuclei are overlapping, additional steps are required. In such scenarios, algorithms like the watershed algorithm can be employed to separate touching nuclei. The watershed algorithm interprets the intensity image as a topographic surface and "floods" the regions from identified markers. We will explore this technique in further detail in the next tutorial.

Conclusion

In this tutorial, we covered the fundamental steps for performing nuclei segmentation in fluorescence microscopy images. We started with preprocessing the image to enhance data quality, applied Gaussian smoothing with different sigma values to reduce noise, and employed thresholding techniques to differentiate the nuclei from the background. Additionally, we discussed post-processing steps, like removing small objects, to refine segmentation results. By experimenting with various parameters and techniques, we have acquired tools to optimize and adapt the segmentation process for diverse biological images, ensuring more accurate and reliable outcomes in our analyses.

In the next tutorial, we will measure the fluorescence intensity of the GFP channel, collect the data, and analyze it with the Pandas library to derive meaningful insights.

References

[1] P. Bankhead, “Introduction to Bioimage Analysis — Introduction to Bioimage Analysis.” https://bioimagebook.github.io/index.html (accessed Jun. 29, 2023).

In Plain English ?

Thank you for being part of the **In Plain English* community! Before you go:*

  • Be sure to clap and follow the writer! ?
  • Follow us: X | LinkedIn | YouTube | Discord | Newsletter
  • Visit our other platforms: CoFeed | Differ
  • More content at PlainEnglish.io