|
a |
|
b/metric_eval.py |
|
|
1 |
# Copyright (C) 2013 Oskar Maier |
|
|
2 |
# |
|
|
3 |
# This program is free software: you can redistribute it and/or modify |
|
|
4 |
# it under the terms of the GNU General Public License as published by |
|
|
5 |
# the Free Software Foundation, either version 3 of the License, or |
|
|
6 |
# (at your option) any later version. |
|
|
7 |
# |
|
|
8 |
# This program is distributed in the hope that it will be useful, |
|
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
11 |
# GNU General Public License for more details. |
|
|
12 |
# |
|
|
13 |
# You should have received a copy of the GNU General Public License |
|
|
14 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
15 |
# |
|
|
16 |
# author Oskar Maier |
|
|
17 |
# version r0.1.1 |
|
|
18 |
# since 2014-03-13 |
|
|
19 |
# status Release |
|
|
20 |
|
|
|
21 |
# build-in modules |
|
|
22 |
|
|
|
23 |
# third-party modules |
|
|
24 |
import numpy |
|
|
25 |
from scipy.ndimage import _ni_support |
|
|
26 |
from scipy.ndimage.morphology import distance_transform_edt, binary_erosion,\ |
|
|
27 |
generate_binary_structure |
|
|
28 |
from scipy.ndimage.measurements import label, find_objects |
|
|
29 |
from scipy.stats import pearsonr |
|
|
30 |
|
|
|
31 |
# own modules |
|
|
32 |
|
|
|
33 |
# code |
|
|
34 |
def dc(result, reference): |
|
|
35 |
r""" |
|
|
36 |
Dice coefficient |
|
|
37 |
|
|
|
38 |
Computes the Dice coefficient (also known as Sorensen index) between the binary |
|
|
39 |
objects in two images. |
|
|
40 |
|
|
|
41 |
The metric is defined as |
|
|
42 |
|
|
|
43 |
.. math:: |
|
|
44 |
|
|
|
45 |
DC=\frac{2|A\cap B|}{|A|+|B|} |
|
|
46 |
|
|
|
47 |
, where :math:`A` is the first and :math:`B` the second set of samples (here: binary objects). |
|
|
48 |
|
|
|
49 |
Parameters |
|
|
50 |
---------- |
|
|
51 |
result : array_like |
|
|
52 |
Input data containing objects. Can be any type but will be converted |
|
|
53 |
into binary: background where 0, object everywhere else. |
|
|
54 |
reference : array_like |
|
|
55 |
Input data containing objects. Can be any type but will be converted |
|
|
56 |
into binary: background where 0, object everywhere else. |
|
|
57 |
|
|
|
58 |
Returns |
|
|
59 |
------- |
|
|
60 |
dc : float |
|
|
61 |
The Dice coefficient between the object(s) in ```result``` and the |
|
|
62 |
object(s) in ```reference```. It ranges from 0 (no overlap) to 1 (perfect overlap). |
|
|
63 |
|
|
|
64 |
Notes |
|
|
65 |
----- |
|
|
66 |
This is a real metric. The binary images can therefore be supplied in any order. |
|
|
67 |
""" |
|
|
68 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
69 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
70 |
|
|
|
71 |
intersection = numpy.count_nonzero(result & reference) |
|
|
72 |
|
|
|
73 |
size_i1 = numpy.count_nonzero(result) |
|
|
74 |
size_i2 = numpy.count_nonzero(reference) |
|
|
75 |
|
|
|
76 |
try: |
|
|
77 |
dc = 2. * intersection / float(size_i1 + size_i2) |
|
|
78 |
except ZeroDivisionError: |
|
|
79 |
dc = 0.0 |
|
|
80 |
|
|
|
81 |
return dc |
|
|
82 |
|
|
|
83 |
def jc(result, reference): |
|
|
84 |
""" |
|
|
85 |
Jaccard coefficient |
|
|
86 |
|
|
|
87 |
Computes the Jaccard coefficient between the binary objects in two images. |
|
|
88 |
|
|
|
89 |
Parameters |
|
|
90 |
---------- |
|
|
91 |
result: array_like |
|
|
92 |
Input data containing objects. Can be any type but will be converted |
|
|
93 |
into binary: background where 0, object everywhere else. |
|
|
94 |
reference: array_like |
|
|
95 |
Input data containing objects. Can be any type but will be converted |
|
|
96 |
into binary: background where 0, object everywhere else. |
|
|
97 |
Returns |
|
|
98 |
------- |
|
|
99 |
jc: float |
|
|
100 |
The Jaccard coefficient between the object(s) in `result` and the |
|
|
101 |
object(s) in `reference`. It ranges from 0 (no overlap) to 1 (perfect overlap). |
|
|
102 |
|
|
|
103 |
Notes |
|
|
104 |
----- |
|
|
105 |
This is a real metric. The binary images can therefore be supplied in any order. |
|
|
106 |
""" |
|
|
107 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
108 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
109 |
|
|
|
110 |
intersection = numpy.count_nonzero(result & reference) |
|
|
111 |
union = numpy.count_nonzero(result | reference) |
|
|
112 |
|
|
|
113 |
jc = float(intersection) / float(union) |
|
|
114 |
|
|
|
115 |
return jc |
|
|
116 |
|
|
|
117 |
def precision(result, reference): |
|
|
118 |
""" |
|
|
119 |
Precison. |
|
|
120 |
|
|
|
121 |
Parameters |
|
|
122 |
---------- |
|
|
123 |
result : array_like |
|
|
124 |
Input data containing objects. Can be any type but will be converted |
|
|
125 |
into binary: background where 0, object everywhere else. |
|
|
126 |
reference : array_like |
|
|
127 |
Input data containing objects. Can be any type but will be converted |
|
|
128 |
into binary: background where 0, object everywhere else. |
|
|
129 |
|
|
|
130 |
Returns |
|
|
131 |
------- |
|
|
132 |
precision : float |
|
|
133 |
The precision between two binary datasets, here mostly binary objects in images, |
|
|
134 |
which is defined as the fraction of retrieved instances that are relevant. The |
|
|
135 |
precision is not symmetric. |
|
|
136 |
|
|
|
137 |
See also |
|
|
138 |
-------- |
|
|
139 |
:func:`recall` |
|
|
140 |
|
|
|
141 |
Notes |
|
|
142 |
----- |
|
|
143 |
Not symmetric. The inverse of the precision is :func:`recall`. |
|
|
144 |
High precision means that an algorithm returned substantially more relevant results than irrelevant. |
|
|
145 |
|
|
|
146 |
References |
|
|
147 |
---------- |
|
|
148 |
.. [1] http://en.wikipedia.org/wiki/Precision_and_recall |
|
|
149 |
.. [2] http://en.wikipedia.org/wiki/Confusion_matrix#Table_of_confusion |
|
|
150 |
""" |
|
|
151 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
152 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
153 |
|
|
|
154 |
tp = numpy.count_nonzero(result & reference) |
|
|
155 |
fp = numpy.count_nonzero(result & ~reference) |
|
|
156 |
|
|
|
157 |
try: |
|
|
158 |
precision = tp / float(tp + fp) |
|
|
159 |
except ZeroDivisionError: |
|
|
160 |
precision = 0.0 |
|
|
161 |
|
|
|
162 |
return precision |
|
|
163 |
|
|
|
164 |
def recall(result, reference): |
|
|
165 |
""" |
|
|
166 |
Recall. |
|
|
167 |
|
|
|
168 |
Parameters |
|
|
169 |
---------- |
|
|
170 |
result : array_like |
|
|
171 |
Input data containing objects. Can be any type but will be converted |
|
|
172 |
into binary: background where 0, object everywhere else. |
|
|
173 |
reference : array_like |
|
|
174 |
Input data containing objects. Can be any type but will be converted |
|
|
175 |
into binary: background where 0, object everywhere else. |
|
|
176 |
|
|
|
177 |
Returns |
|
|
178 |
------- |
|
|
179 |
recall : float |
|
|
180 |
The recall between two binary datasets, here mostly binary objects in images, |
|
|
181 |
which is defined as the fraction of relevant instances that are retrieved. The |
|
|
182 |
recall is not symmetric. |
|
|
183 |
|
|
|
184 |
See also |
|
|
185 |
-------- |
|
|
186 |
:func:`precision` |
|
|
187 |
|
|
|
188 |
Notes |
|
|
189 |
----- |
|
|
190 |
Not symmetric. The inverse of the recall is :func:`precision`. |
|
|
191 |
High recall means that an algorithm returned most of the relevant results. |
|
|
192 |
|
|
|
193 |
References |
|
|
194 |
---------- |
|
|
195 |
.. [1] http://en.wikipedia.org/wiki/Precision_and_recall |
|
|
196 |
.. [2] http://en.wikipedia.org/wiki/Confusion_matrix#Table_of_confusion |
|
|
197 |
""" |
|
|
198 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
199 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
200 |
|
|
|
201 |
tp = numpy.count_nonzero(result & reference) |
|
|
202 |
fn = numpy.count_nonzero(~result & reference) |
|
|
203 |
|
|
|
204 |
try: |
|
|
205 |
recall = tp / float(tp + fn) |
|
|
206 |
except ZeroDivisionError: |
|
|
207 |
recall = 0.0 |
|
|
208 |
|
|
|
209 |
return recall |
|
|
210 |
|
|
|
211 |
def sensitivity(result, reference): |
|
|
212 |
""" |
|
|
213 |
Sensitivity. |
|
|
214 |
Same as :func:`recall`, see there for a detailed description. |
|
|
215 |
|
|
|
216 |
See also |
|
|
217 |
-------- |
|
|
218 |
:func:`specificity` |
|
|
219 |
""" |
|
|
220 |
return recall(result, reference) |
|
|
221 |
|
|
|
222 |
def specificity(result, reference): |
|
|
223 |
""" |
|
|
224 |
Specificity. |
|
|
225 |
|
|
|
226 |
Parameters |
|
|
227 |
---------- |
|
|
228 |
result : array_like |
|
|
229 |
Input data containing objects. Can be any type but will be converted |
|
|
230 |
into binary: background where 0, object everywhere else. |
|
|
231 |
reference : array_like |
|
|
232 |
Input data containing objects. Can be any type but will be converted |
|
|
233 |
into binary: background where 0, object everywhere else. |
|
|
234 |
|
|
|
235 |
Returns |
|
|
236 |
------- |
|
|
237 |
specificity : float |
|
|
238 |
The specificity between two binary datasets, here mostly binary objects in images, |
|
|
239 |
which denotes the fraction of correctly returned negatives. The |
|
|
240 |
specificity is not symmetric. |
|
|
241 |
|
|
|
242 |
See also |
|
|
243 |
-------- |
|
|
244 |
:func:`sensitivity` |
|
|
245 |
|
|
|
246 |
Notes |
|
|
247 |
----- |
|
|
248 |
Not symmetric. The completment of the specificity is :func:`sensitivity`. |
|
|
249 |
High recall means that an algorithm returned most of the irrelevant results. |
|
|
250 |
|
|
|
251 |
References |
|
|
252 |
---------- |
|
|
253 |
.. [1] https://en.wikipedia.org/wiki/Sensitivity_and_specificity |
|
|
254 |
.. [2] http://en.wikipedia.org/wiki/Confusion_matrix#Table_of_confusion |
|
|
255 |
""" |
|
|
256 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
257 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
258 |
|
|
|
259 |
tn = numpy.count_nonzero(~result & ~reference) |
|
|
260 |
fp = numpy.count_nonzero(result & ~reference) |
|
|
261 |
|
|
|
262 |
try: |
|
|
263 |
specificity = tn / float(tn + fp) |
|
|
264 |
except ZeroDivisionError: |
|
|
265 |
specificity = 0.0 |
|
|
266 |
|
|
|
267 |
return specificity |
|
|
268 |
|
|
|
269 |
def true_negative_rate(result, reference): |
|
|
270 |
""" |
|
|
271 |
True negative rate. |
|
|
272 |
Same as :func:`sensitivity`, see there for a detailed description. |
|
|
273 |
|
|
|
274 |
See also |
|
|
275 |
-------- |
|
|
276 |
:func:`true_positive_rate` |
|
|
277 |
:func:`positive_predictive_value` |
|
|
278 |
""" |
|
|
279 |
return sensitivity(result, reference) |
|
|
280 |
|
|
|
281 |
def true_positive_rate(result, reference): |
|
|
282 |
""" |
|
|
283 |
True positive rate. |
|
|
284 |
Same as :func:`recall`, see there for a detailed description. |
|
|
285 |
|
|
|
286 |
See also |
|
|
287 |
-------- |
|
|
288 |
:func:`positive_predictive_value` |
|
|
289 |
:func:`true_negative_rate` |
|
|
290 |
""" |
|
|
291 |
return recall(result, reference) |
|
|
292 |
|
|
|
293 |
def positive_predictive_value(result, reference): |
|
|
294 |
""" |
|
|
295 |
Positive predictive value. |
|
|
296 |
Same as :func:`precision`, see there for a detailed description. |
|
|
297 |
|
|
|
298 |
See also |
|
|
299 |
-------- |
|
|
300 |
:func:`true_positive_rate` |
|
|
301 |
:func:`true_negative_rate` |
|
|
302 |
""" |
|
|
303 |
return precision(result, reference) |
|
|
304 |
|
|
|
305 |
def hd(result, reference, voxelspacing=None, connectivity=1): |
|
|
306 |
""" |
|
|
307 |
Hausdorff Distance. |
|
|
308 |
|
|
|
309 |
Computes the (symmetric) Hausdorff Distance (HD) between the binary objects in two |
|
|
310 |
images. It is defined as the maximum surface distance between the objects. |
|
|
311 |
|
|
|
312 |
Parameters |
|
|
313 |
---------- |
|
|
314 |
result : array_like |
|
|
315 |
Input data containing objects. Can be any type but will be converted |
|
|
316 |
into binary: background where 0, object everywhere else. |
|
|
317 |
reference : array_like |
|
|
318 |
Input data containing objects. Can be any type but will be converted |
|
|
319 |
into binary: background where 0, object everywhere else. |
|
|
320 |
voxelspacing : float or sequence of floats, optional |
|
|
321 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
322 |
along each dimension. If a sequence, must be of length equal to |
|
|
323 |
the input rank; if a single number, this is used for all axes. If |
|
|
324 |
not specified, a grid spacing of unity is implied. |
|
|
325 |
connectivity : int |
|
|
326 |
The neighbourhood/connectivity considered when determining the surface |
|
|
327 |
of the binary objects. This value is passed to |
|
|
328 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
329 |
Note that the connectivity influences the result in the case of the Hausdorff distance. |
|
|
330 |
|
|
|
331 |
Returns |
|
|
332 |
------- |
|
|
333 |
hd : float |
|
|
334 |
The symmetric Hausdorff Distance between the object(s) in ```result``` and the |
|
|
335 |
object(s) in ```reference```. The distance unit is the same as for the spacing of |
|
|
336 |
elements along each dimension, which is usually given in mm. |
|
|
337 |
|
|
|
338 |
See also |
|
|
339 |
-------- |
|
|
340 |
:func:`assd` |
|
|
341 |
:func:`asd` |
|
|
342 |
|
|
|
343 |
Notes |
|
|
344 |
----- |
|
|
345 |
This is a real metric. The binary images can therefore be supplied in any order. |
|
|
346 |
""" |
|
|
347 |
hd1 = __surface_distances(result, reference, voxelspacing, connectivity).max() |
|
|
348 |
hd2 = __surface_distances(reference, result, voxelspacing, connectivity).max() |
|
|
349 |
hd = max(hd1, hd2) |
|
|
350 |
return hd |
|
|
351 |
|
|
|
352 |
|
|
|
353 |
def hd95(result, reference, voxelspacing=None, connectivity=1): |
|
|
354 |
""" |
|
|
355 |
95th percentile of the Hausdorff Distance. |
|
|
356 |
Computes the 95th percentile of the (symmetric) Hausdorff Distance (HD) between the binary objects in two |
|
|
357 |
images. Compared to the Hausdorff Distance, this metric is slightly more stable to small outliers and is |
|
|
358 |
commonly used in Biomedical Segmentation challenges. |
|
|
359 |
Parameters |
|
|
360 |
---------- |
|
|
361 |
result : array_like |
|
|
362 |
Input data containing objects. Can be any type but will be converted |
|
|
363 |
into binary: background where 0, object everywhere else. |
|
|
364 |
reference : array_like |
|
|
365 |
Input data containing objects. Can be any type but will be converted |
|
|
366 |
into binary: background where 0, object everywhere else. |
|
|
367 |
voxelspacing : float or sequence of floats, optional |
|
|
368 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
369 |
along each dimension. If a sequence, must be of length equal to |
|
|
370 |
the input rank; if a single number, this is used for all axes. If |
|
|
371 |
not specified, a grid spacing of unity is implied. |
|
|
372 |
connectivity : int |
|
|
373 |
The neighbourhood/connectivity considered when determining the surface |
|
|
374 |
of the binary objects. This value is passed to |
|
|
375 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
376 |
Note that the connectivity influences the result in the case of the Hausdorff distance. |
|
|
377 |
Returns |
|
|
378 |
------- |
|
|
379 |
hd : float |
|
|
380 |
The symmetric Hausdorff Distance between the object(s) in ```result``` and the |
|
|
381 |
object(s) in ```reference```. The distance unit is the same as for the spacing of |
|
|
382 |
elements along each dimension, which is usually given in mm. |
|
|
383 |
See also |
|
|
384 |
-------- |
|
|
385 |
:func:`hd` |
|
|
386 |
Notes |
|
|
387 |
----- |
|
|
388 |
This is a real metric. The binary images can therefore be supplied in any order. |
|
|
389 |
""" |
|
|
390 |
hd1 = __surface_distances(result, reference, voxelspacing, connectivity) |
|
|
391 |
hd2 = __surface_distances(reference, result, voxelspacing, connectivity) |
|
|
392 |
hd95 = numpy.percentile(numpy.hstack((hd1, hd2)), 95) |
|
|
393 |
return hd95 |
|
|
394 |
|
|
|
395 |
|
|
|
396 |
def assd(result, reference, voxelspacing=None, connectivity=1): |
|
|
397 |
""" |
|
|
398 |
Average symmetric surface distance. |
|
|
399 |
|
|
|
400 |
Computes the average symmetric surface distance (ASD) between the binary objects in |
|
|
401 |
two images. |
|
|
402 |
|
|
|
403 |
Parameters |
|
|
404 |
---------- |
|
|
405 |
result : array_like |
|
|
406 |
Input data containing objects. Can be any type but will be converted |
|
|
407 |
into binary: background where 0, object everywhere else. |
|
|
408 |
reference : array_like |
|
|
409 |
Input data containing objects. Can be any type but will be converted |
|
|
410 |
into binary: background where 0, object everywhere else. |
|
|
411 |
voxelspacing : float or sequence of floats, optional |
|
|
412 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
413 |
along each dimension. If a sequence, must be of length equal to |
|
|
414 |
the input rank; if a single number, this is used for all axes. If |
|
|
415 |
not specified, a grid spacing of unity is implied. |
|
|
416 |
connectivity : int |
|
|
417 |
The neighbourhood/connectivity considered when determining the surface |
|
|
418 |
of the binary objects. This value is passed to |
|
|
419 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
420 |
The decision on the connectivity is important, as it can influence the results |
|
|
421 |
strongly. If in doubt, leave it as it is. |
|
|
422 |
|
|
|
423 |
Returns |
|
|
424 |
------- |
|
|
425 |
assd : float |
|
|
426 |
The average symmetric surface distance between the object(s) in ``result`` and the |
|
|
427 |
object(s) in ``reference``. The distance unit is the same as for the spacing of |
|
|
428 |
elements along each dimension, which is usually given in mm. |
|
|
429 |
|
|
|
430 |
See also |
|
|
431 |
-------- |
|
|
432 |
:func:`asd` |
|
|
433 |
:func:`hd` |
|
|
434 |
|
|
|
435 |
Notes |
|
|
436 |
----- |
|
|
437 |
This is a real metric, obtained by calling and averaging |
|
|
438 |
|
|
|
439 |
>>> asd(result, reference) |
|
|
440 |
|
|
|
441 |
and |
|
|
442 |
|
|
|
443 |
>>> asd(reference, result) |
|
|
444 |
|
|
|
445 |
The binary images can therefore be supplied in any order. |
|
|
446 |
""" |
|
|
447 |
assd = numpy.mean( (asd(result, reference, voxelspacing, connectivity), asd(reference, result, voxelspacing, connectivity)) ) |
|
|
448 |
return assd |
|
|
449 |
|
|
|
450 |
def asd(result, reference, voxelspacing=None, connectivity=1): |
|
|
451 |
""" |
|
|
452 |
Average surface distance metric. |
|
|
453 |
|
|
|
454 |
Computes the average surface distance (ASD) between the binary objects in two images. |
|
|
455 |
|
|
|
456 |
Parameters |
|
|
457 |
---------- |
|
|
458 |
result : array_like |
|
|
459 |
Input data containing objects. Can be any type but will be converted |
|
|
460 |
into binary: background where 0, object everywhere else. |
|
|
461 |
reference : array_like |
|
|
462 |
Input data containing objects. Can be any type but will be converted |
|
|
463 |
into binary: background where 0, object everywhere else. |
|
|
464 |
voxelspacing : float or sequence of floats, optional |
|
|
465 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
466 |
along each dimension. If a sequence, must be of length equal to |
|
|
467 |
the input rank; if a single number, this is used for all axes. If |
|
|
468 |
not specified, a grid spacing of unity is implied. |
|
|
469 |
connectivity : int |
|
|
470 |
The neighbourhood/connectivity considered when determining the surface |
|
|
471 |
of the binary objects. This value is passed to |
|
|
472 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
473 |
The decision on the connectivity is important, as it can influence the results |
|
|
474 |
strongly. If in doubt, leave it as it is. |
|
|
475 |
|
|
|
476 |
Returns |
|
|
477 |
------- |
|
|
478 |
asd : float |
|
|
479 |
The average surface distance between the object(s) in ``result`` and the |
|
|
480 |
object(s) in ``reference``. The distance unit is the same as for the spacing |
|
|
481 |
of elements along each dimension, which is usually given in mm. |
|
|
482 |
|
|
|
483 |
See also |
|
|
484 |
-------- |
|
|
485 |
:func:`assd` |
|
|
486 |
:func:`hd` |
|
|
487 |
|
|
|
488 |
|
|
|
489 |
Notes |
|
|
490 |
----- |
|
|
491 |
This is not a real metric, as it is directed. See `assd` for a real metric of this. |
|
|
492 |
|
|
|
493 |
The method is implemented making use of distance images and simple binary morphology |
|
|
494 |
to achieve high computational speed. |
|
|
495 |
|
|
|
496 |
Examples |
|
|
497 |
-------- |
|
|
498 |
The `connectivity` determines what pixels/voxels are considered the surface of a |
|
|
499 |
binary object. Take the following binary image showing a cross |
|
|
500 |
|
|
|
501 |
>>> from scipy.ndimage.morphology import generate_binary_structure |
|
|
502 |
>>> cross = generate_binary_structure(2, 1) |
|
|
503 |
array([[0, 1, 0], |
|
|
504 |
[1, 1, 1], |
|
|
505 |
[0, 1, 0]]) |
|
|
506 |
|
|
|
507 |
With `connectivity` set to `1` a 4-neighbourhood is considered when determining the |
|
|
508 |
object surface, resulting in the surface |
|
|
509 |
|
|
|
510 |
.. code-block:: python |
|
|
511 |
|
|
|
512 |
array([[0, 1, 0], |
|
|
513 |
[1, 0, 1], |
|
|
514 |
[0, 1, 0]]) |
|
|
515 |
|
|
|
516 |
Changing `connectivity` to `2`, a 8-neighbourhood is considered and we get: |
|
|
517 |
|
|
|
518 |
.. code-block:: python |
|
|
519 |
|
|
|
520 |
array([[0, 1, 0], |
|
|
521 |
[1, 1, 1], |
|
|
522 |
[0, 1, 0]]) |
|
|
523 |
|
|
|
524 |
, as a diagonal connection does no longer qualifies as valid object surface. |
|
|
525 |
|
|
|
526 |
This influences the results `asd` returns. Imagine we want to compute the surface |
|
|
527 |
distance of our cross to a cube-like object: |
|
|
528 |
|
|
|
529 |
>>> cube = generate_binary_structure(2, 1) |
|
|
530 |
array([[1, 1, 1], |
|
|
531 |
[1, 1, 1], |
|
|
532 |
[1, 1, 1]]) |
|
|
533 |
|
|
|
534 |
, which surface is, independent of the `connectivity` value set, always |
|
|
535 |
|
|
|
536 |
.. code-block:: python |
|
|
537 |
|
|
|
538 |
array([[1, 1, 1], |
|
|
539 |
[1, 0, 1], |
|
|
540 |
[1, 1, 1]]) |
|
|
541 |
|
|
|
542 |
Using a `connectivity` of `1` we get |
|
|
543 |
|
|
|
544 |
>>> asd(cross, cube, connectivity=1) |
|
|
545 |
0.0 |
|
|
546 |
|
|
|
547 |
while a value of `2` returns us |
|
|
548 |
|
|
|
549 |
>>> asd(cross, cube, connectivity=2) |
|
|
550 |
0.20000000000000001 |
|
|
551 |
|
|
|
552 |
due to the center of the cross being considered surface as well. |
|
|
553 |
|
|
|
554 |
""" |
|
|
555 |
sds = __surface_distances(result, reference, voxelspacing, connectivity) |
|
|
556 |
asd = sds.mean() |
|
|
557 |
return asd |
|
|
558 |
|
|
|
559 |
def ravd(result, reference): |
|
|
560 |
""" |
|
|
561 |
Relative absolute volume difference. |
|
|
562 |
|
|
|
563 |
Compute the relative absolute volume difference between the (joined) binary objects |
|
|
564 |
in the two images. |
|
|
565 |
|
|
|
566 |
Parameters |
|
|
567 |
---------- |
|
|
568 |
result : array_like |
|
|
569 |
Input data containing objects. Can be any type but will be converted |
|
|
570 |
into binary: background where 0, object everywhere else. |
|
|
571 |
reference : array_like |
|
|
572 |
Input data containing objects. Can be any type but will be converted |
|
|
573 |
into binary: background where 0, object everywhere else. |
|
|
574 |
|
|
|
575 |
Returns |
|
|
576 |
------- |
|
|
577 |
ravd : float |
|
|
578 |
The relative absolute volume difference between the object(s) in ``result`` |
|
|
579 |
and the object(s) in ``reference``. This is a percentage value in the range |
|
|
580 |
:math:`[-1.0, +inf]` for which a :math:`0` denotes an ideal score. |
|
|
581 |
|
|
|
582 |
Raises |
|
|
583 |
------ |
|
|
584 |
RuntimeError |
|
|
585 |
If the reference object is empty. |
|
|
586 |
|
|
|
587 |
See also |
|
|
588 |
-------- |
|
|
589 |
:func:`dc` |
|
|
590 |
:func:`precision` |
|
|
591 |
:func:`recall` |
|
|
592 |
|
|
|
593 |
Notes |
|
|
594 |
----- |
|
|
595 |
This is not a real metric, as it is directed. Negative values denote a smaller |
|
|
596 |
and positive values a larger volume than the reference. |
|
|
597 |
This implementation does not check, whether the two supplied arrays are of the same |
|
|
598 |
size. |
|
|
599 |
|
|
|
600 |
Examples |
|
|
601 |
-------- |
|
|
602 |
Considering the following inputs |
|
|
603 |
|
|
|
604 |
>>> import numpy |
|
|
605 |
>>> arr1 = numpy.asarray([[0,1,0],[1,1,1],[0,1,0]]) |
|
|
606 |
>>> arr1 |
|
|
607 |
array([[0, 1, 0], |
|
|
608 |
[1, 1, 1], |
|
|
609 |
[0, 1, 0]]) |
|
|
610 |
>>> arr2 = numpy.asarray([[0,1,0],[1,0,1],[0,1,0]]) |
|
|
611 |
>>> arr2 |
|
|
612 |
array([[0, 1, 0], |
|
|
613 |
[1, 0, 1], |
|
|
614 |
[0, 1, 0]]) |
|
|
615 |
|
|
|
616 |
comparing `arr1` to `arr2` we get |
|
|
617 |
|
|
|
618 |
>>> ravd(arr1, arr2) |
|
|
619 |
-0.2 |
|
|
620 |
|
|
|
621 |
and reversing the inputs the directivness of the metric becomes evident |
|
|
622 |
|
|
|
623 |
>>> ravd(arr2, arr1) |
|
|
624 |
0.25 |
|
|
625 |
|
|
|
626 |
It is important to keep in mind that a perfect score of `0` does not mean that the |
|
|
627 |
binary objects fit exactely, as only the volumes are compared: |
|
|
628 |
|
|
|
629 |
>>> arr1 = numpy.asarray([1,0,0]) |
|
|
630 |
>>> arr2 = numpy.asarray([0,0,1]) |
|
|
631 |
>>> ravd(arr1, arr2) |
|
|
632 |
0.0 |
|
|
633 |
|
|
|
634 |
""" |
|
|
635 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
636 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
637 |
|
|
|
638 |
vol1 = numpy.count_nonzero(result) |
|
|
639 |
vol2 = numpy.count_nonzero(reference) |
|
|
640 |
|
|
|
641 |
if 0 == vol2: |
|
|
642 |
raise RuntimeError('The second supplied array does not contain any binary object.') |
|
|
643 |
|
|
|
644 |
return (vol1 - vol2) / float(vol2) |
|
|
645 |
|
|
|
646 |
def volume_correlation(results, references): |
|
|
647 |
r""" |
|
|
648 |
Volume correlation. |
|
|
649 |
|
|
|
650 |
Computes the linear correlation in binary object volume between the |
|
|
651 |
contents of the successive binary images supplied. Measured through |
|
|
652 |
the Pearson product-moment correlation coefficient. |
|
|
653 |
|
|
|
654 |
Parameters |
|
|
655 |
---------- |
|
|
656 |
results : sequence of array_like |
|
|
657 |
Ordered list of input data containing objects. Each array_like will be |
|
|
658 |
converted into binary: background where 0, object everywhere else. |
|
|
659 |
references : sequence of array_like |
|
|
660 |
Ordered list of input data containing objects. Each array_like will be |
|
|
661 |
converted into binary: background where 0, object everywhere else. |
|
|
662 |
The order must be the same as for ``results``. |
|
|
663 |
|
|
|
664 |
Returns |
|
|
665 |
------- |
|
|
666 |
r : float |
|
|
667 |
The correlation coefficient between -1 and 1. |
|
|
668 |
p : float |
|
|
669 |
The two-side p value. |
|
|
670 |
|
|
|
671 |
""" |
|
|
672 |
results = numpy.atleast_2d(numpy.array(results).astype(numpy.bool)) |
|
|
673 |
references = numpy.atleast_2d(numpy.array(references).astype(numpy.bool)) |
|
|
674 |
|
|
|
675 |
results_volumes = [numpy.count_nonzero(r) for r in results] |
|
|
676 |
references_volumes = [numpy.count_nonzero(r) for r in references] |
|
|
677 |
|
|
|
678 |
return pearsonr(results_volumes, references_volumes) # returns (Pearson' |
|
|
679 |
|
|
|
680 |
def volume_change_correlation(results, references): |
|
|
681 |
r""" |
|
|
682 |
Volume change correlation. |
|
|
683 |
|
|
|
684 |
Computes the linear correlation of change in binary object volume between |
|
|
685 |
the contents of the successive binary images supplied. Measured through |
|
|
686 |
the Pearson product-moment correlation coefficient. |
|
|
687 |
|
|
|
688 |
Parameters |
|
|
689 |
---------- |
|
|
690 |
results : sequence of array_like |
|
|
691 |
Ordered list of input data containing objects. Each array_like will be |
|
|
692 |
converted into binary: background where 0, object everywhere else. |
|
|
693 |
references : sequence of array_like |
|
|
694 |
Ordered list of input data containing objects. Each array_like will be |
|
|
695 |
converted into binary: background where 0, object everywhere else. |
|
|
696 |
The order must be the same as for ``results``. |
|
|
697 |
|
|
|
698 |
Returns |
|
|
699 |
------- |
|
|
700 |
r : float |
|
|
701 |
The correlation coefficient between -1 and 1. |
|
|
702 |
p : float |
|
|
703 |
The two-side p value. |
|
|
704 |
|
|
|
705 |
""" |
|
|
706 |
results = numpy.atleast_2d(numpy.array(results).astype(numpy.bool)) |
|
|
707 |
references = numpy.atleast_2d(numpy.array(references).astype(numpy.bool)) |
|
|
708 |
|
|
|
709 |
results_volumes = numpy.asarray([numpy.count_nonzero(r) for r in results]) |
|
|
710 |
references_volumes = numpy.asarray([numpy.count_nonzero(r) for r in references]) |
|
|
711 |
|
|
|
712 |
results_volumes_changes = results_volumes[1:] - results_volumes[:-1] |
|
|
713 |
references_volumes_changes = references_volumes[1:] - references_volumes[:-1] |
|
|
714 |
|
|
|
715 |
return pearsonr(results_volumes_changes, references_volumes_changes) # returns (Pearson's correlation coefficient, 2-tailed p-value) |
|
|
716 |
|
|
|
717 |
def obj_assd(result, reference, voxelspacing=None, connectivity=1): |
|
|
718 |
""" |
|
|
719 |
Average symmetric surface distance. |
|
|
720 |
|
|
|
721 |
Computes the average symmetric surface distance (ASSD) between the binary objects in |
|
|
722 |
two images. |
|
|
723 |
|
|
|
724 |
Parameters |
|
|
725 |
---------- |
|
|
726 |
result : array_like |
|
|
727 |
Input data containing objects. Can be any type but will be converted |
|
|
728 |
into binary: background where 0, object everywhere else. |
|
|
729 |
reference : array_like |
|
|
730 |
Input data containing objects. Can be any type but will be converted |
|
|
731 |
into binary: background where 0, object everywhere else. |
|
|
732 |
voxelspacing : float or sequence of floats, optional |
|
|
733 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
734 |
along each dimension. If a sequence, must be of length equal to |
|
|
735 |
the input rank; if a single number, this is used for all axes. If |
|
|
736 |
not specified, a grid spacing of unity is implied. |
|
|
737 |
connectivity : int |
|
|
738 |
The neighbourhood/connectivity considered when determining what accounts |
|
|
739 |
for a distinct binary object as well as when determining the surface |
|
|
740 |
of the binary objects. This value is passed to |
|
|
741 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
742 |
The decision on the connectivity is important, as it can influence the results |
|
|
743 |
strongly. If in doubt, leave it as it is. |
|
|
744 |
|
|
|
745 |
Returns |
|
|
746 |
------- |
|
|
747 |
assd : float |
|
|
748 |
The average symmetric surface distance between all mutually existing distinct |
|
|
749 |
binary object(s) in ``result`` and ``reference``. The distance unit is the same as for |
|
|
750 |
the spacing of elements along each dimension, which is usually given in mm. |
|
|
751 |
|
|
|
752 |
See also |
|
|
753 |
-------- |
|
|
754 |
:func:`obj_asd` |
|
|
755 |
|
|
|
756 |
Notes |
|
|
757 |
----- |
|
|
758 |
This is a real metric, obtained by calling and averaging |
|
|
759 |
|
|
|
760 |
>>> obj_asd(result, reference) |
|
|
761 |
|
|
|
762 |
and |
|
|
763 |
|
|
|
764 |
>>> obj_asd(reference, result) |
|
|
765 |
|
|
|
766 |
The binary images can therefore be supplied in any order. |
|
|
767 |
""" |
|
|
768 |
assd = numpy.mean( (obj_asd(result, reference, voxelspacing, connectivity), obj_asd(reference, result, voxelspacing, connectivity)) ) |
|
|
769 |
return assd |
|
|
770 |
|
|
|
771 |
|
|
|
772 |
def obj_asd(result, reference, voxelspacing=None, connectivity=1): |
|
|
773 |
""" |
|
|
774 |
Average surface distance between objects. |
|
|
775 |
|
|
|
776 |
First correspondences between distinct binary objects in reference and result are |
|
|
777 |
established. Then the average surface distance is only computed between corresponding |
|
|
778 |
objects. Correspondence is defined as unique and at least one voxel overlap. |
|
|
779 |
|
|
|
780 |
Parameters |
|
|
781 |
---------- |
|
|
782 |
result : array_like |
|
|
783 |
Input data containing objects. Can be any type but will be converted |
|
|
784 |
into binary: background where 0, object everywhere else. |
|
|
785 |
reference : array_like |
|
|
786 |
Input data containing objects. Can be any type but will be converted |
|
|
787 |
into binary: background where 0, object everywhere else. |
|
|
788 |
voxelspacing : float or sequence of floats, optional |
|
|
789 |
The voxelspacing in a distance unit i.e. spacing of elements |
|
|
790 |
along each dimension. If a sequence, must be of length equal to |
|
|
791 |
the input rank; if a single number, this is used for all axes. If |
|
|
792 |
not specified, a grid spacing of unity is implied. |
|
|
793 |
connectivity : int |
|
|
794 |
The neighbourhood/connectivity considered when determining what accounts |
|
|
795 |
for a distinct binary object as well as when determining the surface |
|
|
796 |
of the binary objects. This value is passed to |
|
|
797 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
798 |
The decision on the connectivity is important, as it can influence the results |
|
|
799 |
strongly. If in doubt, leave it as it is. |
|
|
800 |
|
|
|
801 |
Returns |
|
|
802 |
------- |
|
|
803 |
asd : float |
|
|
804 |
The average surface distance between all mutually existing distinct binary |
|
|
805 |
object(s) in ``result`` and ``reference``. The distance unit is the same as for the |
|
|
806 |
spacing of elements along each dimension, which is usually given in mm. |
|
|
807 |
|
|
|
808 |
See also |
|
|
809 |
-------- |
|
|
810 |
:func:`obj_assd` |
|
|
811 |
:func:`obj_tpr` |
|
|
812 |
:func:`obj_fpr` |
|
|
813 |
|
|
|
814 |
Notes |
|
|
815 |
----- |
|
|
816 |
This is not a real metric, as it is directed. See `obj_assd` for a real metric of this. |
|
|
817 |
|
|
|
818 |
For the understanding of this metric, both the notions of connectedness and surface |
|
|
819 |
distance are essential. Please see :func:`obj_tpr` and :func:`obj_fpr` for more |
|
|
820 |
information on the first and :func:`asd` on the second. |
|
|
821 |
|
|
|
822 |
Examples |
|
|
823 |
-------- |
|
|
824 |
>>> arr1 = numpy.asarray([[1,1,1],[1,1,1],[1,1,1]]) |
|
|
825 |
>>> arr2 = numpy.asarray([[0,1,0],[0,1,0],[0,1,0]]) |
|
|
826 |
>>> arr1 |
|
|
827 |
array([[1, 1, 1], |
|
|
828 |
[1, 1, 1], |
|
|
829 |
[1, 1, 1]]) |
|
|
830 |
>>> arr2 |
|
|
831 |
array([[0, 1, 0], |
|
|
832 |
[0, 1, 0], |
|
|
833 |
[0, 1, 0]]) |
|
|
834 |
>>> obj_asd(arr1, arr2) |
|
|
835 |
1.5 |
|
|
836 |
>>> obj_asd(arr2, arr1) |
|
|
837 |
0.333333333333 |
|
|
838 |
|
|
|
839 |
With the `voxelspacing` parameter, the distances between the voxels can be set for |
|
|
840 |
each dimension separately: |
|
|
841 |
|
|
|
842 |
>>> obj_asd(arr1, arr2, voxelspacing=(1,2)) |
|
|
843 |
1.5 |
|
|
844 |
>>> obj_asd(arr2, arr1, voxelspacing=(1,2)) |
|
|
845 |
0.333333333333 |
|
|
846 |
|
|
|
847 |
More examples depicting the notion of object connectedness: |
|
|
848 |
|
|
|
849 |
>>> arr1 = numpy.asarray([[1,0,1],[1,0,0],[0,0,0]]) |
|
|
850 |
>>> arr2 = numpy.asarray([[1,0,1],[1,0,0],[0,0,1]]) |
|
|
851 |
>>> arr1 |
|
|
852 |
array([[1, 0, 1], |
|
|
853 |
[1, 0, 0], |
|
|
854 |
[0, 0, 0]]) |
|
|
855 |
>>> arr2 |
|
|
856 |
array([[1, 0, 1], |
|
|
857 |
[1, 0, 0], |
|
|
858 |
[0, 0, 1]]) |
|
|
859 |
>>> obj_asd(arr1, arr2) |
|
|
860 |
0.0 |
|
|
861 |
>>> obj_asd(arr2, arr1) |
|
|
862 |
0.0 |
|
|
863 |
|
|
|
864 |
>>> arr1 = numpy.asarray([[1,0,1],[1,0,1],[0,0,1]]) |
|
|
865 |
>>> arr2 = numpy.asarray([[1,0,1],[1,0,0],[0,0,1]]) |
|
|
866 |
>>> arr1 |
|
|
867 |
array([[1, 0, 1], |
|
|
868 |
[1, 0, 1], |
|
|
869 |
[0, 0, 1]]) |
|
|
870 |
>>> arr2 |
|
|
871 |
array([[1, 0, 1], |
|
|
872 |
[1, 0, 0], |
|
|
873 |
[0, 0, 1]]) |
|
|
874 |
>>> obj_asd(arr1, arr2) |
|
|
875 |
0.6 |
|
|
876 |
>>> obj_asd(arr2, arr1) |
|
|
877 |
0.0 |
|
|
878 |
|
|
|
879 |
Influence of `connectivity` parameter can be seen in the following example, where |
|
|
880 |
with the (default) connectivity of `1` the first array is considered to contain two |
|
|
881 |
objects, while with an increase connectivity of `2`, just one large object is |
|
|
882 |
detected. |
|
|
883 |
|
|
|
884 |
>>> arr1 = numpy.asarray([[1,0,0],[0,1,1],[0,1,1]]) |
|
|
885 |
>>> arr2 = numpy.asarray([[1,0,0],[0,0,0],[0,0,0]]) |
|
|
886 |
>>> arr1 |
|
|
887 |
array([[1, 0, 0], |
|
|
888 |
[0, 1, 1], |
|
|
889 |
[0, 1, 1]]) |
|
|
890 |
>>> arr2 |
|
|
891 |
array([[1, 0, 0], |
|
|
892 |
[0, 0, 0], |
|
|
893 |
[0, 0, 0]]) |
|
|
894 |
>>> obj_asd(arr1, arr2) |
|
|
895 |
0.0 |
|
|
896 |
>>> obj_asd(arr1, arr2, connectivity=2) |
|
|
897 |
1.742955328 |
|
|
898 |
|
|
|
899 |
Note that the connectivity also influence the notion of what is considered an object |
|
|
900 |
surface voxels. |
|
|
901 |
""" |
|
|
902 |
sds = list() |
|
|
903 |
labelmap1, labelmap2, _a, _b, mapping = __distinct_binary_object_correspondences(result, reference, connectivity) |
|
|
904 |
slicers1 = find_objects(labelmap1) |
|
|
905 |
slicers2 = find_objects(labelmap2) |
|
|
906 |
for lid2, lid1 in mapping.items(): |
|
|
907 |
window = __combine_windows(slicers1[lid1 - 1], slicers2[lid2 - 1]) |
|
|
908 |
object1 = labelmap1[window] == lid1 |
|
|
909 |
object2 = labelmap2[window] == lid2 |
|
|
910 |
sds.extend(__surface_distances(object1, object2, voxelspacing, connectivity)) |
|
|
911 |
asd = numpy.mean(sds) |
|
|
912 |
return asd |
|
|
913 |
|
|
|
914 |
def obj_fpr(result, reference, connectivity=1): |
|
|
915 |
""" |
|
|
916 |
The false positive rate of distinct binary object detection. |
|
|
917 |
|
|
|
918 |
The false positive rates gives a percentage measure of how many distinct binary |
|
|
919 |
objects in the second array do not exists in the first array. A partial overlap |
|
|
920 |
(of minimum one voxel) is here considered sufficient. |
|
|
921 |
|
|
|
922 |
In cases where two distinct binary object in the second array overlap with a single |
|
|
923 |
distinct object in the first array, only one is considered to have been detected |
|
|
924 |
successfully and the other is added to the count of false positives. |
|
|
925 |
|
|
|
926 |
Parameters |
|
|
927 |
---------- |
|
|
928 |
result : array_like |
|
|
929 |
Input data containing objects. Can be any type but will be converted |
|
|
930 |
into binary: background where 0, object everywhere else. |
|
|
931 |
reference : array_like |
|
|
932 |
Input data containing objects. Can be any type but will be converted |
|
|
933 |
into binary: background where 0, object everywhere else. |
|
|
934 |
connectivity : int |
|
|
935 |
The neighbourhood/connectivity considered when determining what accounts |
|
|
936 |
for a distinct binary object. This value is passed to |
|
|
937 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
938 |
The decision on the connectivity is important, as it can influence the results |
|
|
939 |
strongly. If in doubt, leave it as it is. |
|
|
940 |
|
|
|
941 |
Returns |
|
|
942 |
------- |
|
|
943 |
tpr : float |
|
|
944 |
A percentage measure of how many distinct binary objects in ``results`` have no |
|
|
945 |
corresponding binary object in ``reference``. It has the range :math:`[0, 1]`, where a :math:`0` |
|
|
946 |
denotes an ideal score. |
|
|
947 |
|
|
|
948 |
Raises |
|
|
949 |
------ |
|
|
950 |
RuntimeError |
|
|
951 |
If the second array is empty. |
|
|
952 |
|
|
|
953 |
See also |
|
|
954 |
-------- |
|
|
955 |
:func:`obj_tpr` |
|
|
956 |
|
|
|
957 |
Notes |
|
|
958 |
----- |
|
|
959 |
This is not a real metric, as it is directed. Whatever array is considered as |
|
|
960 |
reference should be passed second. A perfect score of :math:`0` tells that there are no |
|
|
961 |
distinct binary objects in the second array that do not exists also in the reference |
|
|
962 |
array, but does not reveal anything about objects in the reference array also |
|
|
963 |
existing in the second array (use :func:`obj_tpr` for this). |
|
|
964 |
|
|
|
965 |
Examples |
|
|
966 |
-------- |
|
|
967 |
>>> arr2 = numpy.asarray([[1,0,0],[1,0,1],[0,0,1]]) |
|
|
968 |
>>> arr1 = numpy.asarray([[0,0,1],[1,0,1],[0,0,1]]) |
|
|
969 |
>>> arr2 |
|
|
970 |
array([[1, 0, 0], |
|
|
971 |
[1, 0, 1], |
|
|
972 |
[0, 0, 1]]) |
|
|
973 |
>>> arr1 |
|
|
974 |
array([[0, 0, 1], |
|
|
975 |
[1, 0, 1], |
|
|
976 |
[0, 0, 1]]) |
|
|
977 |
>>> obj_fpr(arr1, arr2) |
|
|
978 |
0.0 |
|
|
979 |
>>> obj_fpr(arr2, arr1) |
|
|
980 |
0.0 |
|
|
981 |
|
|
|
982 |
Example of directedness: |
|
|
983 |
|
|
|
984 |
>>> arr2 = numpy.asarray([1,0,1,0,1]) |
|
|
985 |
>>> arr1 = numpy.asarray([1,0,1,0,0]) |
|
|
986 |
>>> obj_fpr(arr1, arr2) |
|
|
987 |
0.0 |
|
|
988 |
>>> obj_fpr(arr2, arr1) |
|
|
989 |
0.3333333333333333 |
|
|
990 |
|
|
|
991 |
Examples of multiple overlap treatment: |
|
|
992 |
|
|
|
993 |
>>> arr2 = numpy.asarray([1,0,1,0,1,1,1]) |
|
|
994 |
>>> arr1 = numpy.asarray([1,1,1,0,1,0,1]) |
|
|
995 |
>>> obj_fpr(arr1, arr2) |
|
|
996 |
0.3333333333333333 |
|
|
997 |
>>> obj_fpr(arr2, arr1) |
|
|
998 |
0.3333333333333333 |
|
|
999 |
|
|
|
1000 |
>>> arr2 = numpy.asarray([1,0,1,1,1,0,1]) |
|
|
1001 |
>>> arr1 = numpy.asarray([1,1,1,0,1,1,1]) |
|
|
1002 |
>>> obj_fpr(arr1, arr2) |
|
|
1003 |
0.0 |
|
|
1004 |
>>> obj_fpr(arr2, arr1) |
|
|
1005 |
0.3333333333333333 |
|
|
1006 |
|
|
|
1007 |
>>> arr2 = numpy.asarray([[1,0,1,0,0], |
|
|
1008 |
[1,0,0,0,0], |
|
|
1009 |
[1,0,1,1,1], |
|
|
1010 |
[0,0,0,0,0], |
|
|
1011 |
[1,0,1,0,0]]) |
|
|
1012 |
>>> arr1 = numpy.asarray([[1,1,1,0,0], |
|
|
1013 |
[0,0,0,0,0], |
|
|
1014 |
[1,1,1,0,1], |
|
|
1015 |
[0,0,0,0,0], |
|
|
1016 |
[1,1,1,0,0]]) |
|
|
1017 |
>>> obj_fpr(arr1, arr2) |
|
|
1018 |
0.0 |
|
|
1019 |
>>> obj_fpr(arr2, arr1) |
|
|
1020 |
0.2 |
|
|
1021 |
""" |
|
|
1022 |
_, _, _, n_obj_reference, mapping = __distinct_binary_object_correspondences(reference, result, connectivity) |
|
|
1023 |
return (n_obj_reference - len(mapping)) / float(n_obj_reference) |
|
|
1024 |
|
|
|
1025 |
def obj_tpr(result, reference, connectivity=1): |
|
|
1026 |
""" |
|
|
1027 |
The true positive rate of distinct binary object detection. |
|
|
1028 |
|
|
|
1029 |
The true positive rates gives a percentage measure of how many distinct binary |
|
|
1030 |
objects in the first array also exists in the second array. A partial overlap |
|
|
1031 |
(of minimum one voxel) is here considered sufficient. |
|
|
1032 |
|
|
|
1033 |
In cases where two distinct binary object in the first array overlaps with a single |
|
|
1034 |
distinct object in the second array, only one is considered to have been detected |
|
|
1035 |
successfully. |
|
|
1036 |
|
|
|
1037 |
Parameters |
|
|
1038 |
---------- |
|
|
1039 |
result : array_like |
|
|
1040 |
Input data containing objects. Can be any type but will be converted |
|
|
1041 |
into binary: background where 0, object everywhere else. |
|
|
1042 |
reference : array_like |
|
|
1043 |
Input data containing objects. Can be any type but will be converted |
|
|
1044 |
into binary: background where 0, object everywhere else. |
|
|
1045 |
connectivity : int |
|
|
1046 |
The neighbourhood/connectivity considered when determining what accounts |
|
|
1047 |
for a distinct binary object. This value is passed to |
|
|
1048 |
`scipy.ndimage.morphology.generate_binary_structure` and should usually be :math:`> 1`. |
|
|
1049 |
The decision on the connectivity is important, as it can influence the results |
|
|
1050 |
strongly. If in doubt, leave it as it is. |
|
|
1051 |
|
|
|
1052 |
Returns |
|
|
1053 |
------- |
|
|
1054 |
tpr : float |
|
|
1055 |
A percentage measure of how many distinct binary objects in ``result`` also exists |
|
|
1056 |
in ``reference``. It has the range :math:`[0, 1]`, where a :math:`1` denotes an ideal score. |
|
|
1057 |
|
|
|
1058 |
Raises |
|
|
1059 |
------ |
|
|
1060 |
RuntimeError |
|
|
1061 |
If the reference object is empty. |
|
|
1062 |
|
|
|
1063 |
See also |
|
|
1064 |
-------- |
|
|
1065 |
:func:`obj_fpr` |
|
|
1066 |
|
|
|
1067 |
Notes |
|
|
1068 |
----- |
|
|
1069 |
This is not a real metric, as it is directed. Whatever array is considered as |
|
|
1070 |
reference should be passed second. A perfect score of :math:`1` tells that all distinct |
|
|
1071 |
binary objects in the reference array also exist in the result array, but does not |
|
|
1072 |
reveal anything about additional binary objects in the result array |
|
|
1073 |
(use :func:`obj_fpr` for this). |
|
|
1074 |
|
|
|
1075 |
Examples |
|
|
1076 |
-------- |
|
|
1077 |
>>> arr2 = numpy.asarray([[1,0,0],[1,0,1],[0,0,1]]) |
|
|
1078 |
>>> arr1 = numpy.asarray([[0,0,1],[1,0,1],[0,0,1]]) |
|
|
1079 |
>>> arr2 |
|
|
1080 |
array([[1, 0, 0], |
|
|
1081 |
[1, 0, 1], |
|
|
1082 |
[0, 0, 1]]) |
|
|
1083 |
>>> arr1 |
|
|
1084 |
array([[0, 0, 1], |
|
|
1085 |
[1, 0, 1], |
|
|
1086 |
[0, 0, 1]]) |
|
|
1087 |
>>> obj_tpr(arr1, arr2) |
|
|
1088 |
1.0 |
|
|
1089 |
>>> obj_tpr(arr2, arr1) |
|
|
1090 |
1.0 |
|
|
1091 |
|
|
|
1092 |
Example of directedness: |
|
|
1093 |
|
|
|
1094 |
>>> arr2 = numpy.asarray([1,0,1,0,1]) |
|
|
1095 |
>>> arr1 = numpy.asarray([1,0,1,0,0]) |
|
|
1096 |
>>> obj_tpr(arr1, arr2) |
|
|
1097 |
0.6666666666666666 |
|
|
1098 |
>>> obj_tpr(arr2, arr1) |
|
|
1099 |
1.0 |
|
|
1100 |
|
|
|
1101 |
Examples of multiple overlap treatment: |
|
|
1102 |
|
|
|
1103 |
>>> arr2 = numpy.asarray([1,0,1,0,1,1,1]) |
|
|
1104 |
>>> arr1 = numpy.asarray([1,1,1,0,1,0,1]) |
|
|
1105 |
>>> obj_tpr(arr1, arr2) |
|
|
1106 |
0.6666666666666666 |
|
|
1107 |
>>> obj_tpr(arr2, arr1) |
|
|
1108 |
0.6666666666666666 |
|
|
1109 |
|
|
|
1110 |
>>> arr2 = numpy.asarray([1,0,1,1,1,0,1]) |
|
|
1111 |
>>> arr1 = numpy.asarray([1,1,1,0,1,1,1]) |
|
|
1112 |
>>> obj_tpr(arr1, arr2) |
|
|
1113 |
0.6666666666666666 |
|
|
1114 |
>>> obj_tpr(arr2, arr1) |
|
|
1115 |
1.0 |
|
|
1116 |
|
|
|
1117 |
>>> arr2 = numpy.asarray([[1,0,1,0,0], |
|
|
1118 |
[1,0,0,0,0], |
|
|
1119 |
[1,0,1,1,1], |
|
|
1120 |
[0,0,0,0,0], |
|
|
1121 |
[1,0,1,0,0]]) |
|
|
1122 |
>>> arr1 = numpy.asarray([[1,1,1,0,0], |
|
|
1123 |
[0,0,0,0,0], |
|
|
1124 |
[1,1,1,0,1], |
|
|
1125 |
[0,0,0,0,0], |
|
|
1126 |
[1,1,1,0,0]]) |
|
|
1127 |
>>> obj_tpr(arr1, arr2) |
|
|
1128 |
0.8 |
|
|
1129 |
>>> obj_tpr(arr2, arr1) |
|
|
1130 |
1.0 |
|
|
1131 |
""" |
|
|
1132 |
_, _, n_obj_result, _, mapping = __distinct_binary_object_correspondences(reference, result, connectivity) |
|
|
1133 |
return len(mapping) / float(n_obj_result) |
|
|
1134 |
|
|
|
1135 |
def __distinct_binary_object_correspondences(reference, result, connectivity=1): |
|
|
1136 |
""" |
|
|
1137 |
Determines all distinct (where connectivity is defined by the connectivity parameter |
|
|
1138 |
passed to scipy's `generate_binary_structure`) binary objects in both of the input |
|
|
1139 |
parameters and returns a 1to1 mapping from the labelled objects in reference to the |
|
|
1140 |
corresponding (whereas a one-voxel overlap suffices for correspondence) objects in |
|
|
1141 |
result. |
|
|
1142 |
|
|
|
1143 |
All stems from the problem, that the relationship is non-surjective many-to-many. |
|
|
1144 |
|
|
|
1145 |
@return (labelmap1, labelmap2, n_lables1, n_labels2, labelmapping2to1) |
|
|
1146 |
""" |
|
|
1147 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
1148 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
1149 |
|
|
|
1150 |
# binary structure |
|
|
1151 |
footprint = generate_binary_structure(result.ndim, connectivity) |
|
|
1152 |
|
|
|
1153 |
# label distinct binary objects |
|
|
1154 |
labelmap1, n_obj_result = label(result, footprint) |
|
|
1155 |
labelmap2, n_obj_reference = label(reference, footprint) |
|
|
1156 |
|
|
|
1157 |
# find all overlaps from labelmap2 to labelmap1; collect one-to-one relationships and store all one-two-many for later processing |
|
|
1158 |
slicers = find_objects(labelmap2) # get windows of labelled objects |
|
|
1159 |
mapping = dict() # mappings from labels in labelmap2 to corresponding object labels in labelmap1 |
|
|
1160 |
used_labels = set() # set to collect all already used labels from labelmap2 |
|
|
1161 |
one_to_many = list() # list to collect all one-to-many mappings |
|
|
1162 |
for l1id, slicer in enumerate(slicers): # iterate over object in labelmap2 and their windows |
|
|
1163 |
l1id += 1 # labelled objects have ids sarting from 1 |
|
|
1164 |
bobj = (l1id) == labelmap2[slicer] # find binary object corresponding to the label1 id in the segmentation |
|
|
1165 |
l2ids = numpy.unique(labelmap1[slicer][bobj]) # extract all unique object identifiers at the corresponding positions in the reference (i.e. the mapping) |
|
|
1166 |
l2ids = l2ids[0 != l2ids] # remove background identifiers (=0) |
|
|
1167 |
if 1 == len(l2ids): # one-to-one mapping: if target label not already used, add to final list of object-to-object mappings and mark target label as used |
|
|
1168 |
l2id = l2ids[0] |
|
|
1169 |
if not l2id in used_labels: |
|
|
1170 |
mapping[l1id] = l2id |
|
|
1171 |
used_labels.add(l2id) |
|
|
1172 |
elif 1 < len(l2ids): # one-to-many mapping: store relationship for later processing |
|
|
1173 |
one_to_many.append((l1id, set(l2ids))) |
|
|
1174 |
|
|
|
1175 |
# process one-to-many mappings, always choosing the one with the least labelmap2 correspondences first |
|
|
1176 |
while True: |
|
|
1177 |
one_to_many = [(l1id, l2ids - used_labels) for l1id, l2ids in one_to_many] # remove already used ids from all sets |
|
|
1178 |
one_to_many = [x for x in one_to_many if x[1]] # remove empty sets |
|
|
1179 |
one_to_many = sorted(one_to_many, key=lambda x: len(x[1])) # sort by set length |
|
|
1180 |
if 0 == len(one_to_many): |
|
|
1181 |
break |
|
|
1182 |
l2id = one_to_many[0][1].pop() # select an arbitrary target label id from the shortest set |
|
|
1183 |
mapping[one_to_many[0][0]] = l2id # add to one-to-one mappings |
|
|
1184 |
used_labels.add(l2id) # mark target label as used |
|
|
1185 |
one_to_many = one_to_many[1:] # delete the processed set from all sets |
|
|
1186 |
|
|
|
1187 |
return labelmap1, labelmap2, n_obj_result, n_obj_reference, mapping |
|
|
1188 |
|
|
|
1189 |
def __surface_distances(result, reference, voxelspacing=None, connectivity=1): |
|
|
1190 |
""" |
|
|
1191 |
The distances between the surface voxel of binary objects in result and their |
|
|
1192 |
nearest partner surface voxel of a binary object in reference. |
|
|
1193 |
""" |
|
|
1194 |
result = numpy.atleast_1d(result.astype(numpy.bool)) |
|
|
1195 |
reference = numpy.atleast_1d(reference.astype(numpy.bool)) |
|
|
1196 |
if voxelspacing is not None: |
|
|
1197 |
voxelspacing = _ni_support._normalize_sequence(voxelspacing, result.ndim) |
|
|
1198 |
voxelspacing = numpy.asarray(voxelspacing, dtype=numpy.float64) |
|
|
1199 |
if not voxelspacing.flags.contiguous: |
|
|
1200 |
voxelspacing = voxelspacing.copy() |
|
|
1201 |
|
|
|
1202 |
# binary structure |
|
|
1203 |
footprint = generate_binary_structure(result.ndim, connectivity) |
|
|
1204 |
|
|
|
1205 |
# test for emptiness |
|
|
1206 |
if 0 == numpy.count_nonzero(result): |
|
|
1207 |
raise RuntimeError('The first supplied array does not contain any binary object.') |
|
|
1208 |
if 0 == numpy.count_nonzero(reference): |
|
|
1209 |
raise RuntimeError('The second supplied array does not contain any binary object.') |
|
|
1210 |
|
|
|
1211 |
# extract only 1-pixel border line of objects |
|
|
1212 |
result_border = result ^ binary_erosion(result, structure=footprint, iterations=1) |
|
|
1213 |
reference_border = reference ^ binary_erosion(reference, structure=footprint, iterations=1) |
|
|
1214 |
|
|
|
1215 |
# compute average surface distance |
|
|
1216 |
# Note: scipys distance transform is calculated only inside the borders of the |
|
|
1217 |
# foreground objects, therefore the input has to be reversed |
|
|
1218 |
dt = distance_transform_edt(~reference_border, sampling=voxelspacing) |
|
|
1219 |
sds = dt[result_border] |
|
|
1220 |
|
|
|
1221 |
return sds |
|
|
1222 |
|
|
|
1223 |
def __combine_windows(w1, w2): |
|
|
1224 |
""" |
|
|
1225 |
Joins two windows (defined by tuple of slices) such that their maximum |
|
|
1226 |
combined extend is covered by the new returned window. |
|
|
1227 |
""" |
|
|
1228 |
res = [] |
|
|
1229 |
for s1, s2 in zip(w1, w2): |
|
|
1230 |
res.append(slice(min(s1.start, s2.start), max(s1.stop, s2.stop))) |
|
|
1231 |
return tuple(res) |