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)