Diff of /utils/dice3D.py [000000] .. [98e649]

Switch to unified view

a b/utils/dice3D.py
1
"""
2
author: Clément Zotti (clement.zotti@usherbrooke.ca)
3
date: April 2017
4
5
DESCRIPTION :
6
The script provide helpers functions to handle nifti image format:
7
    - load_nii()
8
    - save_nii()
9
10
to generate metrics for two images:
11
    - metrics()
12
13
And it is callable from the command line (see below).
14
Each function provided in this script has comments to understand
15
how they works.
16
17
HOW-TO:
18
19
This script was tested for python 3.4.
20
21
First, you need to install the required packages with
22
    pip install -r requirements.txt
23
24
After the installation, you have two ways of running this script:
25
    1) python metrics.py ground_truth/patient001_ED.nii.gz prediction/patient001_ED.nii.gz
26
    2) python metrics.py ground_truth/ prediction/
27
28
The first option will print in the console the dice and volume of each class for the given image.
29
The second option wiil ouput a csv file where each images will have the dice and volume of each class.
30
31
32
Link: http://acdc.creatis.insa-lyon.fr
33
34
"""
35
36
import os
37
from glob import glob
38
import time
39
import re
40
import argparse
41
import nibabel as nib
42
# import pandas as pd
43
from medpy.metric.binary import hd, dc
44
import numpy as np
45
46
47
48
HEADER = ["Name", "Dice LV", "Volume LV", "Err LV(ml)",
49
          "Dice RV", "Volume RV", "Err RV(ml)",
50
          "Dice MYO", "Volume MYO", "Err MYO(ml)"]
51
52
#
53
# Utils functions used to sort strings into a natural order
54
#
55
def conv_int(i):
56
    return int(i) if i.isdigit() else i
57
58
59
def natural_order(sord):
60
    """
61
    Sort a (list,tuple) of strings into natural order.
62
63
    Ex:
64
65
    ['1','10','2'] -> ['1','2','10']
66
67
    ['abc1def','ab10d','b2c','ab1d'] -> ['ab1d','ab10d', 'abc1def', 'b2c']
68
69
    """
70
    if isinstance(sord, tuple):
71
        sord = sord[0]
72
    return [conv_int(c) for c in re.split(r'(\d+)', sord)]
73
74
75
#
76
# Utils function to load and save nifti files with the nibabel package
77
#
78
def load_nii(img_path):
79
    """
80
    Function to load a 'nii' or 'nii.gz' file, The function returns
81
    everyting needed to save another 'nii' or 'nii.gz'
82
    in the same dimensional space, i.e. the affine matrix and the header
83
84
    Parameters
85
    ----------
86
87
    img_path: string
88
    String with the path of the 'nii' or 'nii.gz' image file name.
89
90
    Returns
91
    -------
92
    Three element, the first is a numpy array of the image values,
93
    the second is the affine transformation of the image, and the
94
    last one is the header of the image.
95
    """
96
    nimg = nib.load(img_path)
97
    return nimg.get_data(), nimg.affine, nimg.header
98
99
100
def save_nii(img_path, data, affine, header):
101
    """
102
    Function to save a 'nii' or 'nii.gz' file.
103
104
    Parameters
105
    ----------
106
107
    img_path: string
108
    Path to save the image should be ending with '.nii' or '.nii.gz'.
109
110
    data: np.array
111
    Numpy array of the image data.
112
113
    affine: list of list or np.array
114
    The affine transformation to save with the image.
115
116
    header: nib.Nifti1Header
117
    The header that define everything about the data
118
    (pleasecheck nibabel documentation).
119
    """
120
    nimg = nib.Nifti1Image(data, affine=affine, header=header)
121
    nimg.to_filename(img_path)
122
123
124
#
125
# Functions to process files, directories and metrics
126
#
127
def metrics(img_gt, img_pred, voxel_size):
128
    """
129
    Function to compute the metrics between two segmentation maps given as input.
130
131
    Parameters
132
    ----------
133
    img_gt: np.array
134
    Array of the ground truth segmentation map.
135
136
    img_pred: np.array
137
    Array of the predicted segmentation map.
138
139
    voxel_size: list, tuple or np.array
140
    The size of a voxel of the images used to compute the volumes.
141
142
    Return
143
    ------
144
    A list of metrics in this order, [Dice LV, Volume LV, Err LV(ml),
145
    Dice RV, Volume RV, Err RV(ml), Dice MYO, Volume MYO, Err MYO(ml)]
146
    """
147
148
    if img_gt.ndim != img_pred.ndim:
149
        raise ValueError("The arrays 'img_gt' and 'img_pred' should have the "
150
                         "same dimension, {} against {}".format(img_gt.ndim,
151
                                                                img_pred.ndim))
