"""
Image Processing Module for PyCAT
This module contains functions for image processing tasks, including image adjustments, enhancements, and filters.
Most functions are decomposed into a function which actually performs the processing and a function which interacts
with the Napari viewer. This separation allows for easier testing and debugging of the processing functions. It also
allows future users to use the processing functions without the Napari viewer if needed, or to add the functions to
Napari as plugins, providing flexibility and reusability.
Author
------
Christian Neureuter, GitHub: https://github.com/cneureuter
Date
----
4-20-2024
"""
# Standard library imports
import math
import warnings
# Third party imports
import numpy as np
import skimage as sk
import napari
from napari.utils.notifications import show_warning as napari_show_warning
import scipy.ndimage as ndi
from scipy.interpolate import RectBivariateSpline
from pywt import wavedecn, waverecn
import SimpleITK as sitk
# Local application imports
from pycat.utils.general_utils import dtype_conversion_func, get_default_intensity_range
from pycat.ui.ui_utils import add_image_with_default_colormap
# Image adjustments #
[docs]
def apply_rescale_intensity(image, out_min=None, out_max=None):
"""
Rescales the intensity of an image to a specified range, adjusting its pixel values accordingly.
This function modifies the intensity values of the image so that the output image's pixel intensities
are scaled between `out_min` and `out_max`. This is useful for enhancing image contrast or normalizing
image data.
Parameters
----------
image : numpy.ndarray
The input image whose intensities are to be rescaled.
out_min : float, optional
The minimum intensity value for the output image. If not provided, defaults to the minimum value
supported by the input image's data type.
out_max : float, optional
The maximum intensity value for the output image. If not provided, defaults to the maximum value
supported by the input image's data type.
Returns
-------
rescaled_image : numpy.ndarray
The image with rescaled intensity values.
Notes
-----
It is crucial not to set `out_min` and `out_max` to the same value to avoid a division by zero error.
Also, ensure that the output range does not exceed the input image's data type range; for example,
scaling an 8-bit image to values outside 0-255 will lead to clipping and potential data loss.
"""
input_dtype = str(image.dtype) # Store the input image data type
# Get the default intensity range for the input image data type
default_min, default_max = get_default_intensity_range(input_dtype)
# Ensure the output range is within the input dtype range
out_min = int(default_min) if out_min is None or (out_min < default_min) else out_min
out_max = int(default_max) if out_max is None or (out_max > default_max) else out_max
# Ensure the output range does not have the same minimum and maximum values
if out_min == out_max:
napari_show_warning("The output range cannot have the same minimum and maximum values.")
# Rescale the intensity of the image to the specified range
out_range = (out_min, out_max)
rescaled_image = sk.exposure.rescale_intensity(image, out_range=out_range).astype(image.dtype)
# Ensure the image is the same dtype as the input
rescaled_image = dtype_conversion_func(rescaled_image, input_dtype)
return rescaled_image
[docs]
def run_apply_rescale_intensity(out_min_input, out_max_input, viewer):
"""
Applies intensity rescaling to the currently active image layer in a Napari viewer based on user-specified
minimum and maximum output intensity values.
This function interacts with a Napari viewer, allowing users to rescale the intensity of the selected image
layer directly through UI elements for specifying the intensity range. The adjusted image is then displayed
in the viewer.
Parameters
----------
out_min_input : UI Element
A UI element for user input of the minimum output intensity value. Typically a text input field.
out_max_input : UI Element
A UI element for user input of the maximum output intensity value. Typically a text input field.
viewer : napari.Viewer
The Napari viewer instance where the image layers are managed and displayed.
Raises
------
Error
If no active image layer is selected or if the active layer is not compatible as an image layer for processing.
Notes
-----
It is assumed that `out_min_input` and `out_max_input` are components of a graphical user interface and can retrieve
textual input from the user, which is then converted to floating point values for the rescaling function.
Error handling for digit inputs should be added. For example:
.. code-block:: python
# Ensure the input is valid and convert to an integer
if new_label_input.text() == "" or not new_label_input.text().isdigit():
print("Please enter a valid label value.")
return
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
napari_show_warning("No active image layer selected.")
return
# Use the user provided values if available, otherwise use the defaults
out_min = float(out_min_input.text()) if out_min_input.text() else None
out_max = float(out_max_input.text()) if out_max_input.text() else None
# Apply the rescale intensity function to the image
rescaled_image = apply_rescale_intensity(image, out_min, out_max)
# Add the rescaled image to the viewer
add_image_with_default_colormap(rescaled_image, viewer, name=f"Intensity Rescaled {active_layer.name}")
[docs]
def invert_image(image):
"""
Inverts the intensity of an image, mapping dark regions to light and vice versa, suitable for different data types.
This function applies an inversion transformation where each pixel's intensity is subtracted from the maximum
possible value for its data type, effectively reversing its brightness. This operation is tailored for different
data types to maintain the integrity of the image's contrast and appearance.
Parameters
----------
image : numpy.ndarray
The input image to be processed. The image can be of any data type that is supported by the function.
Returns
-------
inverted_image : numpy.ndarray
The intensity-inverted image, matching the input image's data type and dimensions.
Notes
-----
The inversion logic varies by data type:
- Unsigned integers: Subtract pixel values from their maximum possible value.
- Signed integers: Subtract pixel values from -1, flipping their sign.
- Floats (0 to 1 range): Subtract pixel values from 1.
This function assumes the input image's pixel values are appropriately scaled for their data type.
"""
inverted_image = sk.util.invert(image)
return inverted_image
[docs]
def run_invert_image(viewer):
"""
Inverts the intensity of the currently active image layer in the Napari viewer using PyQt GUI components.
This function retrieves the currently selected image layer from the Napari viewer, inverts its intensity,
and displays the result as a new layer in the viewer. This is particularly useful for enhancing visual
contrast or highlighting specific features in biophysical imaging data.
Parameters
----------
viewer : napari.Viewer
The Napari viewer instance, used for displaying and processing the image layers. The viewer is expected
to be part of a PyQt application, integrating seamlessly with other GUI components.
Raises
------
Error
If no active image layer is selected, or if the selected layer is not suitable for processing (e.g., not an image layer).
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
raise ValueError("No active image layer selected.")
# Apply the invert image function to the rescaled LoG filtered image
inverted_image = invert_image(image)
# Add the inverted image to the viewer
add_image_with_default_colormap(inverted_image, viewer, name=f"Inverted {active_layer.name}")
[docs]
def upscale_image_interp(image, num_row_initial, num_col_initial, upscale_factor=2, pad=False):
"""
Upscales an image using bicubic interpolation to enhance its resolution. This function increases the density
of image pixels based on the given upscale factor, applying bicubic spline interpolation to estimate the pixel
values at new grid points.
Parameters
----------
image : numpy.ndarray
The input image array to be upscaled.
num_row_initial : int
The number of rows in the input image.
num_col_initial : int
The number of columns in the input image.
upscale_factor : int, optional
The factor by which the image resolution is to be increased, doubling the dimensions by default.
pad : bool, optional
If True, retains a constant border padding of 10 pixels to mitigate edge artifacts. Default is False,
which removes the padding from the final output.
Returns
-------
magnified_image : numpy.ndarray
The upscaled image with increased resolution. If padding is not removed, the returned image includes a
10-pixel border around the edges.
Note
----
The function pads the upscaled image with a constant border of 10 pixels to mitigate edge artifacts, unless
specified otherwise by the `pad` parameter. This is important for applications where edge continuity is critical.
"""
# Define the initial and final grids for interpolation
x0 = np.linspace(-0.5, 0.5, num_col_initial)
y0 = np.linspace(-0.5, 0.5, num_row_initial)
X0, Y0 = np.meshgrid(x0, y0)
x = np.linspace(-0.5, 0.5, int(np.round(upscale_factor * num_col_initial)))
y = np.linspace(-0.5, 0.5, int(np.round(upscale_factor * num_row_initial)))
X, Y = np.meshgrid(x, y)
# Perform bicubic interpolation
interp_func = RectBivariateSpline(y0, x0, image)
magnified_image = interp_func(y, x)
# Clean up by setting negative values to zero and padding the image
magnified_image[magnified_image < 0] = 0
magnified_image = np.pad(magnified_image, 10, mode='constant')
return magnified_image[10:-10, 10:-10] if not pad else magnified_image # Remove the padding if not requested
[docs]
def run_upscaling_func(data_checkbox, data_instance, viewer):
"""
Applies the upscale_image_interp function to selected image layers within a viewer interface, potentially
updating related data in the data_instance based on the upscaling process and user input. It iterates over
selected image layers, applies upscaling, and optionally updates related data in the data_instance. The result
is added back to the viewer as a new image layer.
Parameters
----------
viewer : napari.Viewer
The viewer object that contains the image layers to be upscaled.
data_instance : DataInstance
An object containing data and parameters that may need updating based on the image upscaling (e.g., object
sizes, resolution information).
data_checkbox : Checkbox
A user interface element indicating whether to update certain data in the data_instance based on the upscaling
results.
Notes
-----
The function ensures that upscaling does not apply to images already at or above a specific size threshold (2048x2048).
It also corrects data types and ranges for upscaled images to ensure they are suitable for further processing or
display within the viewer. This function interacts directly with the Napari viewer's GUI elements, facilitating
a seamless user experience in adjusting image properties.
"""
# Get the currently selected layers from the viewer
selected_layers = viewer.layers.selection
# Check if no layers are selected and exit the function if true
if not selected_layers:
napari_show_warning("No layers selected")
return
# Determine whether the user has requested data updates based on the checkbox state
update_data = data_checkbox.isChecked()
# Loop through a copy of the selected layers list to prevent modification during iteration
for layer in list(selected_layers):
# Skip the iteration if the layer is None for some reason
if layer is None:
continue
# Copy the layer data to prevent modifying the original data directly
image = layer.data.copy()
# Retrieve the initial dimensions of the image
num_row_initial, num_col_initial = image.shape
# Decide not to upscale images already at or above a certain size threshold
if num_row_initial >= 2048 or num_col_initial >= 2048:
upscale_factor = 1 # Set the upscale factor to 1, effectively making no change
update_data = False # Prevent updates to data_instance for large images
napari_show_warning("Max resolution is 2048 x 2048. Micron sizes have not been updated.")
continue
else:
upscale_factor = 2
# Apply the upscaling function to the image
upscaled_img = upscale_image_interp(image, num_row_initial, num_col_initial, upscale_factor=upscale_factor)
# Prepare the upscaled image data for safe use in the viewer
data = upscaled_img.copy()
# Ensure integer data types are within a valid range, correcting them if necessary
if np.issubdtype(data.dtype, np.integer):
if np.min(data) < 0 or np.max(data) > 2**16 - 1:
# Give 5% of the range as a buffer which is clipped rather than rescaled
if np.min(data) < (0 - (0.05 * 2**16)) or np.max(data) > (2**16 + (0.05 * 2**16)):
data = np.clip(data, 0, 2**16 - 1).astype(np.uint16)
else:
data = apply_rescale_intensity(data, out_min=0, out_max=2**16 - 1).astype(np.uint16)
# Ensure floating-point data types are within a valid range, correcting them if necessary
elif np.issubdtype(data.dtype, np.floating):
if np.min(data) < 0 or np.max(data) > 1.0:
# Give 5% of the range as a buffer which is clipped rather than rescaled
if np.min(data) < (0 - (0.05)) or np.max(data) > (1.0 + (0.05)):
data = np.clip(data, 0.0, 1.0).astype(np.float32)
else:
data = apply_rescale_intensity(data, out_min=0.0, out_max=1.0).astype(np.float32)
else:
data = apply_rescale_intensity(data, out_min=0.0, out_max=1.0).astype(np.float32)
upscaled_img = data.copy()
# Update relevant data in the data_instance for the first layer processed only if requested
if update_data:
# Calculate new dimensions and scale factors based on the upscaling
scale_factor_width, scale_factor_height = upscaled_img.shape[0] / image.shape[0], upscaled_img.shape[1] / image.shape[1]
square_scale_factor = scale_factor_width * scale_factor_height
linear_scale_factor = np.sqrt(square_scale_factor)
# Update object sizes, ball radius, and cell diameter in the data_instance based on scale factors
data_instance.data_repository['object_size'] *= linear_scale_factor
data_instance.data_repository['cell_diameter'] *= linear_scale_factor
data_instance.data_repository['ball_radius'] *= linear_scale_factor
# Update the microns per pixel squared resolution in the data_instance
data_instance.data_repository['microns_per_pixel_sq'] /= square_scale_factor
# After updating, ensure no further updates are made for subsequent layers
update_data = False
# Add the processed and potentially upscaled image back into the viewer
add_image_with_default_colormap(upscaled_img, viewer, name=f"Upscaled {layer.name}")
# Enhancements and Filters #
[docs]
def gabor_filter_func(image):
"""
Applies a Gabor filter to an image to enhance texture and feature visibility at specific orientations. This function
utilizes a bank of Gabor filters at four distinct angles (0, 45, 90, and 135 degrees), which helps in capturing edge and
texture information effectively. The results from these orientations are summed to create a composite image that
emphasizes variations in pixel intensity related to the filter orientations, thereby enhancing the visibility of
features aligned with these angles.
Parameters
----------
image : numpy.ndarray
A 2D array representing the input image. The image can be of any unsigned data type.
Returns
-------
numpy.ndarray
A 2D numpy array of the enhanced image. This output emphasizes the texture and edge features present in the
original image at the specified filter orientations. The output image is converted back to the original image
data type, ensuring compatibility with further processing or visualization steps.
Notes
-----
The function processes the image using a float32 intermediate data type for filtering operations to ensure accuracy
while maintaining performance. The output is then rescaled to emphasize feature variations and converted back to the
original image data type.
"""
input_dtype = str(image.dtype) # Store the input image's data type for later conversion back
img = dtype_conversion_func(image, 'float32') # Convert the image to float32 for processing
# Initialize a list to store the filtered images
filtered_images = []
# Define the angles (in radians) for the Gabor filters
theta_list = [theta / 4.0 * np.pi for theta in range(4)]
# Apply the Gabor filter at each angle
for theta in theta_list:
# Generate the Gabor kernel for the current angle
kernel = np.abs(sk.filters.gabor_kernel(frequency=1.0, theta=theta, bandwidth=1.0))
# Filter the image with the generated kernel
filtered_image = ndi.convolve(img, kernel, mode='constant')
# Collect the filtered images
filtered_images.append(filtered_image)
# Sum the results of the filtering to enhance edges and textures
filtered_sum = np.sum(filtered_images, axis=0)
# Rescale the sum of filtered images to adjust the intensity range
rescaled_sum = apply_rescale_intensity(filtered_sum, out_min=0.75, out_max=1.0).astype(np.float32)
# Multiply the original image by the rescaled sum to emphasize enhanced features
enhanced_image = rescaled_sum * img
# Convert the enhanced image back to its original data type
enhanced_image = dtype_conversion_func(enhanced_image, output_bit_depth=input_dtype)
return enhanced_image
[docs]
def peak_and_edge_enhancement_func(image, ball_radius):
"""
Enhances the edges and peaks of features within an image through a sequence of image processing operations.
This includes Gaussian background division, application of a Gabor filter, morphological operations, and adaptive
histogram equalization to improve contrast.
Parameters
----------
image : numpy.ndarray
The input image to be enhanced, which can be of any unsigned integer data type.
ball_radius : int
Determines the size of the Gaussian filter used for initial smoothing, indirectly affecting the scale of
features targeted for enhancement.
Returns
-------
output_image : numpy.ndarray
The enhanced image, showing improved visibility of edges and peaks. The output retains the same data type as the input.
Notes
-----
The sequence starts with Gaussian background division to highlight edges by suppressing steady background features,
followed by a Gabor filter for edge and texture enhancement. Morphological dilation and erosion emphasize structures,
and adaptive histogram equalization adjusts contrast. The process is designed for small to medium-sized features,
making it suitable for applications like microscopy or detailed texture analysis.
"""
input_dtype = str(image.dtype) # Store the input image's data type for later conversion back
img = dtype_conversion_func(image, 'float32') # Convert the image data type to float32 for processing
# Apply a large gaussian filter to smooth all objects in the image into the background
gaussian_bg = ndi.gaussian_filter(img, sigma=(ball_radius * 2))
# Perform gaussian background division for edge illumination enhancement
bg_division = img / (gaussian_bg + 0.00001)
# Rescale intensity of the background-divided image
bg_division_rescaled = apply_rescale_intensity(bg_division, out_min=0.75, out_max=1.0)
# Apply the rescaled background-divided image as an attenuation mask
img *= bg_division_rescaled
# Enhance edges and peaks using a Gabor filter
gabor_img = gabor_filter_func(img)
# Create a structural element for morphological operations
selem = sk.morphology.disk(1)
# Apply morphological dilation to enhance bright structures
gabor_img = ndi.grey_dilation(gabor_img, footprint=selem)
# Apply morphological erosion to refine the structures
gabor_img = ndi.grey_erosion(gabor_img, footprint=selem)
# Smooth the enhanced image with a small Gaussian filter
gabor_img = ndi.gaussian_filter(gabor_img, 0.5)
# Set parameters for adaptive histogram equalization
k_size = math.ceil(ball_radius * 4)
clip_lim = 0.0025
# Apply adaptive histogram equalization
output_image = sk.exposure.equalize_adapthist(gabor_img, kernel_size=k_size, clip_limit=clip_lim)
# Convert the output image back to the original input data type for consistency
output_image = dtype_conversion_func(output_image, output_bit_depth=input_dtype)
return output_image
[docs]
def run_peak_and_edge_enhancement(data_instance, viewer):
"""
Applies peak and edge enhancement techniques to the currently active image layer in a Napari viewer. The enhancement
process includes Gabor filtering, morphological operations, Gaussian smoothing, and adaptive histogram equalization.
Parameters
----------
viewer : napari.Viewer
The viewer containing the image layer to be enhanced.
Raises
------
Error
If no active image layer is selected, preventing the function from proceeding.
Notes
-----
The function retrieves the currently active image layer, applies the `peak_and_edge_enhancement_func`, and adds the
enhanced image back as a new layer to the viewer.
"""
ball_radius = math.ceil(data_instance.data_repository['ball_radius'])
# Retrieve the currently active image layer from the viewer
active_layer = viewer.layers.selection.active
# Validate that an active layer is selected
if active_layer is None or not isinstance(active_layer, napari.layers.Image):
# Raise an error if no layer is currently active
raise ValueError("No active image layer selected")
# Retrieve the image data from the active layer
image = active_layer.data
# Apply the peak and edge enhancement function to the input image
enhanced_image = peak_and_edge_enhancement_func(image, ball_radius)
# Add the enhanced image as a new layer to the viewer with a descriptive name
add_image_with_default_colormap(enhanced_image, viewer, name=f"Peak & Edge Enhanced {active_layer.name}")
[docs]
def apply_laplace_of_gauss_filter(image, sigma=3):
"""
Applies a Laplacian of Gaussian (LoG) filter to an input image for edge detection. This method combines
Gaussian smoothing with a Laplacian filter to reduce noise before detecting edges, enhancing feature definition
and image quality.
Parameters
----------
image : numpy.ndarray
The input image to be processed.
sigma : float
Standard deviation of the Gaussian kernel, which determines the level of blurring and influences edge detection sensitivity.
Returns
-------
gauss_laplace_image : numpy.ndarray
The image processed with the LoG filter, highlighting edges and returning it in the original data type.
"""
input_dtype = str(image.dtype) # Store the input image data type
img = dtype_conversion_func(image, 'float32') # Convert the image to float32 for processing
# Apply the LoG filter to the image
gauss_laplace_image = ndi.gaussian_laplace(img, sigma=sigma)
# Convert the image back to the original data type
gauss_laplace_image = dtype_conversion_func(gauss_laplace_image, input_dtype)
return gauss_laplace_image
[docs]
def apply_laplace_of_gauss_enhancement(image, sigma=3):
"""
Enhances an image using a Laplacian of Gaussian (LoG) filter followed by intensity rescaling and inversion to highlight edges.
The process involves edge detection, shifting image intensity to ensure all values are positive, rescaling the intensity to a
specified range, inverting the intensity to emphasize edges, and optionally multiplying with the original image for attenuation.
Parameters
----------
image : numpy.ndarray
The input image to be enhanced.
sigma : float
The standard deviation of the Gaussian kernel used in the LoG filter.
Returns
-------
enhanced_img : numpy.ndarray
The enhanced image, which is the input image attenuated by the processed LoG image for edge enhancement.
inverted_img : numpy.ndarray
The inverted LoG image, useful for visualization and analysis, can be applied as an attenuation mask to the original image.
"""
input_dtype = str(image.dtype) # Store the input image data type
img = dtype_conversion_func(image, 'float32') # Convert the image to float32 for processing
# Apply LoG filter to detect edges and smooth the image
LoG_img = apply_laplace_of_gauss_filter(img, sigma=sigma)
# Shift the image to ensure all values are positive
shifted_image = LoG_img + np.abs(np.min(LoG_img))
# Rescale the intensity to a narrow range to prepare for inversion
rescaled_img = apply_rescale_intensity(shifted_image, out_min=0.0, out_max=0.1)
# Invert the image to emphasize low-intensity edges
inverted_img = invert_image(rescaled_img)
# Apply the inverted LoG as an attenuation mask, this slighty enhances the contrast of edges
enhanced_img = inverted_img * img
# Convert the image back to the original data type
enhanced_img = dtype_conversion_func(enhanced_img, input_dtype)
return enhanced_img, inverted_img
[docs]
def run_apply_laplace_of_gauss_filter(sigma_input, viewer):
"""
Applies the Laplacian of Gaussian (LoG) filter to the currently active image layer in a Napari viewer,
using a user-specified sigma value from UI input. This enhances the image by highlighting edges through LoG filtering.
Parameters
----------
sigma_input : UI Element
A UI element that allows the user to input the sigma value for the LoG filter.
viewer : napari.Viewer
The Napari viewer instance where the image layer is displayed and processed.
Raises
------
Error
If no active image layer is selected in the viewer, prevent the application of the filter.
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
raise ValueError("No active image layer selected.")
sigma = float(sigma_input.text()) if sigma_input.text() else 3
# Apply the LoG filter to the input image
LoG_image = apply_laplace_of_gauss_filter(image, sigma)
# Add the LoG filtered image to the viewer
add_image_with_default_colormap(LoG_image, viewer, name=f"LoG of {active_layer.name}")
[docs]
def run_morphological_gaussian_filter(filter_size_input, viewer):
"""
Applies morphological operations and Gaussian smoothing to the active image layer in the Napari viewer,
enhancing structural features and reducing noise. The process involves morphological dilation and erosion
followed by Gaussian smoothing, with the results added as a new layer to the viewer.
Parameters
----------
filter_size_input : text input
A user interface element that allows the user to input the filter size, influencing the extent of the
morphological operations and Gaussian smoothing.
viewer : Viewer
An image viewer object that contains image layers, such as in a Napari viewer.
Raises
------
Error
If no active image layer is selected in the viewer.
Notes
-----
The filter size from the user input determines the size of the disk-shaped structural element used for
morphological dilation and erosion, directly impacting the degree of feature enhancement and noise reduction.
"""
# Retrieve the currently active image layer from the viewer
active_layer = viewer.layers.selection.active
# Validate that an active layer is selected
if active_layer is None or not isinstance(active_layer, napari.layers.Image):
# Raise an error if no layer is currently active
raise ValueError("No active image layer selected")
input_dtype = str(active_layer.data.dtype) # Store the input data type for conversion back at the end
# Retrieve the image data from the active layer
image = active_layer.data
# Convert the image to float32 for processing
img = dtype_conversion_func(image, output_bit_depth='float32')
# Determine the filter size from the user input or default to 2
filter_size = int(filter_size_input.text()) if filter_size_input.text() else 2
# Create a disk-shaped structural element based on the filter size
selem = sk.morphology.disk(filter_size)
# Apply morphological dilation to emphasize bright structures in the image
img = ndi.grey_dilation(img, footprint=selem)
# Apply morphological erosion to refine bright structures
img = ndi.grey_erosion(img, footprint=selem)
# Apply Gaussian smoothing to reduce noise, with the filter size influencing the smoothing extent
img = ndi.gaussian_filter(img, filter_size)
# Convert the processed image back to the original data type
image = dtype_conversion_func(img, output_bit_depth=input_dtype)
# Add the processed image as a new layer to the viewer with a descriptive name
add_image_with_default_colormap(image, viewer, name=f"Filtered {active_layer.name}")
[docs]
def run_clahe(clip_input, k_size_input, viewer):
"""
Applies Contrast Limited Adaptive Histogram Equalization (CLAHE) to the currently active image layer in the Napari
viewer. This technique enhances the contrast of the image by dividing it into small blocks and applying histogram
equalization to each block independently, limiting the amplification of noise common in standard methods.
Parameters
----------
clip_input : UI Element (Text Input)
A UI element that allows the user to input the clip limit value for CLAHE.
k_size_input : UI Element (Text Input)
A UI element that allows the user to input the kernel size for CLAHE.
viewer : napari.Viewer
The Napari viewer instance where the image layer is displayed and processed.
Raises
------
Error
If no active image layer is selected in the viewer.
Notes
-----
The function processes the image by converting it to float32 for enhanced precision during CLAHE and then converts
it back to its original data type. The clip limit and kernel size are adjustable, allowing for fine-tuning of the
contrast enhancement based on specific image requirements.
"""
# Retrieve the currently active image layer from the viewer
active_layer = viewer.layers.selection.active
# Validate that an active layer is selected
if active_layer is None or not isinstance(active_layer, napari.layers.Image):
# Raise an error if no layer is currently active
raise ValueError("No active image layer selected")
input_dtype = str(active_layer.data.dtype) # Store the input data type for conversion back at the end
# Retrieve the image data from the active layer
image = active_layer.data
# Convert the image to float32 for processing
img = dtype_conversion_func(image, output_bit_depth='float32')
# Retrieve clip limit and kernel size from the UI input, falling back to defaults if necessary
clip_val = float(clip_input.text()) if clip_input.text() else 0.0025
k_size = int(k_size_input.text()) if k_size_input.text() else None
# The number of bins is set dynamically based on the image height
no_bins = image.shape[0]
# Apply CLAHE to the image
CLAHE_img = sk.exposure.equalize_adapthist(img, kernel_size=k_size, clip_limit=clip_val, nbins=no_bins)
# Convert the processed image back to its original data type
CLAHE_img = dtype_conversion_func(CLAHE_img, output_bit_depth=input_dtype)
# Add the CLAHE-enhanced image as a new layer to the Napari viewer
add_image_with_default_colormap(CLAHE_img, viewer, name=f"CLAHE Contrast EQed {active_layer.name}")
[docs]
def deblur_by_pixel_reassignment(I_in, PSF, gain, window_radius):
"""
Performs Deblurring by Pixel Reassignment, adapted from MATLAB code, enhancing microscopy image quality by reducing
blurriness and improving visualization of microscopic entities [dpr_1]_.
Parameters
----------
I_in : numpy.ndarray
The input image array to be processed.
PSF : float
Point Spread Function width, quantifying blurriness in terms of pixels.
gain : float
Gain factor to adjust the intensity of pixel displacement.
window_radius : int
Radius in pixels for the local minimum filter, enhancing local contrast.
Returns
-------
single_frame_I_out : numpy.ndarray
The DPR processed deblurred image.
single_frame_I_magnified : numpy.ndarray
The magnified input image.
Notes
-----
The function adapts MATLAB code to Python, implementing modifications to suit Pythonic nuances and the specific data.
Modifications include scaling down the gain to reduce artifacts introduced by integer gain values in Python,
which differ from MATLAB's handling.
References
----------
.. [dpr_1] MATLAB code source: [DPR Project](https://github.com/biomicroscopy/DPR-Project)
- Related paper: "Advanced Photonics, Vol. 5, Issue 6, 066004 (October 2023)", available at https://doi.org/10.1117/1.AP.5.6.066004
"""
# Initial setup: calculate the upscale factor and prepare the Sobel filters for edge detection
num_row_initial, num_col_initial = I_in.shape
upscale_factor = 5 / PSF # Upscaled image has 5 pixels per PSF (1/e beam radius)
sobelY = np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]]) # Vertical edge detection
sobelX = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]) # Horizontal edge detection
# Preprocessing: Subtract local minimum for local contrast enhancement
single_frame_I_in = I_in - I_in.min()
local_minimum = np.zeros_like(I_in)
single_frame_I_in_localmin = np.zeros_like(I_in)
# Calculate local minimum for each pixel
for u in range(num_row_initial):
for v in range(num_col_initial):
sub_window = single_frame_I_in[max(0, u - window_radius):min(num_row_initial, u + window_radius + 1),
max(0, v - window_radius):min(num_col_initial, v + window_radius + 1)]
local_minimum[u, v] = sub_window.min()
single_frame_I_in_localmin[u, v] = single_frame_I_in[u, v] - local_minimum[u, v]
# Upscale images using interpolation to enhance details
single_frame_localmin_magnified = upscale_image_interp(single_frame_I_in_localmin, num_row_initial, num_col_initial, upscale_factor=upscale_factor, pad=True)
single_frame_I_magnified = upscale_image_interp(single_frame_I_in, num_row_initial, num_col_initial, upscale_factor=upscale_factor, pad=True)
# Normalize and calculate gradients for displacement calculation
I_normalized = single_frame_localmin_magnified / (ndi.gaussian_filter(single_frame_localmin_magnified, 10) + 0.000001)
gradient_x = ndi.convolve(I_normalized, sobelX, mode='nearest')
gradient_y = ndi.convolve(I_normalized, sobelY, mode='nearest')
gradient_x /= (I_normalized + 0.000001)
gradient_y /= (I_normalized + 0.000001)
# Apply gain and limit the displacements
gain_value = 0.1 * gain
displacement_x = gain_value * gradient_x
displacement_y = gain_value * gradient_y
displacement_x[np.abs(displacement_x) > 10] = 0 # Limit displacements to prevent artifacts
displacement_y[np.abs(displacement_y) > 10] = 0
# Weighted pixel displacement for image reconstruction
single_frame_I_out = np.zeros_like(single_frame_I_magnified)
num_row, num_col = single_frame_I_magnified.shape
for nx in range(10, num_row - 10):
for ny in range(10, num_col - 10):
# Process displacement and calculate weights for reconstruction
disp_x = displacement_x[nx, ny]
disp_y = displacement_y[nx, ny]
# Calculating the integer parts of the displacements
int_disp_x = int(np.fix(disp_x))
int_disp_y = int(np.fix(disp_y))
# Weights based on the fractional part of the displacement
weighted1 = (1 - abs(disp_x - int_disp_x)) * (1 - abs(disp_y - int_disp_y))
weighted2 = (1 - abs(disp_x - int_disp_x)) * abs(disp_y - int_disp_y)
weighted3 = abs(disp_x - int_disp_x) * (1 - abs(disp_y - int_disp_y))
weighted4 = abs(disp_x - int_disp_x) * abs(disp_y - int_disp_y)
# Calculating the coordinates for pixel reassignment
coordinate1 = (int_disp_x, int_disp_y)
coordinate2 = (int_disp_x, int(int_disp_y + np.sign(disp_y)))
coordinate3 = (int(int_disp_x + np.sign(disp_x)), int_disp_y)
coordinate4 = (int(int_disp_x + np.sign(disp_x)), int(int_disp_y + np.sign(disp_y)))
# Assigning the weighted pixel values to reconstruct the image
# To shift I-local_min use 'single_frame_localmin_magnified', to shift raw image use 'single_frame_I_magnified'
single_frame_I_out[nx + coordinate1[0], ny + coordinate1[1]] += weighted1 * single_frame_I_magnified[nx, ny]
single_frame_I_out[nx + coordinate2[0], ny + coordinate2[1]] += weighted2 * single_frame_I_magnified[nx, ny]
single_frame_I_out[nx + coordinate3[0], ny + coordinate3[1]] += weighted3 * single_frame_I_magnified[nx, ny]
single_frame_I_out[nx + coordinate4[0], ny + coordinate4[1]] += weighted4 * single_frame_I_magnified[nx, ny]
# Crop the borders added by displacement
single_frame_I_out = single_frame_I_out[10:-10, 10:-10]
single_frame_I_magnified = single_frame_I_magnified[10:-10, 10:-10]
return single_frame_I_out, single_frame_I_magnified
[docs]
def run_dpr(psf_input, gain_input, data_instance, viewer):
"""
Executes the Deblurring by Pixel Reassignment (DPR) process on a selected layer within a napari viewer, using user-defined
PSF and gain settings to improve image clarity and detail.
Parameters
----------
psf_input : QLineEdit
Widget for inputting the PSF value.
gain_input : QLineEdit
Widget for inputting the gain value.
data_instance : object
Contains data and metadata, such as cell diameter.
viewer : napari.Viewer
Viewer instance where images are displayed.
Raises
------
Error
If no active image layer is selected in the viewer.
"""
# Ensure an active layer is selected
active_layer = viewer.layers.selection.active
if active_layer is None:
raise ValueError("No active layer selected")
# Read PSF and gain from inputs, defaulting to preset values if empty
psf_0 = int(psf_input.text()) if psf_input.text() else 3
gain_0 = int(gain_input.text()) if gain_input.text() else 2
window_size = math.ceil(data_instance.data_repository['cell_diameter'] / 2)
# Apply DPR processing on the selected image layer
DPR_img, _ = deblur_by_pixel_reassignment(active_layer.data, psf_0, gain_0, window_size)
# Display the processed image in the viewer
add_image_with_default_colormap(DPR_img, viewer, name=f"DPR Corrected {active_layer.name}")
# Background and Noise Correction #
[docs]
def background_inpainting_func(image, mask, ball_radius):
"""
This function uses skimage biharmonic inpainting to 'extend' the masked region of an image to avoid edge effects and
artifacts from the rolling ball background subtraction method.
This function first uses dilation and erosion of the mask to define the 'unknown' region for inpainting, then applies
a biharmonic inpainting algorithm to fill in the background.
Parameters
----------
image : numpy.ndarray
The input image as a NumPy array.
mask : numpy.ndarray
A binary mask identifying the region of the image to be inpainted (background).
ball_radius : int
Radius used to adjust the size of the mask dilation and erosion, aiding in defining the inpainting region.
Returns
-------
inpainted_img : numpy.ndarray
The image with the background inpainted, returned as a NumPy array of the same type as the input image.
"""
# Store the input image data type
input_dtype = str(image.dtype)
# Convert input image to float32 for processing
img = dtype_conversion_func(image, 'float32')
# Erode the mask to ensure no background is erroneously left behind
eroded_mask = ndi.binary_erosion(mask, sk.morphology.disk(3))
# Dilate the mask to ensure the inpainting region extends beyond the rolling ball radius
dilated_mask = mask.copy()
for _ in range(int(4*ball_radius)):
dilated_mask = ndi.binary_dilation(dilated_mask, sk.morphology.disk(1))
# Identify the region for inpainting
unknown_region = dilated_mask ^ eroded_mask
# Perform inpainting on the identified region
inpainted_img = sk.restoration.inpaint_biharmonic(img, unknown_region)
# Convert the inpainted image back to the original data type
inpainted_img = dtype_conversion_func(inpainted_img, input_dtype)
return inpainted_img
[docs]
def compute_rolling_ball_background(image, ball_radius):
"""
Computes the background of an image using a rolling ball algorithm. This method is particularly useful for subtracting
background from unevenly lit images.
The function handles both 2D and 3D images, applying a suitable kernel for the rolling ball algorithm based on the image dimensions.
Parameters
----------
image : numpy.ndarray
The input image as a NumPy array.
ball_radius : int
Radius of the rolling ball, determining the size of the structure that can be subtracted.
Returns
-------
rb_background : numpy.ndarray
The background of the image computed using the rolling ball algorithm, matching the shape and dtype of the original image.
"""
# Get the input dtype
input_dtype = str(image.dtype)
# Convert the image to uint16 to ensure compatibility with the rolling ball algorithm
image = dtype_conversion_func(image, output_bit_depth='uint16')
# Apply rolling ball algorithm based on image dimensionality
if image.ndim == 2:
# 2D images
rb_background = sk.restoration.rolling_ball(image, radius=ball_radius)
else:
# 3D images, use an ellipsoid kernel for asymmetric z-stack handling
kernel = sk.restoration.ellipsoid_kernel((2*ball_radius, 2*ball_radius), 3)
rb_background = sk.restoration.rolling_ball(image, kernel=kernel)
# Smooth the computed background
rb_background = ndi.grey_dilation(rb_background, footprint=sk.morphology.disk(1))
rb_background = ndi.grey_erosion(rb_background, footprint=sk.morphology.disk(1))
# Convert the background back to the original data type
rb_background = dtype_conversion_func(rb_background, output_bit_depth=input_dtype)
return rb_background
[docs]
def subtract_background(image, background, bg_scaling_factor=0.75, equalize_intensity=False, window_size=None):
"""
Subtracts the background from an image, optionally scaling the background intensity and applying local contrast enhancement.
Parameters
----------
image : numpy.ndarray
The input image from which to subtract the background.
background : numpy.ndarray
The computed background to be subtracted.
bg_scaling_factor : float, optional
A scaling factor for the background intensity before subtraction, defaulting to 0.75.
equalize_intensity : bool, optional
Whether to apply adaptive histogram equalization to the result, defaulting to False.
window_size : int, optional
The window size for adaptive histogram equalization, required if equalize_intensity is True.
Returns
-------
ouput_image : numpy.ndarray
The image with the background subtracted and optional contrast enhancement, matching the original data type.
"""
# Store the input image data type
input_dtype = str(image.dtype)
# Convert input image to float32 for processing
img = dtype_conversion_func(image, 'float32')
# Subtract the scaled background from the image
bg_subtracted_img = img - (bg_scaling_factor * background)
bg_subtracted_img[bg_subtracted_img < 0] = 0 # Set negative values to zero
# Use morphological smoothing on the bg subtracted image
bg_subtracted_img = ndi.grey_erosion(bg_subtracted_img, footprint=sk.morphology.disk(1))
bg_subtracted_img = ndi.grey_dilation(bg_subtracted_img, footprint=sk.morphology.disk(1))
# Optionally apply adaptive histogram equalization
if equalize_intensity:
if window_size:
k_size = math.ceil(window_size)
else:
k_size = None # Uses the skimage default window size of image.shape // 8
bg_subtracted_img = sk.exposure.equalize_adapthist(bg_subtracted_img, kernel_size=k_size, clip_limit=0.0025)
# Convert back to the original data type
ouput_image = dtype_conversion_func(bg_subtracted_img, input_dtype)
return ouput_image
[docs]
def rb_gaussian_background_removal(image, ball_radius, equalize_intensity=False, roi_mask=None):
"""
Removes background from an image using rolling ball and Gaussian blur techniques, aiming to enhance
contrast and detail by minimizing background noise. The rolling ball algorithm is used first to model
and subtract the background. Then, Gaussian blur is applied to smooth all image details into the background,
which is then subtracted from the original image to remove the remaining background.
Parameters
----------
image : numpy.ndarray
The input image array from which to remove background noise.
ball_radius : int
Determines the radius for the rolling ball filter and influences the smoothing level of the Gaussian blur.
equalize_intensity : bool, optional
Enables intensity histogram equalization for the output image for improved visualization. Defaults to False.
roi_mask : numpy.ndarray, optional
A binary mask indicating the region of interest within the image. Background removal is confined to this region.
Returns
-------
bg_removed_image : numpy.ndarray
The image with the background removed, maintaining the same dimensions and data type as the input.
Note
----
The function handles data type conversions internally for processing and reverts to the original data type
for compatibility with downstream imaging tasks.
"""
# Get the input dtype
input_dtype = str(image.dtype)
# Convert the input image to float32 for processing
img = dtype_conversion_func(image, 'float32')
# Apply the ROI mask if provided
if roi_mask is not None:
roi_mask = roi_mask.astype(bool) # Ensure the ROI mask is boolean
img *= roi_mask # Apply the mask to the image
# Inpaint the background aroud the mask to avoid edge artifcats from the rolling ball algorithm
bg_img = background_inpainting_func(img, roi_mask, ball_radius)
else:
# If no mask is provided, use the entire image
bg_img = img.copy()
# Compute the background using the rolling ball algorithm
rb_background = compute_rolling_ball_background(bg_img, ball_radius)
# Apply a gaussian filter to smooth the edges of the translated ball
rb_background = ndi.gaussian_filter(rb_background, sigma=ball_radius//2)
# Subtract the rolling ball background from the original image
rb_bg_subtracted_img = subtract_background(img, rb_background, bg_scaling_factor=0.75, equalize_intensity=False)
# Apply a large gaussian filter to smooth all objects in the image into the background
gaussian_bg = ndi.gaussian_filter(rb_bg_subtracted_img, sigma=(ball_radius*2))
# Subtract the Gaussian background for final background removal
gaussian_bg_subtracted_img = subtract_background(rb_bg_subtracted_img, gaussian_bg, bg_scaling_factor=0.75, equalize_intensity=equalize_intensity, window_size=ball_radius*4)
# Convert the final image back to the original data type
bg_removed_image = dtype_conversion_func(gaussian_bg_subtracted_img, output_bit_depth=input_dtype)
return bg_removed_image
[docs]
def run_rb_gaussian_background_removal(eq_int_input, data_instance, viewer):
"""
Executes the rb_gaussian_background_removal function on an active image layer within a napari viewer,
enhancing the image by removing background noise using a combined rolling ball and Gaussian blur approach.
Parameters
----------
eq_int_input : QtWidgets.QCheckBox
Input checkbox to specify whether intensity equalization should be applied to the processed image.
data_instance : DataInstance
An object encapsulating relevant data and parameters, including the ball radius for background removal.
viewer : napari.Viewer
The image viewer in which the processed image will be displayed.
Raises
------
Error
If no active image layer is selected or if the layer is not compatible for processing.
Note
----
This function fetches parameters from the data_instance, applies background removal to the selected image,
and updates the viewer by adding the processed image as a new layer. The name of the new layer reflects the
background removal process.
"""
active_layer = viewer.layers.selection.active # Retrieve the active layer from the viewer
ball_radius = math.ceil(data_instance.data_repository['ball_radius']) # Get the ball radius from the data instance
equalize_intensity_input = eq_int_input.isChecked() # Check if equalize intensity is enabled
# Check if there is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
napari_show_warning("No active image layer selected.")
# Perform the background removal on the image
bg_removed_image = rb_gaussian_background_removal(image, ball_radius, equalize_intensity=equalize_intensity_input)
# Add the processed image as a new layer in the viewer
add_image_with_default_colormap(bg_removed_image, viewer, name=f'RB-Gaussian Background Removed {active_layer.name}')
[docs]
def rb_gaussian_bg_removal_with_edge_enhancement(image, ball_radius, roi_mask=None):
"""
Applies background removal and edge enhancement to an image using a combination of processing techniques.
The method involves rolling ball and Gaussian background subtraction followed by edge enhancement
through Gabor filtering and adaptive histogram equalization to improve feature visibility, particularly
useful in microscopic image analysis.
Parameters
----------
image : numpy.ndarray
The input image to be processed.
ball_radius : int
The radius of the rolling ball filter, used in the initial background removal step.
roi_mask : numpy.ndarray, optional
A binary mask defining the region of interest (ROI); processing is confined to this region if provided.
Returns
-------
output_image : numpy.ndarray
The enhanced image after background removal and edge enhancement, returned in the original data type.
Note
----
The sequence of image processing steps integrates background subtraction with texture and edge enhancement
to enhance microscopic images or similar detailed visual data.
"""
input_dtype = str(image.dtype) # Store the input image's data type for later conversion back
img = dtype_conversion_func(image, 'float32') # Convert the image data type to float32 for processing
# Remove the background using rolling ball and Gaussian-based method
bg_removed_image = rb_gaussian_background_removal(img, ball_radius, equalize_intensity=True, roi_mask=roi_mask)
output_image = peak_and_edge_enhancement_func(bg_removed_image, ball_radius)
# Convert the processed image back to its original data type
output_image = dtype_conversion_func(output_image, output_bit_depth=input_dtype)
return output_image
[docs]
def run_enhanced_rb_gaussian_bg_removal(data_instance, viewer):
"""
Executes an enhanced RB Gaussian background removal process on the active image layer within a napari viewer,
improving the image by removing background noise and enhancing edges. The processed image is then displayed
as a new layer in the viewer.
Parameters
----------
viewer : napari.Viewer
The napari viewer object to which the processed image will be added.
data_instance : DataInstance
Encapsulates relevant data and parameters for the session, including the necessary ball radius for processing.
Raises
------
Error
If no active image layer is selected, preventing process execution.
Note
----
Retrieves parameters from the data_instance, applies the background removal and enhancement process,
and updates the viewer with a new layer named to indicate the applied enhancements.
"""
# Retrieve the active layer and the ball radius from the data instance
active_layer = viewer.layers.selection.active
ball_radius = math.ceil(data_instance.data_repository['ball_radius'])
# Validate the active layer
if active_layer is None or not isinstance(active_layer, napari.layers.Image):
raise ValueError("No active image layer selected or the selected layer is not an image layer.")
# Process the active image layer
image = active_layer.data
enhanced_image = rb_gaussian_bg_removal_with_edge_enhancement(image, ball_radius)
# Add the processed image as a new layer with an indicative name
add_image_with_default_colormap(enhanced_image, viewer, name=f'Enhanced Background Removed {active_layer.name}')
[docs]
def wavelet_bg_and_noise_calculation(image, num_levels, noise_lvl):
"""
Decomposes an image using a wavelet transform and selectively modifies the coefficients to isolate
and remove noise and background before reconstructing the image. This method allows for precise
background and noise estimation and removal.
Parameters
----------
image : numpy.ndarray
Input image array for processing.
num_levels : int
Number of decomposition levels for background estimation in the wavelet transform.
noise_lvl : int
Levels considered for noise estimation and removal in the wavelet decomposition.
Returns
-------
Background : numpy.ndarray
The estimated background after wavelet decomposition and reconstruction.
Noise : numpy.ndarray
The noise component extracted from the wavelet decomposition.
BG_unfiltered : numpy.ndarray
The raw background before filtering and Gaussian smoothing.
Authors
-------
- Manuel Hüpfel, Institute of Applied Physics, KIT, Karlsruhe, Germany
- Improved documentation: Christian Neureuter, University at Buffalo
"""
# Wavelet decomposition
coeffs = wavedecn(image, 'db1', level=None) # 'db1' denotes Daubechies wavelet with one vanishing moment
coeffs2 = coeffs.copy()
# Zeroing coefficients at specified levels to remove background
for BGlvl in range(1, num_levels):
coeffs[-BGlvl] = {k: np.zeros_like(v) for k, v in coeffs[-BGlvl].items()}
# Wavelet reconstruction to obtain the background
Background = waverecn(coeffs, 'db1')
BG_unfiltered = Background.copy()
Background = ndi.gaussian_filter(Background, sigma=2**num_levels) # Smooth the background with a gaussian filter w/ sigma=2^(#lvls)
# Modify coefficients for noise estimation and removal
coeffs2[0] = np.ones_like(coeffs2[0]) # Set approximation coefficients to 1 (constant)
for lvl in range(1, len(coeffs2) - noise_lvl):
coeffs2[lvl] = {k: np.zeros_like(v) for k, v in coeffs2[lvl].items()} # Keep the first detail lvl only
# Wavelet reconstruction to obtain the noise component
Noise = waverecn(coeffs2, 'db1')
return Background, Noise, BG_unfiltered
[docs]
def wbns_func(img, psf_px_resolution, noise_lvl):
"""
Wrapper function for wavelet-based background and noise subtraction (WBNS), adapted from [wbns_1]_.
It adjusts image dimensions for compatibility, performs background and noise subtraction using
wavelet transforms, and restores original dimensions, improving image clarity.
Parameters
----------
img : numpy.ndarray
Input image array to be processed.
psf_px_resolution : int
Resolution of the image in pixels, based on the point spread function (PSF) size, used to calculate
decomposition levels for wavelet processing.
noise_lvl : int
Number of levels to consider for noise estimation and removal during wavelet processing.
Returns
-------
bg_noise_output_image : numpy.ndarray
The image with background and noise subtracted, maintaining the original data type.
noise_output_image : numpy.ndarray
The image with noise subtracted, but not backgroud correction, maintains the original data type.
tuple
Returns a tuple of numpy.ndarrays: the background and noise corrected image, and the noise-only corrected image.
Notes
-----
In adapting this code, the paralell processing was removed as was the 3D z-stack processing. It would be simpe enough to refer
to their original source code, should the functionality be desired to be re-added. The noise level also had a gaussian blur added
to it, similar to the reconstrcuted background, to further reduce artifacts, as leaving it as is, caused some artifacts in the
final image. The background and noise images were scaled by 0.75 and 0.25 respectively, as leaving them as is, was a bit aggressive.
References
----------
.. [wbns_1] Original Python code: [HuepfelM WBNS](https://github.com/NienhausLabKIT/HuepfelM/blob/master/WBNS/python_script/WBNS.py)
- Related paper: Biomed. Opt. Express 12, 969-980 (2021), [DOI](https://doi.org/10.1364/BOE.413181)
"""
# Determine the number of decomposition levels based on image resolution
num_levels = np.uint16(np.ceil(np.log2(psf_px_resolution)))
input_dtype = str(img.dtype) # store the input dtype for conversion back at the end
# These calculations utilize decimals and are therefore better run on floats, float32 saves time and memory
img = dtype_conversion_func(img, output_bit_depth='float32')
shape = np.shape(img)
# Padding to make image dimensions even
pad_1, pad_2 = False, False
if shape[0] % 2 != 0:
img = np.pad(img, ((0,1), (0, 0)), 'edge')
pad_1 = True
if shape[1] % 2 != 0:
img = np.pad(img, ((0,0), (0, 1)), 'edge')
pad_2 = True
# Suppress VisibleDeprecationWarning from NumPy, potentially triggered by pywavelets.
# This is a temporary measure due to an unresolved issue in the pywavelets package
# that does not affect the functionality of our code but produces unwanted warning messages.
warnings.filterwarnings("ignore", category=np.VisibleDeprecationWarning) # pywavelets must have some stupid bug that I cant figure out so Im just going to suppress the annoying warning here
# Background and noise extraction
Background, Noise, BG_unfiltered = wavelet_bg_and_noise_calculation(img, num_levels, noise_lvl)
# Convert arrays to float32 for processing
Noise = np.asarray(Noise, dtype='float32')
Background = np.asarray(Background, dtype='float32')
BG_unfiltered = np.asarray(BG_unfiltered, dtype='float32')
# Undo padding
if pad_1:
img = img[:-1,:]
Noise = Noise[:-1,:]
Background = Background[:-1,:]
BG_unfiltered = BG_unfiltered[:-1,:]
if pad_2:
img = img[:,:-1]
Noise = Noise[:,:-1]
Background = Background[:,:-1]
BG_unfiltered = BG_unfiltered[:,:-1]
# Background subtraction and positivity constraint
bg_subtracted = img - (0.65*Background)
bg_subtracted[bg_subtracted < 0] = 0
# Noise correction and thresholding
Noise[Noise < 0] = 0
noise_threshold = np.mean(Noise) + 2*np.std(Noise) # 2 sigma threshold reduces artifacts
Noise[Noise > noise_threshold] = noise_threshold
Noise_smooth = ndi.gaussian_filter(Noise, sigma=(num_levels)) # Gaussian filter smooths the noise to further reduce artifacts
# Noise subtraction and positivity constraint from the original image
noise_corrected = img - (0.7*Noise_smooth)
noise_corrected[noise_corrected < 0] = 0
# Noise subtraction and positivity constraint from the background corrected image
bg_noise_corrected = bg_subtracted - (0.2*Noise_smooth)
bg_noise_corrected[bg_noise_corrected < 0] = 0
# Convert the output image back to the original data type
bg_noise_output_image = dtype_conversion_func(bg_noise_corrected, output_bit_depth=input_dtype)
noise_output_image = dtype_conversion_func(noise_corrected, output_bit_depth=input_dtype)
return bg_noise_output_image, noise_output_image
[docs]
def run_wbns(psf_input, noise_level_input, viewer):
"""
Executes the WBNS process on an active image layer within a napari viewer, enhancing the image by removing
background noise and correcting noise artifacts based on user-input PSF and noise level settings.
Parameters
----------
psf_input : QLineEdit
Input field for the PSF value, determining the decomposition level for wavelet processing.
noise_level_input : QLineEdit
Input field for the noise level, specifying how many wavelet decomposition levels are used for noise reduction.
viewer : napari.viewer.Viewer
Viewer instance where the processed images are displayed.
Raises
------
Error
If no active image layer is selected.
Notes
-----
The function retrieves settings from user inputs, applies the WBNS algorithm to the selected image,
and updates the viewer with the processed image, adding it as a new layer with a descriptive name.
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
raise ValueError("No active image layer selected.")
psf_fwhm = int(psf_input.text()) if psf_input.text() else 3
noise_lvl = int(noise_level_input.text()) if noise_level_input.text() else 1
# Process the image with the WBNS function
WBNS_img, _ = wbns_func(image, psf_fwhm, noise_lvl)
# Display the processed image
add_image_with_default_colormap(WBNS_img, viewer, name=f"BG and Noise Corrected {active_layer.name}")
[docs]
def run_wavelet_noise_subtraction(psf_input, noise_level_input, viewer):
"""
Applies the wavelet noise subtraction from the WBNS process to an active image layer selected in a viewer.
Reads PSF and noise level values from input fields, processes the selected image using the WBNS algorithm,
and updates the viewer with the corrected image.
Parameters
----------
psf_input : QLineEdit
Input field for specifying the point spread function (PSF) value, which determines the wavelet decomposition levels for noise processing.
noise_level_input : QLineEdit
Input field for setting the noise level, indicating the intensity of noise reduction to apply.
viewer : napari.Viewer
Viewer instance where the processed images are displayed.
Raises
------
Error
If no active image layer is selected.
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
napari_show_warning("No active image layer selected.")
return
psf_fwhm = int(psf_input.text()) if psf_input.text() else 3
noise_lvl = int(noise_level_input.text()) if noise_level_input.text() else 1
# Process the image with the WBNS function
_, wavelet_noise_corrected = wbns_func(image, psf_fwhm, noise_lvl)
# Display the processed image
add_image_with_default_colormap(wavelet_noise_corrected, viewer, name=f"Wavelet Noise Corrected {active_layer.name}")
[docs]
def apply_bilateral_filter(image, radius):
"""
Applies a bilateral filter to an image to reduce noise while preserving edges. The filter considers both
spatial proximity and intensity similarity between pixels, which makes it particularly effective for
denoising while maintaining important structural details in images.
Parameters
----------
image : numpy.ndarray
The input image array to be processed.
radius : int
The radius of the filter, determining the size of the spatial neighborhood for smoothing.
Returns
-------
filtered_image : numpy.ndarray
The image with noise reduced using the bilateral filter, returned in the original data type.
Notes
-----
The function uses the SimpleITK library for the bilateral filter application, ensuring high performance
and quality of noise reduction. Images are temporarily converted to float32 for processing to maintain precision.
"""
input_dtype = str(image.dtype) # Store the input data type for conversion back at the end
img = dtype_conversion_func(image, output_bit_depth='float32') # Convert the image to float32 for processing
# Convert the image to SimpleITK format
img_sitk = sitk.GetImageFromArray(img)
# Apply the bilateral filter to the image
filtered_img_sitk = sitk.Bilateral(img_sitk, radius)
# Convert the filtered image back to a NumPy array
img = sitk.GetArrayFromImage(filtered_img_sitk)
# Deprecated skimage bilateral filter (for some reason the skimage version adds some sort of shift to the image)
# Apply the bilateral filter to the image
#filtered_image = sk.restoration.denoise_bilateral(image, win_size=2*radius, multichannel=False)
# Convert the filtered image back to the original data type
filtered_image = dtype_conversion_func(img, output_bit_depth=input_dtype)
return filtered_image
[docs]
def run_apply_bilateral_filter(radius_input, viewer):
"""
Applies a bilateral filter to an active image layer in a Napari viewer to reduce noise while preserving
important details. The filter radius is retrieved from the user's input.
Parameters
----------
radius_input : QLineEdit
The input field where users specify the filter radius.
viewer : napari.Viewer
The viewer instance where the processed image will be displayed.
Raises
------
Error
If no active image layer is selected.
Notes
----
The function retrieves the radius from the input, applies the bilateral filter, and displays the result as a
new layer in the viewer, facilitating immediate visual feedback.
"""
active_layer = viewer.layers.selection.active
# Check if their is an active layer, and that it is a Napari image layer
if active_layer is not None and isinstance(active_layer, napari.layers.Image):
image = active_layer.data
else:
napari_show_warning("No active image layer selected.")
return
# Get the radius value from the input field
radius = float(radius_input.text()) if radius_input.text() else 2
# Apply the bilateral filter to the image
filtered_image = apply_bilateral_filter(image, radius)
# Add the filtered image as a new layer to the viewer
add_image_with_default_colormap(filtered_image, viewer, name=f"Bilateral Filtered {active_layer.name}")
[docs]
def pre_process_image(image, ball_radius, window_size):
"""
Enhances features in an image through a comprehensive pre-processing pipeline that includes noise reduction,
feature enhancement, and contrast improvement. This function is tailored for images where maintaining
feature integrity and detail is crucial, such as in microscopic imaging.
Parameters
----------
image : numpy.ndarray
The input image array to be processed.
ball_radius : int
The radius used for the disk element in the White Top Hat filter and other morphological operations.
window_size : int
The window size used for CLAHE, influencing how contrast is adapted locally in the image.
Returns
-------
output_image : numpy.ndarray
The pre-processed image, converted back to its original data type, with enhanced features and reduced noise.
Notes
-----
The pre-processing pipeline includes the following steps:
- Converting image data type to float32 for processing.
- Applying a White Top Hat filter to highlight bright elements smaller than the footprint.
- Enhancing the image using a Laplacian of Gaussian filter.
- Removing background and noise using a custom wavelet based noise and backkgroud removal function.
- Performing erosion and dilation for noise reduction.
- Applying Gaussian filter for smoothing.
- Enhancing contrast using CLAHE (Contrast Limited Adaptive Histogram Equalization).
"""
input_dtype = str(image.dtype) # Store original image data type for conversion back after processing
img = dtype_conversion_func(image, output_bit_depth='float32') # Convert image data type to float32 for processing
# Apply White Top Hat filter to highlight small elements in the image
white_top_hat_img = ndi.white_tophat(img, footprint=sk.morphology.disk(ball_radius))
rescaled_top_hat = apply_rescale_intensity(white_top_hat_img, out_min=0.3, out_max=1.0)
top_hat_enhanced_img = rescaled_top_hat * img
# Enhance the image using the Laplacian of Gaussian (LoG) filter
_, inverted_LoG_img = apply_laplace_of_gauss_enhancement(img, sigma=3)
LoG_enhanced_img = inverted_LoG_img * top_hat_enhanced_img
# Parameters for background and noise removal
psf_res = 4 # Point Spread Function resolution
noise_lvl = 1 # Noise level
# Remove background and noise using WBNS function
WBNS_img, _ = wbns_func(LoG_enhanced_img, psf_res, noise_lvl)
# Noise reduction through morphological operations
img = WBNS_img.copy()
selem = sk.morphology.disk(1) # Structuring element for erosion and dilation
img = ndi.grey_erosion(img, footprint=selem)
img = ndi.grey_dilation(img, footprint=selem)
# Apply Gaussian filter for image smoothing
img = ndi.gaussian_filter(img, 1)
# Apply CLAHE for contrast enhancement
k_size = math.ceil(window_size) # Kernel size for CLAHE
clip_limit = 0.0025 # Clip limit for CLAHE
img = sk.exposure.equalize_adapthist(img, kernel_size=k_size, clip_limit=clip_limit)
# Convert the processed image back to its original data type
output_image = dtype_conversion_func(img, output_bit_depth=input_dtype)
return output_image
[docs]
def run_pre_process_image(data_instance, viewer):
"""
Run the pre-processing function on an image selected in a viewer interface. This function handles the selection
of an active image layer, retrieves necessary parameters from a data instance, applies the pre-processing, and
then adds the processed image back to the viewer.
Parameters
----------
viewer : napari.Viewer
The Napari viewer instance where the image layers are managed.
data_instance : object
An object containing the data repository with parameters such as ball radius and window size for the pre-processing.
Raises
------
Error
If no active image layer is selected.
Notes
-----
Retrieves necessary parameters from the data_instance, applies a comprehensive pre-processing pipeline to the selected
image, and displays the enhanced image as a new layer in the viewer. This allows users to immediately observe and analyze the
effects of the pre-processing on the original image.
"""
# Check for an active image layer in the viewer
active_layer = viewer.layers.selection.active
if active_layer is None or not isinstance(active_layer, napari.layers.Image):
raise ValueError("No active image layer selected")
# Retrieve the image and parameters for pre-processing from the data instance
image = active_layer.data
ball_radius = data_instance.data_repository['ball_radius']
window_size = data_instance.data_repository['cell_diameter'] // 2
# Apply pre-processing to the selected image
pre_processed_image = pre_process_image(image, ball_radius, window_size)
# Add the pre-processed image to the viewer with a default colormap
add_image_with_default_colormap(pre_processed_image, viewer, name=f"Pre-Processed {active_layer.name}")