Coverage for nilearn/plotting/edge_detect.py: 0%

42 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-03 10:21 +0200

1"""Edge detection routines: this file provides a Canny filter.""" 

2 

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) 

11 

12from nilearn._utils.extmath import fast_abs_percentile 

13 

14############################################################################### 

15# Edge detection 

16 

17 

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) 

33 

34 

35def _edge_detect(image, high_threshold=0.75, low_threshold=0.4): 

36 """Edge detection for 2D images based on Canny filtering. 

37 

38 Parameters 

39 ---------- 

40 image : 2D array 

41 The image on which edge detection is applied. 

42 

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. 

46 

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. 

50 

51 Returns 

52 ------- 

53 grad_mag : 2D array of floats 

54 The magnitude of the gradient. 

55 

56 edge_mask : 2D array of booleans 

57 A mask of where have edges been detected. 

58 

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. 

64 

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. 

69 

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 

106 

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 

119 

120 

121def edge_map(image): 

122 """Return a maps of edges suitable for visualization. 

123 

124 Parameters 

125 ---------- 

126 image : 2D array 

127 The image that the edges are extracted from. 

128 

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. 

135 

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