-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #145 from fmi-faim/spot-detection
Add spot and blob detection functions.
- Loading branch information
Showing
8 changed files
with
473 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from typing import Optional | ||
|
||
import numpy as np | ||
from scipy.ndimage import gaussian_laplace | ||
from skimage.feature import peak_local_max | ||
from skimage.morphology import h_maxima, ball | ||
from skimage.util import img_as_float32 | ||
from skimage.feature.blob import _prune_blobs | ||
|
||
from faim_ipa.detection.utils import estimate_log_rescale_factor | ||
|
||
|
||
def detect_blobs( | ||
img: np.ndarray, | ||
axial_sigma: float, | ||
lateral_sigma: float, | ||
h: int, | ||
n_scale_levels: int, | ||
overlap: float, | ||
background_img: Optional[np.ndarray] = None, | ||
) -> np.ndarray: | ||
"""Detect blobs of different sizes. | ||
The blob detection finds blobs of different sizes by applying a | ||
Laplacian of Gaussian with increasing sigmas followed by h-maxima | ||
filtering. The blob detection starts with the provided sigmas and | ||
then doubles them for `n_scale_levels`. | ||
Parameters | ||
---------- | ||
img : | ||
Image containing spot signal. | ||
axial_sigma : | ||
Z extension of the spots. | ||
lateral_sigma : | ||
YX extension of the spots. | ||
h : | ||
h-maxima threshold. | ||
n_scale_levels : | ||
Number of upscaling rounds. | ||
overlap : | ||
A value between 0 and 1. If the fraction of area overlapping for 2 | ||
blobs is greater than `overlap` the smaller blob is eliminated. | ||
background_img : | ||
Estimated background image. This is subtracted before the | ||
blob detection. | ||
Returns | ||
------- | ||
Detected spots. | ||
""" | ||
if background_img is not None: | ||
image = img_as_float32(img) - img_as_float32(background_img) | ||
else: | ||
image = img_as_float32(img) | ||
|
||
rescale_factor = estimate_log_rescale_factor( | ||
axial_sigma=axial_sigma, lateral_sigma=lateral_sigma | ||
) | ||
|
||
sigmas = [ | ||
(axial_sigma * 2**i, lateral_sigma * 2**i, lateral_sigma * 2**i) | ||
for i in range(n_scale_levels) | ||
] | ||
|
||
scale_cube = np.empty(image.shape + (len(sigmas),), dtype=np.uint8) | ||
|
||
h_ = img_as_float32(np.array(h, dtype=img.dtype)) | ||
for i, sigma in enumerate(sigmas): | ||
log_img = ( | ||
-gaussian_laplace(image, sigma=sigma) | ||
* rescale_factor | ||
* (np.mean(sigma) / np.mean(sigmas[0])) ** 2 | ||
) | ||
scale_cube[..., i] = h_maxima(log_img, h=h_, footprint=ball(1)) | ||
|
||
maxima = peak_local_max( | ||
scale_cube, | ||
threshold_abs=0.1, | ||
exclude_border=False, | ||
footprint=np.ones((3,) * scale_cube.ndim), | ||
) | ||
|
||
# Convert local_maxima to float64 | ||
lm = maxima.astype(np.float64) | ||
|
||
# translate final column of lm, which contains the index of the | ||
# sigma that produced the maximum intensity value, into the sigma | ||
sigmas_of_peaks = np.array(sigmas)[maxima[:, -1]] | ||
|
||
# Remove sigma index and replace with sigmas | ||
lm = np.hstack([lm[:, :-1], sigmas_of_peaks]) | ||
|
||
sigma_dim = sigmas_of_peaks.shape[1] | ||
|
||
return _prune_blobs(np.array(lm), overlap=overlap, sigma_dim=sigma_dim) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from typing import Optional | ||
|
||
import numpy as np | ||
from scipy.ndimage import gaussian_laplace | ||
from skimage.morphology import h_maxima, ball | ||
from skimage.util import img_as_float32 | ||
|
||
from faim_ipa.detection.utils import estimate_log_rescale_factor | ||
|
||
|
||
def detect_spots( | ||
img: np.ndarray, | ||
axial_sigma: float, | ||
lateral_sigma: float, | ||
h: int, | ||
background_img: Optional[np.ndarray] = None, | ||
) -> np.ndarray: | ||
"""Detect diffraction limited spots. | ||
The spot detection uses a Laplacian of Gaussian filter to detect | ||
spots of a given size. These detections are intensity filtered with | ||
a h-maxima filter. | ||
Parameters | ||
---------- | ||
img : | ||
Image containing spot signal. | ||
axial_sigma : | ||
Z extension of the spots. | ||
lateral_sigma : | ||
YX extension of the spots. | ||
h : | ||
h-maxima threshold. | ||
background_img : | ||
Estimated background image. This is subtracted before the | ||
spot detection. | ||
Returns | ||
------- | ||
Detected spots. | ||
""" | ||
if background_img is not None: | ||
image = img_as_float32(img) - img_as_float32(background_img) | ||
else: | ||
image = img_as_float32(img) | ||
|
||
rescale_factor = estimate_log_rescale_factor( | ||
axial_sigma=axial_sigma, lateral_sigma=lateral_sigma | ||
) | ||
log_img = ( | ||
-gaussian_laplace(image, sigma=(axial_sigma, lateral_sigma, lateral_sigma)) | ||
* rescale_factor | ||
) | ||
|
||
h_ = img_as_float32(np.array(h, dtype=img.dtype)) | ||
h_detections = h_maxima(log_img, h=h_, footprint=ball(1)) | ||
return np.array(np.where(h_detections)).T |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import numpy as np | ||
from scipy.ndimage import gaussian_laplace, gaussian_filter | ||
|
||
|
||
def compute_axial_sigma(wavelength: float, NA: float, axial_spacing: float): | ||
""" | ||
Sigma which produces a Gaussian with the same full width | ||
half maximum as described by Abbe's diffraction formula for axial resolution. | ||
R = 2 * lambda / (NA**2) | ||
Parameters | ||
---------- | ||
wavelength : | ||
Emission wavelength | ||
NA : | ||
Numerical Aperture | ||
axial_spacing : | ||
Spacing of z planes in nanometers | ||
Returns | ||
------- | ||
theoretical sigma in pixels | ||
""" | ||
return 2 * wavelength / (NA**2) / (2 * np.sqrt(2 * np.log(2))) / axial_spacing | ||
|
||
|
||
def compute_lateral_sigma(wavelength: float, NA: float, lateral_spacing: float): | ||
""" | ||
Sigma which produces a Gaussian with the same full width | ||
half maximum as the theoretical resolution limit in Y/X described by E. Abbe. | ||
d = lambda / (2*NA) | ||
Parameters | ||
---------- | ||
wavelength : | ||
Emission wavelength | ||
NA : | ||
Numerical Aperture | ||
lateral_spacing : | ||
Pixel size in YX. | ||
Returns | ||
------- | ||
theoretical sigma in pixels | ||
""" | ||
return wavelength / (2 * NA) / (2 * np.sqrt(2 * np.log(2))) / lateral_spacing | ||
|
||
|
||
def estimate_log_rescale_factor(axial_sigma: float, lateral_sigma: float) -> float: | ||
""" | ||
Estimate the rescale factor for a LoG filter response, such that | ||
the LoG filter response intensities are equal to the input image | ||
intensities for spots of size equal to a Gaussian with sigma | ||
(axial_sigma, lateral_sigma, lateral_sigma). | ||
Parameters | ||
---------- | ||
axial_sigma : | ||
Sigma in axial direction. Usually along Z. | ||
lateral_sigma : | ||
Sigma in lateral direction. Usually along Y and X. | ||
Returns | ||
------- | ||
rescale_factor | ||
""" | ||
extend = int(max(axial_sigma, lateral_sigma) * 7) | ||
img = np.zeros((extend,) * 3, dtype=np.float32) | ||
img[extend // 2, extend // 2, extend // 2] = 1 | ||
img = gaussian_filter(img, (axial_sigma, lateral_sigma, lateral_sigma)) | ||
img = img / img.max() | ||
img_log = -gaussian_laplace( | ||
input=img, sigma=(axial_sigma, lateral_sigma, lateral_sigma) | ||
) | ||
return 1 / img_log.max() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import numpy as np | ||
from numpy.testing import assert_array_equal | ||
from scipy.ndimage import gaussian_filter | ||
|
||
from faim_ipa.detection.blobs import detect_blobs | ||
|
||
|
||
def test_detect_blobs(): | ||
np.random.seed(0) | ||
img = np.zeros((101, 101, 101), dtype=np.float32) | ||
|
||
# Create 3 spots with intensities of 100, 200, 300 | ||
img[25, 25, 25] = 1 | ||
img[50, 50, 50] = 1 | ||
img[75, 75, 75] = 1 | ||
img = gaussian_filter(img, (2.07, 0.75, 0.75)) | ||
img = 300 * img / img.max() | ||
|
||
# Create 1 larger spot | ||
large_spot = np.zeros((101, 101, 101), dtype=np.float32) | ||
large_spot[50, 75, 75] = 1 | ||
large_spot = gaussian_filter(large_spot, (2 * 2.07, 2 * 0.75, 2 * 0.75)) | ||
large_spot = 300 * large_spot / large_spot.max() | ||
|
||
# Create random background noise | ||
background_img = np.random.normal(10, 2, (101, 101, 101)).astype(np.float32) | ||
|
||
# Create hot-pixel | ||
hot_pixels = np.zeros((101, 101, 101), dtype=np.float32) | ||
hot_pixels[25, 25, 50] = 1000 | ||
|
||
# Combine to final image | ||
img_final = (img + large_spot + hot_pixels + background_img).astype(np.uint16) | ||
|
||
# Fake estimated background with hot-pixels | ||
estimated_bg = (np.ones_like(background_img) * background_img.mean()).astype( | ||
np.uint16 | ||
) | ||
estimated_bg[hot_pixels > 0] = 1000 | ||
|
||
# Detect spots without estimated background | ||
blobs = detect_blobs( | ||
img=img_final, | ||
axial_sigma=2.07, | ||
lateral_sigma=0.75, | ||
h=200, | ||
n_scale_levels=2, | ||
overlap=0.875, | ||
background_img=None, | ||
) | ||
assert blobs.shape[0] == 5 | ||
assert_array_equal( | ||
blobs, | ||
np.array( | ||
[ | ||
[25, 25, 25, 2.07, 0.75, 0.75], | ||
[25, 25, 50, 2.07, 0.75, 0.75], | ||
[50, 50, 50, 2.07, 0.75, 0.75], | ||
[50, 75, 75, 2 * 2.07, 2 * 0.75, 2 * 0.75], | ||
[75, 75, 75, 2.07, 0.75, 0.75], | ||
] | ||
), | ||
) | ||
|
||
# Detect spots with estimated background | ||
blobs = detect_blobs( | ||
img=img_final, | ||
axial_sigma=2.07, | ||
lateral_sigma=0.75, | ||
h=200, | ||
n_scale_levels=2, | ||
overlap=0.875, | ||
background_img=estimated_bg, | ||
) | ||
assert blobs.shape[0] == 4 | ||
assert_array_equal( | ||
blobs, | ||
np.array( | ||
[ | ||
[25, 25, 25, 2.07, 0.75, 0.75], | ||
[50, 50, 50, 2.07, 0.75, 0.75], | ||
[50, 75, 75, 2 * 2.07, 2 * 0.75, 2 * 0.75], | ||
[75, 75, 75, 2.07, 0.75, 0.75], | ||
] | ||
), | ||
) |
Oops, something went wrong.