-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathisles_metrics.py
More file actions
204 lines (163 loc) · 6.88 KB
/
Copy pathisles_metrics.py
File metadata and controls
204 lines (163 loc) · 6.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
### All code in this file is from the ISLES 2022 challenge repository: https://github.com/ezequieldlrosa/isles22/blob/main/utils/eval_utils.py
import numpy as np
import warnings
import cc3d
def compute_dice(im1, im2, empty_value=1.0):
"""
Computes the Dice coefficient, a measure of set similarity.
Parameters
----------
im1 : array-like, bool
Any array of arbitrary size. If not boolean, will be converted.
im2 : array-like, bool
Any other array of identical size as im1. If not boolean, it will be converted.
empty_value : scalar, float.
Returns
-------
dice : float
Dice coefficient as a float on range [0,1].
Maximum similarity = 1
No similarity = 0
If both images are empty (sum equal to zero) = empty_value
Notes
-----
The order of inputs for `dice` is irrelevant. The result will be
identical if `im1` and `im2` are switched.
This function has been adapted from the Verse Challenge repository:
https://github.com/anjany/verse/blob/main/utils/eval_utilities.py
"""
im1 = np.asarray(im1).astype(np.bool)
im2 = np.asarray(im2).astype(np.bool)
if im1.shape != im2.shape:
raise ValueError("Shape mismatch: im1 and im2 must have the same shape.")
im_sum = im1.sum() + im2.sum()
if im_sum == 0:
return empty_value
# Compute Dice coefficient
intersection = np.logical_and(im1, im2)
return 2.0 * intersection.sum() / im_sum
def compute_absolute_volume_difference(im1, im2, voxel_size=1.):
"""
Computes the absolute volume difference between two masks.
Parameters
----------
im1 : array-like, bool
Any array of arbitrary size. If not boolean, will be converted.
im2 : array-like, bool
Any other array of identical size as 'ground_truth'. If not boolean, it will be converted.
voxel_size : scalar, float (ml)
If not float, it will be converted.
Returns
-------
abs_vol_diff : float, measured in ml.
Absolute volume difference as a float.
Maximum similarity = 0
No similarity = inf
Notes
-----
The order of inputs is irrelevant. The result will be identical if `im1` and `im2` are switched.
"""
im1 = np.asarray(im1).astype(np.bool)
im2 = np.asarray(im2).astype(np.bool)
voxel_size = float(voxel_size)
if im1.shape != im2.shape:
warnings.warn(
"Shape mismatch: ground_truth and prediction have difference shapes."
" The absolute volume difference is computed with mismatching shape masks"
)
ground_truth_volume = np.sum(im1) * voxel_size
prediction_volume = np.sum(im2) * voxel_size
abs_vol_diff = np.abs(ground_truth_volume - prediction_volume)
return abs_vol_diff
def compute_absolute_lesion_difference(ground_truth, prediction, connectivity=26):
"""
Computes the absolute lesion difference between two masks. The number of lesions are counted for
each volume, and their absolute difference is computed.
Parameters
----------
ground_truth : array-like, bool
Any array of arbitrary size. If not boolean, will be converted.
prediction : array-like, bool
Any other array of identical size as 'ground_truth'. If not boolean, it will be converted.
Returns
-------
abs_les_diff : int
Absolute lesion difference as integer.
Maximum similarity = 0
No similarity = inf
Notes
-----
"""
ground_truth = np.asarray(ground_truth).astype(np.bool)
prediction = np.asarray(prediction).astype(np.bool)
_, ground_truth_numb_lesion = cc3d.connected_components(ground_truth, connectivity=connectivity, return_N=True)
_, prediction_numb_lesion = cc3d.connected_components(prediction, connectivity=connectivity, return_N=True)
abs_les_diff = abs(ground_truth_numb_lesion - prediction_numb_lesion)
return abs_les_diff
def compute_lesion_f1_score(ground_truth, prediction, empty_value=1.0, connectivity=26):
"""
Computes the lesion-wise F1-score between two masks.
Parameters
----------
ground_truth : array-like, bool
Any array of arbitrary size. If not boolean, will be converted.
prediction : array-like, bool
Any other array of identical size as 'ground_truth'. If not boolean, it will be converted.
empty_value : scalar, float.
connectivity : scalar, int.
Returns
-------
f1_score : float
Lesion-wise F1-score as float.
Max score = 1
Min score = 0
If both images are empty (tp + fp + fn =0) = empty_value
Notes
-----
This function computes lesion-wise score by defining true positive lesions (tp), false positive lesions (fp) and
false negative lesions (fn) using 3D connected-component-analysis.
tp: 3D connected-component from the ground-truth image that overlaps at least on one voxel with the prediction image.
fp: 3D connected-component from the prediction image that has no voxel overlapping with the ground-truth image.
fn: 3d connected-component from the ground-truth image that has no voxel overlapping with the prediction image.
"""
ground_truth = np.asarray(ground_truth).astype(np.bool)
prediction = np.asarray(prediction).astype(np.bool)
tp = 0
fp = 0
fn = 0
# Check if ground-truth connected-components are detected or missed (tp and fn respectively).
intersection = np.logical_and(ground_truth, prediction)
labeled_ground_truth, N = cc3d.connected_components(
ground_truth, connectivity=connectivity, return_N=True
)
# Iterate over ground_truth clusters to find tp and fn.
# tp and fn are only computed if the ground-truth is not empty.
if N > 0:
for _, binary_cluster_image in cc3d.each(labeled_ground_truth, binary=True, in_place=True):
if np.logical_and(binary_cluster_image, intersection).any():
tp += 1
else:
fn += 1
# iterate over prediction clusters to find fp.
# fp are only computed if the prediction image is not empty.
labeled_prediction, N = cc3d.connected_components(
prediction, connectivity=connectivity, return_N=True
)
if N > 0:
for _, binary_cluster_image in cc3d.each(labeled_prediction, binary=True, in_place=True):
if not np.logical_and(binary_cluster_image, ground_truth).any():
fp += 1
# Define case when both images are empty.
if tp + fp + fn == 0:
_, N = cc3d.connected_components(ground_truth, connectivity=connectivity, return_N=True)
if N == 0:
f1_score = empty_value
else:
f1_score = tp / (tp + (fp + fn) / 2)
return f1_score
isles_metric = {
"isles_dice": compute_dice,
"absolute_volume_difference": compute_absolute_volume_difference,
"absolute_lesion_difference": compute_absolute_lesion_difference,
"lesion_f1_score": compute_lesion_f1_score,
}