|
a |
|
b/blobs_detection.py |
|
|
1 |
from __future__ import division |
|
|
2 |
import numpy as np |
|
|
3 |
from scipy.ndimage import gaussian_filter, gaussian_laplace |
|
|
4 |
import math |
|
|
5 |
from math import sqrt, log |
|
|
6 |
from scipy import spatial |
|
|
7 |
from skimage.util import img_as_float |
|
|
8 |
from skimage.feature.peak import peak_local_max |
|
|
9 |
|
|
|
10 |
|
|
|
11 |
# code from |
|
|
12 |
# https://github.com/emmanuelle/scikit-image/blob/0228772de6f55a053f350665dd3128b1a0193b98/skimage/feature/blob.py |
|
|
13 |
|
|
|
14 |
# This basic blob detection algorithm is based on: |
|
|
15 |
# http://www.cs.utah.edu/~jfishbau/advimproc/project1/ (04.04.2013) |
|
|
16 |
# Theory behind: http://en.wikipedia.org/wiki/Blob_detection (04.04.2013) |
|
|
17 |
|
|
|
18 |
|
|
|
19 |
def _compute_disk_overlap(d, r1, r2): |
|
|
20 |
""" |
|
|
21 |
Compute surface overlap between two disks of radii ``r1`` and ``r2``, |
|
|
22 |
with centers separated by a distance ``d``. |
|
|
23 |
|
|
|
24 |
Parameters |
|
|
25 |
---------- |
|
|
26 |
d : float |
|
|
27 |
Distance between centers. |
|
|
28 |
r1 : float |
|
|
29 |
Radius of the first disk. |
|
|
30 |
r2 : float |
|
|
31 |
Radius of the second disk. |
|
|
32 |
|
|
|
33 |
Returns |
|
|
34 |
------- |
|
|
35 |
vol: float |
|
|
36 |
Volume of the overlap between the two disks. |
|
|
37 |
""" |
|
|
38 |
|
|
|
39 |
ratio1 = (d ** 2 + r1 ** 2 - r2 ** 2) / (2 * d * r1) |
|
|
40 |
ratio1 = np.clip(ratio1, -1, 1) |
|
|
41 |
acos1 = math.acos(ratio1) |
|
|
42 |
|
|
|
43 |
ratio2 = (d ** 2 + r2 ** 2 - r1 ** 2) / (2 * d * r2) |
|
|
44 |
ratio2 = np.clip(ratio2, -1, 1) |
|
|
45 |
acos2 = math.acos(ratio2) |
|
|
46 |
|
|
|
47 |
a = -d + r2 + r1 |
|
|
48 |
b = d - r2 + r1 |
|
|
49 |
c = d + r2 - r1 |
|
|
50 |
d = d + r2 + r1 |
|
|
51 |
area = (r1 ** 2 * acos1 + r2 ** 2 * acos2 - |
|
|
52 |
0.5 * sqrt(abs(a * b * c * d))) |
|
|
53 |
return area / (math.pi * (min(r1, r2) ** 2)) |
|
|
54 |
|
|
|
55 |
|
|
|
56 |
def _compute_sphere_overlap(d, r1, r2): |
|
|
57 |
""" |
|
|
58 |
Compute volume overlap between two spheres of radii ``r1`` and ``r2``, |
|
|
59 |
with centers separated by a distance ``d``. |
|
|
60 |
|
|
|
61 |
Parameters |
|
|
62 |
---------- |
|
|
63 |
d : float |
|
|
64 |
Distance between centers. |
|
|
65 |
r1 : float |
|
|
66 |
Radius of the first sphere. |
|
|
67 |
r2 : float |
|
|
68 |
Radius of the second sphere. |
|
|
69 |
|
|
|
70 |
Returns |
|
|
71 |
------- |
|
|
72 |
vol: float |
|
|
73 |
Volume of the overlap between the two spheres. |
|
|
74 |
|
|
|
75 |
Notes |
|
|
76 |
----- |
|
|
77 |
See for example http://mathworld.wolfram.com/Sphere-SphereIntersection.html |
|
|
78 |
for more details. |
|
|
79 |
""" |
|
|
80 |
vol = (math.pi / (12 * d) * (r1 + r2 - d) ** 2 * |
|
|
81 |
(d ** 2 + 2 * d * (r1 + r2) - 3 * (r1 ** 2 + r2 ** 2) + 6 * r1 * r2)) |
|
|
82 |
return vol / (4. / 3 * math.pi * min(r1, r2) ** 3) |
|
|
83 |
|
|
|
84 |
|
|
|
85 |
def _blob_overlap(blob1, blob2): |
|
|
86 |
"""Finds the overlapping area fraction between two blobs. |
|
|
87 |
|
|
|
88 |
Returns a float representing fraction of overlapped area. |
|
|
89 |
|
|
|
90 |
Parameters |
|
|
91 |
---------- |
|
|
92 |
blob1 : sequence of arrays |
|
|
93 |
A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``, |
|
|
94 |
where ``row, col`` (or ``(pln, row, col, sigma)``) are coordinates |
|
|
95 |
of blob and ``sigma`` is the standard deviation of the Gaussian kernel |
|
|
96 |
which detected the blob. |
|
|
97 |
blob2 : sequence of arrays |
|
|
98 |
A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``, |
|
|
99 |
where ``row, col`` (or ``(pln, row, col, sigma)``) are coordinates |
|
|
100 |
of blob and ``sigma`` is the standard deviation of the Gaussian kernel |
|
|
101 |
which detected the blob. |
|
|
102 |
|
|
|
103 |
Returns |
|
|
104 |
------- |
|
|
105 |
f : float |
|
|
106 |
Fraction of overlapped area (or volume in 3D). |
|
|
107 |
""" |
|
|
108 |
n_dim = len(blob1) - 1 |
|
|
109 |
root_ndim = sqrt(n_dim) |
|
|
110 |
|
|
|
111 |
# extent of the blob is given by sqrt(2)*scale |
|
|
112 |
r1 = blob1[-1] * root_ndim |
|
|
113 |
r2 = blob2[-1] * root_ndim |
|
|
114 |
|
|
|
115 |
d = sqrt(np.sum((blob1[:-1] - blob2[:-1]) ** 2)) |
|
|
116 |
if d > r1 + r2: |
|
|
117 |
return 0 |
|
|
118 |
|
|
|
119 |
# one blob is inside the other, the smaller blob must die |
|
|
120 |
if d <= abs(r1 - r2): |
|
|
121 |
return 1 |
|
|
122 |
|
|
|
123 |
if n_dim == 2: |
|
|
124 |
return _compute_disk_overlap(d, r1, r2) |
|
|
125 |
|
|
|
126 |
else: # http://mathworld.wolfram.com/Sphere-SphereIntersection.html |
|
|
127 |
return _compute_sphere_overlap(d, r1, r2) |
|
|
128 |
|
|
|
129 |
|
|
|
130 |
def _prune_blobs(blobs_array, overlap): |
|
|
131 |
"""Eliminated blobs with area overlap. |
|
|
132 |
|
|
|
133 |
Parameters |
|
|
134 |
---------- |
|
|
135 |
blobs_array : ndarray |
|
|
136 |
A 2d array with each row representing 3 (or 4) values, |
|
|
137 |
``(row, col, sigma)`` or ``(pln, row, col, sigma)`` in 3D, |
|
|
138 |
where ``(row, col)`` (``(pln, row, col)``) are coordinates of the blob |
|
|
139 |
and ``sigma`` is the standard deviation of the Gaussian kernel which |
|
|
140 |
detected the blob. |
|
|
141 |
overlap : float |
|
|
142 |
A value between 0 and 1. If the fraction of area overlapping for 2 |
|
|
143 |
blobs is greater than `overlap` the smaller blob is eliminated. |
|
|
144 |
|
|
|
145 |
Returns |
|
|
146 |
------- |
|
|
147 |
A : ndarray |
|
|
148 |
`array` with overlapping blobs removed. |
|
|
149 |
""" |
|
|
150 |
|
|
|
151 |
# iterating again might eliminate more blobs, but one iteration suffices |
|
|
152 |
# for most cases |
|
|
153 |
if len(blobs_array) == 0: |
|
|
154 |
return np.array([]) |
|
|
155 |
sigma = blobs_array[:, -1].max() |
|
|
156 |
distance = 2 * sigma * sqrt(blobs_array.shape[1] - 1) |
|
|
157 |
try: |
|
|
158 |
tree = spatial.cKDTree(blobs_array[:, :-1]) |
|
|
159 |
pairs = np.array(list(tree.query_pairs(distance))) |
|
|
160 |
except AttributeError: # scipy 0.9, min requirements |
|
|
161 |
tree = spatial.KDTree(blobs_array[:, :-1]) |
|
|
162 |
pairs = np.array(list(tree.query_pairs(distance))) |
|
|
163 |
if len(pairs) == 0: |
|
|
164 |
return blobs_array |
|
|
165 |
else: |
|
|
166 |
for (i, j) in pairs: |
|
|
167 |
blob1, blob2 = blobs_array[i], blobs_array[j] |
|
|
168 |
if _blob_overlap(blob1, blob2) > overlap: |
|
|
169 |
if blob1[-1] > blob2[-1]: |
|
|
170 |
blob2[-1] = 0 |
|
|
171 |
else: |
|
|
172 |
blob1[-1] = 0 |
|
|
173 |
|
|
|
174 |
return np.array([b for b in blobs_array if b[-1] > 0]) |
|
|
175 |
|
|
|
176 |
|
|
|
177 |
def blob_dog(image, min_sigma=1, max_sigma=50, sigma_ratio=1.6, threshold=2.0, |
|
|
178 |
overlap=.5, ): |
|
|
179 |
"""Finds blobs in the given grayscale image. |
|
|
180 |
|
|
|
181 |
Blobs are found using the Difference of Gaussian (DoG) method [1]_. |
|
|
182 |
For each blob found, the method returns its coordinates and the standard |
|
|
183 |
deviation of the Gaussian kernel that detected the blob. |
|
|
184 |
|
|
|
185 |
Parameters |
|
|
186 |
---------- |
|
|
187 |
image : 2D or 3D ndarray |
|
|
188 |
Input grayscale image, blobs are assumed to be light on dark |
|
|
189 |
background (white on black). |
|
|
190 |
min_sigma : float, optional |
|
|
191 |
The minimum standard deviation for Gaussian Kernel. Keep this low to |
|
|
192 |
detect smaller blobs. |
|
|
193 |
max_sigma : float, optional |
|
|
194 |
The maximum standard deviation for Gaussian Kernel. Keep this high to |
|
|
195 |
detect larger blobs. |
|
|
196 |
sigma_ratio : float, optional |
|
|
197 |
The ratio between the standard deviation of Gaussian Kernels used for |
|
|
198 |
computing the Difference of Gaussians |
|
|
199 |
threshold : float, optional. |
|
|
200 |
The absolute lower bound for scale space maxima. Local maxima smaller |
|
|
201 |
than thresh are ignored. Reduce this to detect blobs with less |
|
|
202 |
intensities. |
|
|
203 |
overlap : float, optional |
|
|
204 |
A value between 0 and 1. If the area of two blobs overlaps by a |
|
|
205 |
fraction greater than `threshold`, the smaller blob is eliminated. |
|
|
206 |
|
|
|
207 |
Returns |
|
|
208 |
------- |
|
|
209 |
A : (n, image.ndim + 1) ndarray |
|
|
210 |
A 2d array with each row representing 3 values for a 2D image, |
|
|
211 |
and 4 values for a 3D image: ``(r, c, sigma)`` or ``(p, r, c, sigma)`` |
|
|
212 |
where ``(r, c)`` or ``(p, r, c)`` are coordinates of the blob and |
|
|
213 |
``sigma`` is the standard deviation of the Gaussian kernel which |
|
|
214 |
detected the blob. |
|
|
215 |
|
|
|
216 |
References |
|
|
217 |
---------- |
|
|
218 |
.. [1] http://en.wikipedia.org/wiki/Blob_detection#The_difference_of_Gaussians_approach |
|
|
219 |
|
|
|
220 |
Examples |
|
|
221 |
-------- |
|
|
222 |
>>> from skimage import data, feature |
|
|
223 |
>>> feature.blob_dog(data.coins(), threshold=.5, max_sigma=40) |
|
|
224 |
array([[ 45. , 336. , 16.777216], |
|
|
225 |
[ 52. , 155. , 16.777216], |
|
|
226 |
[ 52. , 216. , 16.777216], |
|
|
227 |
[ 54. , 42. , 16.777216], |
|
|
228 |
[ 54. , 276. , 10.48576 ], |
|
|
229 |
[ 58. , 100. , 10.48576 ], |
|
|
230 |
[ 120. , 272. , 16.777216], |
|
|
231 |
[ 124. , 337. , 10.48576 ], |
|
|
232 |
[ 125. , 45. , 16.777216], |
|
|
233 |
[ 125. , 208. , 10.48576 ], |
|
|
234 |
[ 127. , 102. , 10.48576 ], |
|
|
235 |
[ 128. , 154. , 10.48576 ], |
|
|
236 |
[ 185. , 347. , 16.777216], |
|
|
237 |
[ 193. , 213. , 16.777216], |
|
|
238 |
[ 194. , 277. , 16.777216], |
|
|
239 |
[ 195. , 102. , 16.777216], |
|
|
240 |
[ 196. , 43. , 10.48576 ], |
|
|
241 |
[ 198. , 155. , 10.48576 ], |
|
|
242 |
[ 260. , 46. , 16.777216], |
|
|
243 |
[ 261. , 173. , 16.777216], |
|
|
244 |
[ 263. , 245. , 16.777216], |
|
|
245 |
[ 263. , 302. , 16.777216], |
|
|
246 |
[ 267. , 115. , 10.48576 ], |
|
|
247 |
[ 267. , 359. , 16.777216]]) |
|
|
248 |
|
|
|
249 |
Notes |
|
|
250 |
----- |
|
|
251 |
The radius of each blob is approximately :math:`\sqrt{2}sigma` for |
|
|
252 |
a 2-D image and :math:`\sqrt{3}sigma` for a 3-D image. |
|
|
253 |
""" |
|
|
254 |
image = img_as_float(image) |
|
|
255 |
|
|
|
256 |
# k such that min_sigma*(sigma_ratio**k) > max_sigma |
|
|
257 |
k = int(log(float(max_sigma) / min_sigma, sigma_ratio)) + 1 |
|
|
258 |
|
|
|
259 |
# a geometric progression of standard deviations for gaussian kernels |
|
|
260 |
sigma_list = np.array([min_sigma * (sigma_ratio ** i) |
|
|
261 |
for i in range(k + 1)]) |
|
|
262 |
|
|
|
263 |
gaussian_images = [gaussian_filter(image, s) for s in sigma_list] |
|
|
264 |
|
|
|
265 |
# computing difference between two successive Gaussian blurred images |
|
|
266 |
# multiplying with standard deviation provides scale invariance |
|
|
267 |
dog_images = [(gaussian_images[i] - gaussian_images[i + 1]) |
|
|
268 |
* sigma_list[i] for i in range(k)] |
|
|
269 |
|
|
|
270 |
# Replace by image_cube = np.stack(hessian_images, axis=-1) |
|
|
271 |
# When we upgrade minimal requirements to NumPy 1.10 |
|
|
272 |
sl = (slice(None),) * image.ndim + (np.newaxis,) |
|
|
273 |
arrays = [np.asanyarray(arr) for arr in dog_images] |
|
|
274 |
extended_arrays = [arr[sl] for arr in arrays] |
|
|
275 |
image_cube = np.concatenate(extended_arrays, axis=-1) |
|
|
276 |
|
|
|
277 |
# local_maxima = get_local_maxima(image_cube, threshold) |
|
|
278 |
local_maxima = peak_local_max(image_cube, threshold_abs=threshold, |
|
|
279 |
footprint=np.ones((3,) * (image.ndim + 1)), |
|
|
280 |
threshold_rel=0.0, |
|
|
281 |
exclude_border=False) |
|
|
282 |
# Convert local_maxima to float64 |
|
|
283 |
lm = local_maxima.astype(np.float64) |
|
|
284 |
# Convert the last index to its corresponding scale value |
|
|
285 |
lm[:, -1] = sigma_list[local_maxima[:, -1]] |
|
|
286 |
return _prune_blobs(lm, overlap) |
|
|
287 |
|
|
|
288 |
|
|
|
289 |
def blob_log(image, min_sigma=1, max_sigma=50, num_sigma=10, threshold=.2, |
|
|
290 |
overlap=.5, log_scale=False): |
|
|
291 |
"""Finds blobs in the given grayscale image. |
|
|
292 |
|
|
|
293 |
Blobs are found using the Laplacian of Gaussian (LoG) method [1]_. |
|
|
294 |
For each blob found, the method returns its coordinates and the standard |
|
|
295 |
deviation of the Gaussian kernel that detected the blob. |
|
|
296 |
|
|
|
297 |
Parameters |
|
|
298 |
---------- |
|
|
299 |
image : 2D or 3D ndarray |
|
|
300 |
Input grayscale image, blobs are assumed to be light on dark |
|
|
301 |
background (white on black). |
|
|
302 |
min_sigma : float, optional |
|
|
303 |
The minimum standard deviation for Gaussian Kernel. Keep this low to |
|
|
304 |
detect smaller blobs. |
|
|
305 |
max_sigma : float, optional |
|
|
306 |
The maximum standard deviation for Gaussian Kernel. Keep this high to |
|
|
307 |
detect larger blobs. |
|
|
308 |
num_sigma : int, optional |
|
|
309 |
The number of intermediate values of standard deviations to consider |
|
|
310 |
between `min_sigma` and `max_sigma`. |
|
|
311 |
threshold : float, optional. |
|
|
312 |
The absolute lower bound for scale space maxima. Local maxima smaller |
|
|
313 |
than thresh are ignored. Reduce this to detect blobs with less |
|
|
314 |
intensities. |
|
|
315 |
overlap : float, optional |
|
|
316 |
A value between 0 and 1. If the area of two blobs overlaps by a |
|
|
317 |
fraction greater than `threshold`, the smaller blob is eliminated. |
|
|
318 |
log_scale : bool, optional |
|
|
319 |
If set intermediate values of standard deviations are interpolated |
|
|
320 |
using a logarithmic scale to the base `10`. If not, linear |
|
|
321 |
interpolation is used. |
|
|
322 |
|
|
|
323 |
Returns |
|
|
324 |
------- |
|
|
325 |
A : (n, image.ndim + 1) ndarray |
|
|
326 |
A 2d array with each row representing 3 values for a 2D image, |
|
|
327 |
and 4 values for a 3D image: ``(r, c, sigma)`` or ``(f, r, c, sigma)`` |
|
|
328 |
where ``(r, c)`` or ``(f, r, c)`` are coordinates of the blob and |
|
|
329 |
``sigma`` is the standard deviation of the Gaussian kernel which |
|
|
330 |
detected the blob. |
|
|
331 |
|
|
|
332 |
References |
|
|
333 |
---------- |
|
|
334 |
.. [1] http://en.wikipedia.org/wiki/Blob_detection#The_Laplacian_of_Gaussian |
|
|
335 |
|
|
|
336 |
Examples |
|
|
337 |
-------- |
|
|
338 |
>>> from skimage import data, feature, exposure |
|
|
339 |
>>> img = data.coins() |
|
|
340 |
>>> img = exposure.equalize_hist(img) # improves detection |
|
|
341 |
>>> feature.blob_log(img, threshold = .3) |
|
|
342 |
array([[ 113. , 323. , 1. ], |
|
|
343 |
[ 121. , 272. , 17.33333333], |
|
|
344 |
[ 124. , 336. , 11.88888889], |
|
|
345 |
[ 126. , 46. , 11.88888889], |
|
|
346 |
[ 126. , 208. , 11.88888889], |
|
|
347 |
[ 127. , 102. , 11.88888889], |
|
|
348 |
[ 128. , 154. , 11.88888889], |
|
|
349 |
[ 185. , 344. , 17.33333333], |
|
|
350 |
[ 194. , 213. , 17.33333333], |
|
|
351 |
[ 194. , 276. , 17.33333333], |
|
|
352 |
[ 197. , 44. , 11.88888889], |
|
|
353 |
[ 198. , 103. , 11.88888889], |
|
|
354 |
[ 198. , 155. , 11.88888889], |
|
|
355 |
[ 260. , 174. , 17.33333333], |
|
|
356 |
[ 263. , 244. , 17.33333333], |
|
|
357 |
[ 263. , 302. , 17.33333333], |
|
|
358 |
[ 266. , 115. , 11.88888889]]) |
|
|
359 |
|
|
|
360 |
Notes |
|
|
361 |
----- |
|
|
362 |
The radius of each blob is approximately :math:`\sqrt{2}sigma` for |
|
|
363 |
a 2-D image and :math:`\sqrt{3}sigma` for a 3-D image. |
|
|
364 |
""" |
|
|
365 |
image = img_as_float(image) |
|
|
366 |
|
|
|
367 |
if log_scale: |
|
|
368 |
start, stop = log(min_sigma, 10), log(max_sigma, 10) |
|
|
369 |
sigma_list = np.logspace(start, stop, num_sigma) |
|
|
370 |
else: |
|
|
371 |
sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) |
|
|
372 |
|
|
|
373 |
# computing gaussian laplace |
|
|
374 |
# s**2 provides scale invariance |
|
|
375 |
gl_images = [-gaussian_laplace(image, s) * s ** 2 for s in sigma_list] |
|
|
376 |
|
|
|
377 |
# Replace by image_cube = np.stack(hessian_images, axis=-1) |
|
|
378 |
# When we upgrade minimal requirements to NumPy 1.10 |
|
|
379 |
sl = (slice(None),) * image.ndim + (np.newaxis,) |
|
|
380 |
arrays = [np.asanyarray(arr) for arr in gl_images] |
|
|
381 |
extended_arrays = [arr[sl] for arr in arrays] |
|
|
382 |
image_cube = np.concatenate(extended_arrays, axis=-1) |
|
|
383 |
|
|
|
384 |
local_maxima = peak_local_max(image_cube, threshold_abs=threshold, |
|
|
385 |
footprint=np.ones((3,) * (image.ndim + 1)), |
|
|
386 |
threshold_rel=0.0, |
|
|
387 |
exclude_border=False) |
|
|
388 |
|
|
|
389 |
# Convert local_maxima to float64 |
|
|
390 |
lm = local_maxima.astype(np.float64) |
|
|
391 |
# Convert the last index to its corresponding scale value |
|
|
392 |
lm[:, -1] = sigma_list[local_maxima[:, -1]] |
|
|
393 |
return _prune_blobs(lm, overlap) |