Coverage for nilearn/plotting/displays/edge_detect.py: 0%
42 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1"""Edge detection routines: this file provides a Canny filter."""
3import numpy as np
4from scipy import signal
5from scipy.ndimage import (
6 binary_dilation,
7 distance_transform_cdt,
8 maximum_filter,
9 sobel,
10)
12from nilearn._utils.extmath import fast_abs_percentile
14###############################################################################
15# Edge detection
18def _orientation_kernel(t):
19 """Structure elements for calculating the value of neighbors in several \
20 directions.
21 """
22 sin = np.sin
23 pi = np.pi
24 t = pi * t
25 arr = np.array(
26 [
27 [sin(t), sin(t + 0.5 * pi), sin(t + pi)],
28 [sin(t + 1.5 * pi), 0, sin(t + 1.5 * pi)],
29 [sin(t + pi), sin(t + 0.5 * pi), sin(t)],
30 ]
31 )
32 return np.round(0.5 * (1 + arr) ** 2).astype(bool)
35def _edge_detect(image, high_threshold=0.75, low_threshold=0.4):
36 """Edge detection for 2D images based on Canny filtering.
38 Parameters
39 ----------
40 image : 2D array
41 The image on which edge detection is applied.
43 high_threshold : float, default=0.75
44 The quantile defining the upper threshold of the hysteries.
45 thresholding decrease this to keep more edges.
47 low_threshold : float, default=0.4
48 The quantile defining the lower threshold of the hysteries
49 thresholding decrease this to extract wider edges.
51 Returns
52 -------
53 grad_mag : 2D array of floats
54 The magnitude of the gradient.
56 edge_mask : 2D array of booleans
57 A mask of where have edges been detected.
59 Notes
60 -----
61 This function is based on a Canny filter, however it has been
62 tailored to visualization purposes on brain images: don't use it
63 in the general case.
65 It computes the norm of the gradient, extracts the ridge by
66 keeping only local maximum in each direction, and performs
67 hysteresis filtering to keep only edges with high gradient
68 magnitude.
70 """
71 # This code is loosely based on code by Stefan van der Waalt
72 # Convert to floats to avoid overflows
73 np_err = np.seterr(all="ignore")
74 # Replace NaNs by 0s to avoid meaningless outputs
75 image = np.nan_to_num(image)
76 img = signal.wiener(image.astype(np.float64))
77 np.seterr(**np_err)
78 # Where the noise variance is 0, Wiener can create nans
79 img[np.isnan(img)] = image[np.isnan(img)]
80 img /= img.max()
81 grad_x = sobel(img, mode="constant", axis=0)
82 grad_y = sobel(img, mode="constant", axis=1)
83 grad_mag = np.hypot(grad_x, grad_y)
84 grad_angle = np.arctan2(grad_y, grad_x)
85 # Scale the angles in the range [0, 2]
86 grad_angle = (grad_angle + np.pi) / np.pi
87 # Non-maximal suppression: an edge pixel is only good if its magnitude is
88 # greater than its neighbors normal to the edge direction.
89 thinner = np.zeros(grad_mag.shape, dtype=bool)
90 for angle in np.arange(0, 2, 0.25):
91 thinner = thinner | (
92 (
93 grad_mag
94 > 0.85
95 * maximum_filter(
96 grad_mag, footprint=_orientation_kernel(angle)
97 )
98 )
99 & (((grad_angle - angle) % 2) < 0.75)
100 )
101 # Remove the edges next to the side of the image: they are not reliable
102 thinner[0] = 0
103 thinner[-1] = 0
104 thinner[:, 0] = 0
105 thinner[:, -1] = 0
107 thinned_grad = thinner * grad_mag
108 # Hysteresis thresholding: find seeds above a high threshold, then
109 # expand out until we go below the low threshold
110 grad_values = thinned_grad[thinner]
111 high = thinned_grad > fast_abs_percentile(
112 grad_values, 100 * high_threshold
113 )
114 low = thinned_grad > fast_abs_percentile(grad_values, 100 * low_threshold)
115 edge_mask = binary_dilation(
116 high, structure=np.ones((3, 3)), iterations=-1, mask=low
117 )
118 return grad_mag, edge_mask
121def edge_map(image):
122 """Return a maps of edges suitable for visualization.
124 Parameters
125 ----------
126 image : 2D array
127 The image that the edges are extracted from.
129 Returns
130 -------
131 edge_mask : 2D masked array
132 A mask of the edge as a masked array with parts without
133 edges masked and the large extents detected with lower
134 coefficients.
136 """
137 edge_mask = _edge_detect(image)[-1]
138 edge_mask = edge_mask.astype(float)
139 edge_mask = -np.sqrt(distance_transform_cdt(edge_mask))
140 edge_mask[edge_mask != 0] -= -0.05 + edge_mask.min()
141 edge_mask = np.ma.masked_less(edge_mask, 0.01)
142 return edge_mask