152
153
    res = []
154
    # Loop on each classes of the input images
155
    for c in [3, 1, 2]:
156
        # Copy the gt image to not alterate the input
157
        gt_c_i = np.copy(img_gt)
158
        gt_c_i[gt_c_i != c] = 0
159
160
        # Copy the pred image to not alterate the input
161
        pred_c_i = np.copy(img_pred)
162
        pred_c_i[pred_c_i != c] = 0
163
164
        # Clip the value to compute the volumes
165
        gt_c_i = np.clip(gt_c_i, 0, 1)
166
        pred_c_i = np.clip(pred_c_i, 0, 1)
167
168
        # Compute the Dice
169
        dice = dc(gt_c_i, pred_c_i)
170
171
        # Compute volume
172
        volpred = pred_c_i.sum() * np.prod(voxel_size) / 1000.
173
        volgt = gt_c_i.sum() * np.prod(voxel_size) / 1000.
174
175
        # res += [dice, volpred, volpred-volgt]
176
        res += [dice]
177
178
    return res
179
180
181
def compute_metrics_on_files(path_gt, path_pred):
182
    """
183
    Function to give the metrics for two files
184
185
    Parameters
186
    ----------
187
188
    path_gt: string
189
    Path of the ground truth image.
190
191
    path_pred: string
192
    Path of the predicted image.
193
    """
194
    gt, _, header = load_nii(path_gt)
195
    pred, _, _ = load_nii(path_pred)
196
    zooms = header.get_zooms()
197
198
    name = os.path.basename(path_gt)
199
    name = name.split('.')[0]
200
    res = metrics(gt, pred, zooms)
201
    res = ["{:.3f}".format(r) for r in res]
202
203
    formatting = "{:>14}, {:>7}, {:>9}, {:>10}, {:>7}, {:>9}, {:>10}, {:>8}, {:>10}, {:>11}"
204
    print(formatting.format(*HEADER))
205
    print(formatting.format(name, *res))
206
207
208
def compute_metrics_on_directories(dir_gt, dir_pred):
209
    """
210
    Function to generate a csv file for each images of two directories.
211
212
    Parameters
213
    ----------
214
215
    path_gt: string
216
    Directory of the ground truth segmentation maps.
217
218
    path_pred: string
219
    Directory of the predicted segmentation maps.
220
    """
221
    lst_gt = sorted(glob(os.path.join(dir_gt, '*')), key=natural_order)
222
    lst_pred = sorted(glob(os.path.join(dir_pred, '*')), key=natural_order)
223
224
    res = []
225
    for p_gt, p_pred in zip(lst_gt, lst_pred):
226
        if os.path.basename(p_gt) != os.path.basename(p_pred):
227
            raise ValueError("The two files don't have the same name"
228
                             " {}, {}.".format(os.path.basename(p_gt),
229
                                               os.path.basename(p_pred)))
230
231
        gt, _, header = load_nii(p_gt)
232
        pred, _, _ = load_nii(p_pred)
233
        zooms = header.get_zooms()
234
        res.append(metrics(gt, pred, zooms))
235
236
    lst_name_gt = [os.path.basename(gt).split(".")[0] for gt in lst_gt]
237
    res = [[n,] + r for r, n in zip(res, lst_name_gt)]
238
    df = pd.DataFrame(res, columns=HEADER)
239
    df.to_csv("results_{}.csv".format(time.strftime("%Y%m%d_%H%M%S")), index=False)
240
241
def main(path_gt, path_pred):
242
    """
243
    Main function to select which method to apply on the input parameters.
244
    """
245
    if os.path.isfile(path_gt) and os.path.isfile(path_pred):
246
        compute_metrics_on_files(path_gt, path_pred)
247
    elif os.path.isdir(path_gt) and os.path.isdir(path_pred):
248
        compute_metrics_on_directories(path_gt, path_pred)
249
    else:
250
        raise ValueError(
251
            "The paths given needs to be two directories or two files.")
252
253
254
if __name__ == "__main__":
255
    # parser = argparse.ArgumentParser(
256
    #     description="Script to compute ACDC challenge metrics.")
257
    # parser.add_argument("GT_IMG", type=str, help="Ground Truth image")
258
    # parser.add_argument("PRED_IMG", type=str, help="Predicted image")
259
    # args = parser.parse_args()
260
    # main(args.GT_IMG, args.PRED_IMG)
261
262
    ##############################################################
263
    gt = np.random.randint(0, 4, size=(224, 224, 100))
264
    print(np.unique(gt))
265
    pred = np.array(gt)
266
    pred[pred==2] = 3
267
    result = metrics(gt, pred, voxel_size=(224, 224, 100))
268
    print(result)