Пример #1
0
def build_longitudinal_consensus(labels_dir_1,
                                 labels_dir_2,
                                 result_dir,
                                 recompute=True):

    # create result dir
    utils.mkdir(result_dir)

    # list all segmentations
    path_labels_1 = utils.list_files(labels_dir_1)
    path_labels_2 = utils.list_files(labels_dir_2)

    for path_lab_1, path_lab_2 in zip(path_labels_1, path_labels_2):

        # check if result is already saved
        path_result = os.path.join(result_dir, os.path.basename(path_lab_1))
        if (not os.path.isfile(path_result)) | recompute:

            # load volumes
            lab_1, aff, h = utils.load_volume(path_lab_1, im_only=False)
            lab_2 = utils.load_volume(path_lab_2)

            # compute and save consensus
            dist_masp_1 = edit_volumes.compute_distance_map(lab_1,
                                                            crop_margin=20)
            dist_masp_2 = edit_volumes.compute_distance_map(lab_2,
                                                            crop_margin=20)
            consensus = (np.mean(np.stack([dist_masp_1, dist_masp_2], axis=-1),
                                 axis=-1) > 0) * 1
            utils.save_volume(consensus, aff, h, path_result)
Пример #2
0
def dice_evaluation(gt_dir,
                    seg_dir,
                    path_label_list,
                    path_result_dice_array):
    """Computes Dice scores for all labels contained in path_segmentation_label_list. Files in gt_folder and seg_folder
    are matched by sorting order.
    :param gt_dir: folder containing ground truth files.
    :param seg_dir: folder containing evaluation files.
    :param path_label_list: path of numpy vector containing all labels to compute the Dice for.
    :param path_result_dice_array: path where the resulting Dice will be writen as numpy array.
    :return: numpy array containing all dice scores (labels in rows, subjects in columns).
    """

    # create result folder
    if not os.path.exists(os.path.dirname(path_result_dice_array)):
        os.mkdir(os.path.dirname(path_result_dice_array))

    # get list label maps to compare
    path_gt_labels = utils.list_images_in_folder(gt_dir)
    path_segs = utils.list_images_in_folder(seg_dir)
    if len(path_gt_labels) != len(path_segs):
        print('different number of files in data folders, had {} and {}'.format(len(path_gt_labels), len(path_segs)))

    # load labels list
    label_list, neutral_labels = utils.get_list_labels(label_list=path_label_list, FS_sort=True, labels_dir=gt_dir)
    label_list_sorted = np.sort(label_list)

    # initialise result matrix
    dice_coefs = np.zeros((label_list.shape[0], len(path_segs)))

    # loop over segmentations
    for idx, (path_gt, path_seg) in enumerate(zip(path_gt_labels, path_segs)):
        utils.print_loop_info(idx, len(path_segs), 10)

        # load gt labels and segmentation
        gt_labels = utils.load_volume(path_gt, dtype='int')
        seg = utils.load_volume(path_seg, dtype='int')
        # crop images
        gt_labels, cropping = edit_volumes.crop_volume_around_region(gt_labels, margin=10)
        seg = edit_volumes.crop_volume_with_idx(seg, cropping)
        # compute dice scores
        tmp_dice = fast_dice(gt_labels, seg, label_list_sorted)
        dice_coefs[:, idx] = tmp_dice[np.searchsorted(label_list_sorted, label_list)]

    # write dice results
    np.save(path_result_dice_array, dice_coefs)

    return dice_coefs
Пример #3
0
def dilate_lesions(labels_dir, result_dir, recompute=True):

    utils.mkdir(result_dir)

    path_labels = utils.list_images_in_folder(labels_dir)
    for path_label in path_labels:
        path_result_label = os.path.join(result_dir,
                                         os.path.basename(path_label))
        if (not os.path.isfile(path_result_label)) | recompute:

            label, aff, h = utils.load_volume(path_label, im_only=False)

            # define lesion, WM, and LV masks
            WM = (label == 2) | (label == 41)
            lesion = label == 77
            LV_and_lesion = (label == 4) | lesion

            # morphological operations to bridge the gaps between lesions and LV
            morph_struct = utils.build_binary_structure(2, len(WM.shape))
            LV_and_lesion = binary_dilation(LV_and_lesion, morph_struct)
            LV_and_lesion = binary_erosion(LV_and_lesion, morph_struct)
            lesion = (LV_and_lesion & WM) | lesion
            label[lesion] = 77

            # save new label maps
            utils.save_volume(label, aff, h, path_result_label)
Пример #4
0
def paste_lesions_on_buckner(lesion_dir,
                             buckner_dir,
                             result_dir,
                             dilate=2,
                             recompute=False):

    path_lesions = utils.list_images_in_folder(lesion_dir)
    path_buckners = utils.list_images_in_folder(buckner_dir)

    utils.mkdir(result_dir)

    # loop over buckner label maps
    loop_info = utils.LoopInfo(len(path_buckners), 1, 'processing', True)
    for idx_buckner, path_buckner in enumerate(path_buckners):
        loop_info.update(idx_buckner)
        buckner_name = os.path.basename(path_buckner).replace(
            '_seg', '').replace('.nii.gz', '')
        buckner = utils.load_volume(path_buckner)
        WM = (buckner == 2) | (buckner == 7) | (buckner == 16) | (
            buckner == 41) | (buckner == 46)

        # loop over challenge data
        for path_lesion in path_lesions:
            lesion_name = os.path.basename(path_lesion).replace(
                '.samseg_and_lesions.nii.gz', '')
            path_result = os.path.join(
                result_dir, buckner_name + '_' + lesion_name + '.nii.gz')
            if (not os.path.isfile(path_result)) | recompute:

                lesion = utils.load_volume(path_lesion)
                assert lesion.shape == buckner.shape, 'lesions should have same shape as buckner labels'

                # define lesion, WM, and LV masks
                lesion = (lesion == 77) & WM
                LV_and_lesion = (buckner == 4) | lesion

                # morphological operations to bridge the gaps between lesions and LV
                morph_struct = utils.build_binary_structure(
                    dilate, len(lesion.shape))
                lesion = binary_dilation(LV_and_lesion, morph_struct)
                lesion = binary_erosion(lesion, morph_struct)
                lesion = lesion & WM
                buckner_lesions = np.where(lesion, 77, buckner)

                # save map
                utils.save_volume(buckner_lesions, None, None, path_result)
Пример #5
0
def cross_validate_posteriors_threshold(list_seg_dir,
                                        list_posteriors_dir,
                                        list_gt_dir,
                                        list_thresholds,
                                        recompute=True):

    for fold_idx, (seg_dir, posteriors_dir, gt_dir) in enumerate(
            zip(list_seg_dir, list_posteriors_dir, list_gt_dir)):

        path_dice = os.path.join(os.path.dirname(seg_dir),
                                 'dice_lesions_for_thresholds.npy')
        path_dice_means = os.path.join(
            os.path.dirname(seg_dir), 'dice_lesions_means_for_thresholds.npy')
        if (not os.path.isfile(path_dice)) | (
                not os.path.isfile(path_dice_means)) | recompute:

            path_segs = [path for path in utils.list_images_in_folder(seg_dir)]
            path_posteriors = [
                path for path in utils.list_images_in_folder(posteriors_dir)
            ]
            path_gts = [path for path in utils.list_images_in_folder(gt_dir)]
            dice = np.zeros((len(list_thresholds), len(path_gts)))

            for subject_idx, (path_seg, path_post, path_gt) in enumerate(
                    zip(path_segs, path_posteriors, path_gts)):

                seg = utils.load_volume(path_seg)
                posteriors = utils.load_volume(path_post)
                gt = utils.load_volume(path_gt)

                seg[seg == 77] = 2
                for idx, threshold in enumerate(list_thresholds):
                    tmp_seg = deepcopy(seg)
                    lesion_mask = posteriors > threshold
                    tmp_seg[lesion_mask] = 77
                    dice[idx, subject_idx] = fast_dice(gt, tmp_seg, [77])

            np.save(path_dice, dice)
            np.save(path_dice_means, np.mean(dice, axis=1))

        dice_means = np.load(path_dice_means)
        max_threshold = list_thresholds[np.argmax(dice_means)]
        print('max threshold for fold {0}: {1:.2f}'.format(
            fold_idx, max_threshold))
Пример #6
0
def estimate_t2_cropping(image_dir, result_dir=None, dilation=5):
    """This function takes all the hippocampus images (with 2 channels) within the specified directory, and estimates
    the cropping dimensions around the hippocampus in the t2 channel.
    It returns the mean and sts deviation for the minimal and maximal croppings, proportional to image size.
    :param image_dir: path of the folder containing hippocampus images
    :param result_dir: if not None, path of the folder where to write the computed statistics.
    :param dilation: dilation coefficient used to extract full brain mask. Default is 5.
    :returns t2_cropping_stats: numpy vector of size 4 [mean min crop, std min crop, mean max crop, std max crop]
    """

    # create result dir
    if result_dir is not None:
        if not os.path.exists(result_dir):
            os.mkdir(result_dir)

    # loop through images
    list_image_paths = utils.list_images_in_folder(image_dir)
    max_cropping_proportions = np.zeros(len(list_image_paths))
    min_cropping_proportions = np.zeros(len(list_image_paths))
    for im_idx, image_path in enumerate(list_image_paths):
        utils.print_loop_info(im_idx, len(list_image_paths), 10)

        # load t2 channel
        im = utils.load_volume(image_path)
        t2 = im[..., 1]
        shape = t2.shape
        hdim = int(np.argmax(shape))

        # mask image
        _, mask = edit_volumes.mask_volume(t2,
                                           threshold=0,
                                           dilate=dilation,
                                           return_mask=True)

        # find cropping indices
        indices = np.nonzero(mask)[hdim]
        min_cropping_proportions[im_idx] = np.maximum(
            np.min(indices) + int(dilation / 2), 0) / shape[hdim]
        max_cropping_proportions[im_idx] = np.minimum(
            np.max(indices) - int(dilation / 2), shape[hdim]) / shape[hdim]

    # compute and save stats
    t2_cropping_stats = np.array([
        np.mean(min_cropping_proportions),
        np.std(min_cropping_proportions),
        np.mean(max_cropping_proportions),
        np.std(max_cropping_proportions)
    ])

    # save stats if necessary
    if result_dir is not None:
        np.save(os.path.join(result_dir, 't2_cropping_stats.npy'),
                t2_cropping_stats)

    return t2_cropping_stats
Пример #7
0
def build_model_inputs(path_images,
                       path_label_maps,
                       batchsize=1):

    # get label info
    _, _, n_dims, n_channels, _, _ = utils.get_volume_info(path_images[0])

    # Generate!
    while True:

        # randomly pick as many images as batchsize
        indices = npr.randint(len(path_label_maps), size=batchsize)

        # initialise input lists
        list_images = list()
        list_label_maps = list()

        for idx in indices:

            # add image
            image = utils.load_volume(path_images[idx], aff_ref=np.eye(4))
            if n_channels > 1:
                list_images.append(utils.add_axis(image, axis=0))
            else:
                list_images.append(utils.add_axis(image, axis=[0, -1]))

            # add labels
            labels = utils.load_volume(path_label_maps[idx], dtype='int', aff_ref=np.eye(4))
            list_label_maps.append(utils.add_axis(labels, axis=[0, -1]))

        # build list of inputs of augmentation model
        list_inputs = [list_images, list_label_maps]
        if batchsize > 1:  # concatenate individual input types if batchsize > 1
            list_inputs = [np.concatenate(item, 0) for item in list_inputs]
        else:
            list_inputs = [item[0] for item in list_inputs]

        yield list_inputs
Пример #8
0
def inter_rater_reproducibility_cross_val_exp(manual_seg_dir,
                                              ref_image_dir=None,
                                              recompute=True):

    # list subjects
    list_subjects = utils.list_subfolders(manual_seg_dir)

    # create result directories
    if ref_image_dir is not None:
        realigned_seg_dir = os.path.join(os.path.dirname(manual_seg_dir),
                                         'registered_to_t1')
        list_ref_subjects = utils.list_images_in_folder(ref_image_dir)
    else:
        realigned_seg_dir = os.path.join(os.path.dirname(manual_seg_dir),
                                         'realigned')
        list_ref_subjects = [None] * len(list_subjects)
    utils.mkdir(realigned_seg_dir)
    path_dice = os.path.join(realigned_seg_dir, 'dice.npy')

    # loop over subjects
    dice = list()
    if (not os.path.isfile(path_dice)) | recompute:
        for subject_dir, ref_subject in zip(list_subjects, list_ref_subjects):

            # align all images to first image
            if ref_subject is not None:
                ref_image = ref_subject
            else:
                ref_image = utils.list_images_in_folder(subject_dir)[0]
            result_dir = os.path.join(realigned_seg_dir,
                                      os.path.basename(subject_dir))
            edit_volumes.mri_convert_images_in_dir(subject_dir,
                                                   result_dir,
                                                   interpolation='nearest',
                                                   reference_dir=ref_image,
                                                   same_reference=True,
                                                   recompute=recompute)

            # load all volumes and compute distance maps
            list_segs = [
                utils.load_volume(path)
                for path in utils.list_images_in_folder(result_dir)
            ]
            list_distance_maps = [
                edit_volumes.compute_distance_map(labels, crop_margin=20)
                for labels in list_segs
            ]
            distance_maps = np.stack(list_distance_maps, axis=-1)
            n_raters = len(list_segs)

            # compare each segmentation to the consensus of all others
            tmp_dice = list()
            for i, seg in enumerate(list_segs):
                tmp_distance_maps = distance_maps[...,
                                                  np.arange(n_raters) != i]
                tmp_distance_maps = (np.mean(tmp_distance_maps, axis=-1) >
                                     0) * 1
                seg = (seg > 0) * 1
                tmp_dice.append(2 * np.sum(tmp_distance_maps * seg) /
                                (np.sum(tmp_distance_maps) + np.sum(seg)))
            dice.append(tmp_dice)

        np.save(path_dice, np.array(dice))
Пример #9
0
def prepare_hippo_training_atlases(labels_dir,
                                   result_dir,
                                   image_dir=None,
                                   image_result_dir=None,
                                   smooth=True,
                                   crop_margin=50,
                                   recompute=True):
    """This function prepares training label maps from CobraLab. It first crops each atlas around the right and left
    hippocampi, with a margin. It then equalises the shape of these atlases by croppping them to the size of the
    smallest hippocampus. Finally it realigns the obtained atlases to FS orientation axes.
    :param labels_dir: path of directory with label maps to prepare
    :param result_dir: path of directory where prepared atlases will be writen
    :param image_dir: (optional) path of directory with images corresponding to the label maps to prepare.
    This can be sued to prepare a dataset of real images for supervised training.
    :param image_result_dir: (optional) path of directory where images corresponding to prepared atlases will be writen
    :param smooth: (optional) whether to smooth the final cropped label maps
    :param crop_margin: (optional) margin to add around hippocampi when cropping
    :param recompute: (optional) whether to recompute result files even if they already exists"""

    # create results dir
    if not os.path.exists(result_dir):
        os.mkdir(result_dir)
    tmp_result_dir = os.path.join(result_dir, 'first_cropping')
    if not os.path.exists(tmp_result_dir):
        os.mkdir(tmp_result_dir)
    if image_dir is not None:
        assert image_result_dir is not None, 'image_result_dir should not be None if image_dir is specified'
        if not os.path.exists(image_result_dir):
            os.mkdir(image_result_dir)
        tmp_image_result_dir = os.path.join(image_result_dir, 'first_cropping')
        if not os.path.exists(tmp_image_result_dir):
            os.mkdir(tmp_image_result_dir)
    else:
        tmp_image_result_dir = None

    # list labels and images
    labels_paths = utils.list_images_in_folder(labels_dir)
    if image_dir is not None:
        path_images = utils.list_images_in_folder(image_dir)
    else:
        path_images = [None] * len(labels_paths)

    # crop all atlases around hippo
    print('\ncropping around hippo')
    shape_array = np.zeros((len(labels_paths)*2, 3))
    for idx, (path_label, path_image) in enumerate(zip(labels_paths, path_images)):
        utils.print_loop_info(idx, len(labels_paths), 1)

        # crop left hippo first
        path_label_first_crop_l = os.path.join(tmp_result_dir,
                                               os.path.basename(path_label).replace('.nii', '_left.nii'))
        lab, aff, h = utils.load_volume(path_label, im_only=False)
        lab_l, croppping_idx, aff_l = edit_volumes.crop_volume_around_region(lab, crop_margin,
                                                                             list(range(20101, 20109)), aff=aff)
        if (not os.path.exists(path_label_first_crop_l)) | recompute:
            utils.save_volume(lab_l, aff_l, h, path_label_first_crop_l)
        else:
            lab_l = utils.load_volume(path_label_first_crop_l)
        if path_image is not None:
            path_image_first_crop_l = os.path.join(tmp_image_result_dir,
                                                   os.path.basename(path_image).replace('.nii', '_left.nii'))
            if (not os.path.exists(path_image_first_crop_l)) | recompute:
                im, aff, h = utils.load_volume(path_image, im_only=False)
                im, aff = edit_volumes.crop_volume_with_idx(im, croppping_idx, aff)
                utils.save_volume(im, aff, h, path_image_first_crop_l)
        shape_array[2*idx, :] = np.array(lab_l.shape)

        # crop right hippo and flip them
        path_label_first_crop_r = os.path.join(tmp_result_dir,
                                               os.path.basename(path_label).replace('.nii', '_right_flipped.nii'))
        lab, aff, h = utils.load_volume(path_label, im_only=False)
        lab_r, croppping_idx, aff_r = edit_volumes.crop_volume_around_region(lab, crop_margin,
                                                                             list(range(20001, 20009)), aff=aff)
        if (not os.path.exists(path_label_first_crop_r)) | recompute:
            lab_r = edit_volumes.flip_volume(lab_r, direction='rl', aff=aff_r)
            utils.save_volume(lab_r, aff_r, h, path_label_first_crop_r)
        else:
            lab_r = utils.load_volume(path_label_first_crop_r)
        if path_image is not None:
            path_image_first_crop_r = os.path.join(tmp_image_result_dir,
                                                   os.path.basename(path_image).replace('.nii', '_right.nii'))
            if (not os.path.exists(path_image_first_crop_r)) | recompute:
                im, aff, h = utils.load_volume(path_image, im_only=False)
                im, aff = edit_volumes.crop_volume_with_idx(im, croppping_idx, aff)
                im = edit_volumes.flip_volume(im, direction='rl', aff=aff)
                utils.save_volume(im, aff, h, path_image_first_crop_r)
        shape_array[2*idx+1, :] = np.array(lab_r.shape)

    # list croppped files
    path_labels_first_cropped = utils.list_images_in_folder(tmp_result_dir)
    if tmp_image_result_dir is not None:
        path_images_first_cropped = utils.list_images_in_folder(tmp_image_result_dir)
    else:
        path_images_first_cropped = [None] * len(path_labels_first_cropped)

    # crop all label maps to same size
    print('\nequalising shapes')
    new_shape = np.min(shape_array, axis=0).astype('int32')
    for i, (path_label, path_image) in enumerate(zip(path_labels_first_cropped, path_images_first_cropped)):
        utils.print_loop_info(i, len(path_labels_first_cropped), 1)

        # get cropping indices
        path_lab_cropped = os.path.join(result_dir, os.path.basename(path_label))
        lab, aff, h = utils.load_volume(path_label, im_only=False)
        lab_shape = lab.shape
        min_cropping = np.array([np.maximum(int((lab_shape[i]-new_shape[i])/2), 0) for i in range(3)])
        max_cropping = np.array([min_cropping[i] + new_shape[i] for i in range(3)])

        # crop labels and realign on adni format
        if (not os.path.exists(path_lab_cropped)) | recompute:
            lab, aff = edit_volumes.crop_volume_with_idx(lab, np.concatenate([min_cropping, max_cropping]), aff)
            # realign on adni format
            lab = np.flip(lab, axis=2)
            aff[0:3, 0:3] = np.array([[-0.6, 0, 0], [0, 0, -0.6], [0, -0.6, 0]])
            utils.save_volume(lab, aff, h, path_lab_cropped)

        # crop image and realign on adni format
        if path_image is not None:
            path_im_cropped = os.path.join(image_result_dir, os.path.basename(path_image))
            if (not os.path.exists(path_im_cropped)) | recompute:
                im, aff, h = utils.load_volume(path_image, im_only=False)
                im, aff = edit_volumes.crop_volume_with_idx(im, np.concatenate([min_cropping, max_cropping]), aff)
                im = np.flip(im, axis=2)
                aff[0:3, 0:3] = np.array([[-0.6, 0, 0], [0, 0, -0.6], [0, -0.6, 0]])
                im = edit_volumes.mask_volume(im, lab)
                utils.save_volume(im, aff, h, path_im_cropped)

    # correct all labels to left values
    print('\ncorrecting labels')
    list_incorrect_labels = [77, 80, 251, 252, 253, 254, 255, 29, 41, 42, 43, 44, 46, 47, 49, 50, 51, 52, 54, 58, 60,
                             61, 62, 63, 7012, 20001, 20002, 20004, 20005, 20006, 20007, 20008]
    list_correct_labels = [2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 7, 8, 10, 11, 12, 13, 18, 26, 28, 2, 30, 31, 20108,
                           20101, 20102, 20104, 20105, 20106, 20107, 20108]
    edit_volumes.correct_labels_in_dir(result_dir, list_incorrect_labels, list_correct_labels, result_dir)

    # smooth labels
    if smooth:
        print('\nsmoothing labels')
        edit_volumes.smooth_labels_in_dir(result_dir, result_dir)
Пример #10
0
def preprocess_adni_hippo(path_t1,
                          path_t2,
                          path_aseg,
                          result_dir,
                          target_res,
                          padding_margin=85,
                          remove=False,
                          path_freesurfer='/usr/local/freesurfer/',
                          verbose=True,
                          recompute=True):
    """This function builds a T1+T2 multimodal image from the ADNI dataset.
    It first rescales intensities of each channel between 0 and 255.
    It then resamples the T2 image (which are 0.4*0.4*2.0 resolution) to target resolution.
    The obtained T2 is then padded in all directions by the padding_margin param (typically large 85).
    The T1 and aseg are then resampled like the T2 using mri_convert.
    Now that the T1, T2 and asegs are aligned and at the same resolution, we crop them around the right and left hippo.
    Finally, the T1 and T2 are concatenated into one single multimodal image.
    :param path_t1: path input T1 (typically at 1mm isotropic)
    :param path_t2: path input T2 (typically cropped around the hippo in sagittal axis, 0.4x0.4x2.0)
    :param path_aseg: path input segmentation (typically at 1mm isotropic)
    :param result_dir: path of directory where prepared images and labels will be writen.
    :param target_res: resolution at which to resample the label maps, and the images.
    Can be a number (isotropic resolution), a sequence, or a 1d numpy array.
    :param padding_margin: (optional) margin to add around hippocampi when cropping
    :param remove: (optional) whether to delete temporary files. Default is True.
    :param path_freesurfer: (optional) path of FreeSurfer home, to use mri_convert
    :param verbose: (optional) whether to print out mri_convert output when resampling images
    :param recompute: (optional) whether to recompute result files even if they already exists
    """

    # create results dir
    if not os.path.isdir(result_dir):
        os.mkdir(result_dir)

    path_test_im_right = os.path.join(result_dir, 'hippo_right.nii.gz')
    path_test_aseg_right = os.path.join(result_dir, 'hippo_right_aseg.nii.gz')
    path_test_im_left = os.path.join(result_dir, 'hippo_left.nii.gz')
    path_test_aseg_left = os.path.join(result_dir, 'hippo_left_aseg.nii.gz')
    if (not os.path.isfile(path_test_im_right)) | (not os.path.isfile(path_test_aseg_right)) | \
       (not os.path.isfile(path_test_im_left)) | (not os.path.isfile(path_test_aseg_left)) | recompute:

        # set up FreeSurfer
        os.environ['FREESURFER_HOME'] = path_freesurfer
        os.system(os.path.join(path_freesurfer, 'SetUpFreeSurfer.sh'))
        mri_convert = os.path.join(path_freesurfer, 'bin/mri_convert.bin')

        # rescale T1
        path_t1_rescaled = os.path.join(result_dir, 't1_rescaled.nii.gz')
        if (not os.path.isfile(path_t1_rescaled)) | recompute:
            im, aff, h = utils.load_volume(path_t1, im_only=False)
            im = edit_volumes.rescale_volume(im)
            utils.save_volume(im, aff, h, path_t1_rescaled)
        # rescale T2
        path_t2_rescaled = os.path.join(result_dir, 't2_rescaled.nii.gz')
        if (not os.path.isfile(path_t2_rescaled)) | recompute:
            im, aff, h = utils.load_volume(path_t2, im_only=False)
            im = edit_volumes.rescale_volume(im)
            utils.save_volume(im, aff, h, path_t2_rescaled)

        # resample T2 to target res
        path_t2_resampled = os.path.join(result_dir, 't2_rescaled_resampled.nii.gz')
        if (not os.path.isfile(path_t2_resampled)) | recompute:
            str_res = ' '.join([str(r) for r in utils.reformat_to_list(target_res, length=3)])
            cmd = mri_convert + ' ' + path_t2_rescaled + ' ' + path_t2_resampled + ' --voxsize ' + str_res
            cmd += ' -odt float'
            if not verbose:
                cmd += ' >/dev/null 2>&1'
            _ = os.system(cmd)

        # pad T2
        path_t2_padded = os.path.join(result_dir, 't2_rescaled_resampled_padded.nii.gz')
        if (not os.path.isfile(path_t2_padded)) | recompute:
            t2, aff, h = utils.load_volume(path_t2_resampled, im_only=False)
            t2_padded = np.pad(t2, padding_margin, 'constant')
            aff[:3, -1] = aff[:3, -1] - (aff[:3, :3] @ (padding_margin * np.ones((3, 1)))).T
            utils.save_volume(t2_padded, aff, h, path_t2_padded)

        # resample T1 and aseg accordingly
        path_t1_resampled = os.path.join(result_dir, 't1_rescaled_resampled.nii.gz')
        if (not os.path.isfile(path_t1_resampled)) | recompute:
            cmd = mri_convert + ' ' + path_t1_rescaled + ' ' + path_t1_resampled + ' -rl ' + path_t2_padded
            cmd += ' -odt float'
            if not verbose:
                cmd += ' >/dev/null 2>&1'
            _ = os.system(cmd)
        path_aseg_resampled = os.path.join(result_dir, 'aseg_resampled.nii.gz')
        if (not os.path.isfile(path_aseg_resampled)) | recompute:
            cmd = mri_convert + ' ' + path_aseg + ' ' + path_aseg_resampled + ' -rl ' + path_t2_padded
            cmd += ' -rt nearest -odt float'
            if not verbose:
                cmd += ' >/dev/null 2>&1'
            _ = os.system(cmd)

        # crop images and concatenate T1 and T2
        for lab, side in zip([17, 53], ['left', 'right']):
            path_test_image = os.path.join(result_dir, 'hippo_{}.nii.gz'.format(side))
            path_test_aseg = os.path.join(result_dir, 'hippo_{}_aseg.nii.gz'.format(side))
            if (not os.path.isfile(path_test_image)) | (not os.path.isfile(path_test_aseg)) | recompute:
                aseg, aff, h = utils.load_volume(path_aseg_resampled, im_only=False)
                tmp_aseg, cropping, tmp_aff = edit_volumes.crop_volume_around_region(aseg,
                                                                                     margin=30,
                                                                                     masking_labels=lab,
                                                                                     aff=aff)
                if side == 'right':
                    tmp_aseg = edit_volumes.flip_volume(tmp_aseg, direction='rl', aff=tmp_aff)
                utils.save_volume(tmp_aseg, tmp_aff, h, path_test_aseg)
                if (not os.path.isfile(path_test_image)) | recompute:
                    t1 = utils.load_volume(path_t1_resampled)
                    t1 = edit_volumes.crop_volume_with_idx(t1, crop_idx=cropping)
                    t1 = edit_volumes.mask_volume(t1, tmp_aseg, dilate=6, erode=5)
                    t2 = utils.load_volume(path_t2_padded)
                    t2 = edit_volumes.crop_volume_with_idx(t2, crop_idx=cropping)
                    t2 = edit_volumes.mask_volume(t2, tmp_aseg, dilate=6, erode=5)
                    if side == 'right':
                        t1 = edit_volumes.flip_volume(t1, direction='rl', aff=tmp_aff)
                        t2 = edit_volumes.flip_volume(t2, direction='rl', aff=tmp_aff)
                    test_image = np.stack([t1, t2], axis=-1)
                    utils.save_volume(test_image, tmp_aff, h, path_test_image)

        # remove unnecessary files
        if remove:
            list_files_to_remove = [path_t1_rescaled,
                                    path_t2_rescaled,
                                    path_t2_resampled,
                                    path_t2_padded,
                                    path_t1_resampled,
                                    path_aseg_resampled]
            for path in list_files_to_remove:
                os.remove(path)
Пример #11
0
def dice_evaluation(gt_dir,
                    seg_dir,
                    label_list,
                    compute_distances=False,
                    compute_score_whole_structure=False,
                    path_dice=None,
                    path_hausdorff=None,
                    path_mean_distance=None,
                    crop_margin_around_gt=10,
                    recompute=True,
                    verbose=True):
    """This function computes Dice scores between two sets of labels maps in gt_dir (ground truth) and seg_dir
    (typically predictions). Labels maps in both folders are matched by sorting order.
    :param gt_dir: path of directory with gt label maps
    :param seg_dir: path of directory with label maps to compare to gt_dir. Matched to gt label maps by sorting order.
    :param label_list: list of label values for which to compute evaluation metrics. Can be a sequence, a 1d numpy
    array, or the path to such array.
    :param compute_distances: (optional) whether to compute distances (Hausdorff and mean distance) between the surfaces
    of GT and predicted labels. Default is False.
    :param compute_score_whole_structure: (optional) whether to also compute the selected scores for the whole segmented
    structure (i.e. scores are computed for a single structure obtained by regrouping all non-zero values). If True, the
    resulting scores are added as an extra row to the result matrices. Default is False.
    :param path_dice: path where the resulting Dice will be writen as numpy array.
    Default is None, where the array is not saved.
    :param path_hausdorff: path where the resulting Hausdorff distances will be writen as numpy array (only if
    compute_distances is True). Default is None, where the array is not saved.
    :param path_mean_distance: path where the resulting mean distances will be writen as numpy array (only if
    compute_distances is True). Default is None, where the array is not saved.
    :param crop_margin_around_gt: (optional) margin by which to crop around the gt volumes, in order to copute the
    scores more efficiently. If None, no cropping is performed.
    :param recompute: (optional) whether to recompute the already existing results. Default is True.
    :param verbose: (optional) whether to print out info about the remaining number of cases.
    :return: numpy array containing all Dice scores (labels in rows, subjects in columns). Also returns numpy arrays
    with the same structures for Hausdorff and mean distances if compute_distances is True.
    """

    # check whether to recompute
    compute_dice = not os.path.isfile(path_dice) if (path_dice
                                                     is not None) else True
    if compute_distances:
        compute_hausdorff = not os.path.isfile(path_hausdorff) if (
            path_hausdorff is not None) else True
        compute_mean_dist = not os.path.isfile(path_mean_distance) if (
            path_mean_distance is not None) else True
    else:
        compute_hausdorff = compute_mean_dist = False

    if compute_dice | compute_hausdorff | compute_mean_dist | recompute:

        # get list label maps to compare
        path_gt_labels = utils.list_images_in_folder(gt_dir)
        path_segs = utils.list_images_in_folder(seg_dir)
        if len(path_gt_labels) != len(path_segs):
            print(
                'gt and segmentation folders must have the same amount of label maps.'
            )

        # load labels list
        label_list, _ = utils.get_list_labels(label_list=label_list,
                                              FS_sort=True,
                                              labels_dir=gt_dir)
        n_labels = len(label_list)

        # initialise result matrices
        if compute_score_whole_structure:
            max_dists = np.zeros((n_labels + 1, len(path_segs)))
            mean_dists = np.zeros((n_labels + 1, len(path_segs)))
            dice_coefs = np.zeros((n_labels + 1, len(path_segs)))
        else:
            max_dists = np.zeros((n_labels, len(path_segs)))
            mean_dists = np.zeros((n_labels, len(path_segs)))
            dice_coefs = np.zeros((n_labels, len(path_segs)))

        # loop over segmentations
        loop_info = utils.LoopInfo(len(path_segs), 10, 'evaluating')
        for idx, (path_gt,
                  path_seg) in enumerate(zip(path_gt_labels, path_segs)):
            if verbose:
                loop_info.update(idx)

            # load gt labels and segmentation
            gt_labels = utils.load_volume(path_gt, dtype='int')
            seg = utils.load_volume(path_seg, dtype='int')

            # crop images
            if crop_margin_around_gt is not None:
                gt_labels, cropping = edit_volumes.crop_volume_around_region(
                    gt_labels, margin=crop_margin_around_gt)
                seg = edit_volumes.crop_volume_with_idx(seg, cropping)

            # compute Dice scores
            dice_coefs[:n_labels, idx] = fast_dice(gt_labels, seg, label_list)

            # compute Dice scores for whole structures
            if compute_score_whole_structure:
                temp_gt = (gt_labels > 0) * 1
                temp_seg = (seg > 0) * 1
                dice_coefs[-1, idx] = dice(temp_gt, temp_seg)
            else:
                temp_gt = temp_seg = None

            # compute average and Hausdorff distances
            if compute_distances:

                # compute unique label values
                unique_gt_labels = np.unique(gt_labels)
                unique_seg_labels = np.unique(seg)

                # compute max/mean surface distances for all labels
                for index, label in enumerate(label_list):
                    if (label in unique_gt_labels) & (label
                                                      in unique_seg_labels):
                        mask_gt = np.where(gt_labels == label, True, False)
                        mask_seg = np.where(seg == label, True, False)
                        max_dists[index,
                                  idx], mean_dists[index,
                                                   idx] = surface_distances(
                                                       mask_gt, mask_seg)
                    else:
                        max_dists[index, idx] = max(gt_labels.shape)
                        mean_dists[index, idx] = max(gt_labels.shape)

                # compute max/mean distances for whole structure
                if compute_score_whole_structure:
                    max_dists[-1, idx], mean_dists[-1,
                                                   idx] = surface_distances(
                                                       temp_gt, temp_seg)

        # write results
        if path_dice is not None:
            utils.mkdir(os.path.dirname(path_dice))
            np.save(path_dice, dice_coefs)
        if compute_distances and path_hausdorff is not None:
            utils.mkdir(os.path.dirname(path_hausdorff))
            np.save(path_hausdorff, max_dists)
        if compute_distances and path_mean_distance is not None:
            utils.mkdir(os.path.dirname(path_mean_distance))
            np.save(path_mean_distance, mean_dists)

    else:
        dice_coefs = np.load(path_dice)
        if compute_distances:
            max_dists = np.load(path_hausdorff)
            mean_dists = np.load(path_mean_distance)
        else:
            max_dists = mean_dists = None

    if compute_distances:
        return dice_coefs, max_dists, mean_dists
    else:
        return dice_coefs, None, None
Пример #12
0
def dice_evaluation(gt_folder, seg_folder, path_segmentation_label_list,
                    path_result_dice_array):
    """Computes Dice scores for all labels contained in path_segmentation_label_list. Files in gt_folder and seg_folder
    are matched by sorting order.
    :param gt_folder: folder containing ground truth files.
    :param seg_folder: folder containing evaluation files.
    :param path_segmentation_label_list: path of numpy vector containing all labels to compute the Dice for.
    :param path_result_dice_array: path where the resulting Dice will be writen as numpy array.
    :return: numpy array containing all dice scores (labels in rows, subjects in columns).
    """

    # get list of automated and manual segmentations
    list_path_gt_labels = utils.list_images_in_folder(gt_folder)
    list_path_segs = utils.list_images_in_folder(seg_folder)
    if len(list_path_gt_labels) != len(list_path_segs):
        warnings.warn(
            'both data folders should have the same length, had {} and {}'.
            format(len(list_path_gt_labels), len(list_path_segs)))

    # load labels list
    label_list, neutral_labels = utils.get_list_labels(
        label_list=path_segmentation_label_list,
        FS_sort=True,
        labels_dir=gt_folder)

    # create result folder
    if not os.path.exists(os.path.dirname(path_result_dice_array)):
        os.mkdir(os.path.dirname(path_result_dice_array))

    # initialise result matrix
    dice_coefs = np.zeros((label_list.shape[0], len(list_path_segs)))

    # start analysis
    for im_idx, (path_gt, path_seg) in enumerate(
            zip(list_path_gt_labels, list_path_segs)):
        utils.print_loop_info(im_idx, len(list_path_segs), 10)

        # load gt labels and segmentation
        gt_labels = utils.load_volume(path_gt, dtype='int')
        seg = utils.load_volume(path_seg, dtype='int')
        n_dims = len(gt_labels.shape)
        # crop images
        gt_labels, cropping = edit_volumes.crop_volume_around_region(gt_labels,
                                                                     margin=10)
        if n_dims == 2:
            seg = seg[cropping[0]:cropping[2], cropping[1]:cropping[3]]
        elif n_dims == 3:
            seg = seg[cropping[0]:cropping[3], cropping[1]:cropping[4],
                      cropping[2]:cropping[5]]
        else:
            raise Exception(
                'cannot evaluate images with more than 3 dimensions')
        # extract list of unique labels
        label_list_sorted = np.sort(label_list)
        tmp_dice = fast_dice(gt_labels, seg, label_list_sorted)
        dice_coefs[:,
                   im_idx] = tmp_dice[np.searchsorted(label_list_sorted,
                                                      label_list)]

    # write dice results
    np.save(path_result_dice_array, dice_coefs)

    return dice_coefs
Пример #13
0
def build_model_input_generator(images_paths,
                                labels_paths,
                                n_channels,
                                im_shape,
                                scaling_range=None,
                                rotation_range=None,
                                shearing_range=None,
                                nonlin_shape_fact=0.0625,
                                nonlin_std_dev=3,
                                batch_size=1):

    # Generate!
    while True:

        # randomly pick as many images as batch_size
        indices = npr.randint(len(images_paths), size=batch_size)

        # initialise input tensors
        images_all = []
        labels_all = []
        aff_all = []
        nonlinear_field_all = []

        for idx in indices:

            # add image
            image = utils.load_volume(images_paths[idx])
            if n_channels > 1:
                images_all.append(utils.add_axis(image, axis=0))
            else:
                images_all.append(utils.add_axis(image, axis=-2))

            # add labels
            labels = utils.load_volume(labels_paths[idx], dtype='int')
            labels_all.append(utils.add_axis(labels, axis=-2))

            # get affine transformation: rotate, scale, shear (translation done during random cropping)
            n_dims, _ = utils.get_dims(im_shape)
            scaling = utils.draw_value_from_distribution(scaling_range, size=n_dims, centre=1, default_range=.15)
            if n_dims == 2:
                rotation_angle = utils.draw_value_from_distribution(rotation_range, default_range=15.0)
            else:
                rotation_angle = utils.draw_value_from_distribution(rotation_range, size=n_dims, default_range=15.0)
            shearing = utils.draw_value_from_distribution(shearing_range, size=n_dims ** 2 - n_dims, default_range=.01)
            aff = utils.create_affine_transformation_matrix(n_dims, scaling, rotation_angle, shearing)
            aff_all.append(utils.add_axis(aff))

            # add non linear field
            deform_shape = utils.get_resample_shape(im_shape, nonlin_shape_fact, len(im_shape))
            nonlinear_field = npr.normal(loc=0, scale=nonlin_std_dev * npr.rand(), size=deform_shape)
            nonlinear_field_all.append(utils.add_axis(nonlinear_field))

        # build list of inputs of the augmentation model
        inputs_vals = [images_all, labels_all, aff_all, nonlinear_field_all]

        # put images and labels (concatenated if batch_size>1) into a tuple of 2 elements: (cat_images, cat_labels)
        if batch_size > 1:
            inputs_vals = [np.concatenate(item, 0) for item in inputs_vals]
        else:
            inputs_vals = [item[0] for item in inputs_vals]

        yield inputs_vals
Пример #14
0
def sample_intensity_stats_from_single_dataset(image_dir, labels_dir, labels_list, classes_list=None, max_channel=3,
                                               rescale=True):
    """This function aims at estimating the intensity distributions of K different structure types from a set of images.
    The distribution of each structure type is modelled as a Gaussian, parametrised by a mean and a standard deviation.
    Because the intensity distribution of structures can vary accross images, we additionally use Gausian priors for the
    parameters of each Gaussian distribution. Therefore, the intensity distribution of each structure type is described
    by 4 parameters: a mean/std for the mean intensity, and a mean/std for the std deviation.
    This function uses a set of images along with corresponding segmentations to estimate the 4*K parameters.
    Structures can share the same statistics by being regrouped into classes of similar structure types.
    Images can be multi-modal (n_channels), in which case different statistics are estimated for each modality.
    :param image_dir: path of directory with images to estimate the intensity distribution
    :param labels_dir: path of directory with segmentation of input images.
    They are matched with images by sorting order.
    :param labels_list: list of labels for which to evaluate mean and std intensity.
    Can be a sequence, a 1d numpy array, or the path to a 1d numpy array.
    :param classes_list: (optional) enables to regroup structures into classes of similar intensity statistics.
    Intenstites associated to regrouped labels will thus contribute to the same Gaussian during statistics estimation.
    Can be a sequence, a 1d numpy array, or the path to a 1d numpy array.
    It should have the same length as labels_list, and contain values between 0 and K-1, where K is the total number of
    classes. Default is all labels have different classes (K=len(labels_list)).
    :param max_channel: (optional) maximum number of channels to consider if the data is multispectral. Default is 3.
    :param rescale: (optional) whether to rescale images between 0 and 255 before intensity estimation
    :return: 2 numpy arrays of size (2*n_channels, K), one with the evaluated means/std for the mean
    intensity, and one for the mean/std for the standard deviation.
    Each block of two rows correspond to a different modality (channel). For each block of two rows, the first row
    represents the mean, and the second represents the std.
    """

    # list files
    path_images = utils.list_images_in_folder(image_dir)
    path_labels = utils.list_images_in_folder(labels_dir)
    assert len(path_images) == len(path_labels), 'image and labels folders do not have the same number of files'

    # reformat list labels and classes
    labels_list = np.array(utils.reformat_to_list(labels_list, load_as_numpy=True, dtype='int'))
    if classes_list is not None:
        classes_list = np.array(utils.reformat_to_list(classes_list, load_as_numpy=True, dtype='int'))
    else:
        classes_list = np.arange(labels_list.shape[0])
    assert len(classes_list) == len(labels_list), 'labels and classes lists should have the same length'

    # get unique classes
    unique_classes, unique_indices = np.unique(classes_list, return_index=True)
    n_classes = len(unique_classes)
    if not np.array_equal(unique_classes, np.arange(n_classes)):
        raise ValueError('classes_list should only contain values between 0 and K-1, '
                         'where K is the total number of classes. Here K = %d' % n_classes)

    # initialise result arrays
    n_dims, n_channels = utils.get_dims(utils.load_volume(path_images[0]).shape, max_channels=max_channel)
    means = np.zeros((len(path_images), n_classes, n_channels))
    stds = np.zeros((len(path_images), n_classes, n_channels))

    # loop over images
    loop_info = utils.LoopInfo(len(path_images), 10, 'estimating', print_time=True)
    for idx, (path_im, path_la) in enumerate(zip(path_images, path_labels)):
        loop_info.update(idx)

        # load image and label map
        image = utils.load_volume(path_im)
        la = utils.load_volume(path_la)
        if n_channels == 1:
            image = utils.add_axis(image, -1)

        # loop over channels
        for channel in range(n_channels):
            im = image[..., channel]
            if rescale:
                im = edit_volumes.rescale_volume(im)
            stats = sample_intensity_stats_from_image(im, la, labels_list, classes_list=classes_list)
            means[idx, :, channel] = stats[0, :]
            stds[idx, :, channel] = stats[1, :]

    # compute prior parameters for mean/std
    mean_means = np.mean(means, axis=0)
    std_means = np.std(means, axis=0)
    mean_stds = np.mean(stds, axis=0)
    std_stds = np.std(stds, axis=0)

    # regroup prior parameters in two different arrays: one for the mean and one for the std
    prior_means = np.zeros((2 * n_channels, n_classes))
    prior_stds = np.zeros((2 * n_channels, n_classes))
    for channel in range(n_channels):
        prior_means[2 * channel, :] = mean_means[:, channel]
        prior_means[2 * channel + 1, :] = std_means[:, channel]
        prior_stds[2 * channel, :] = mean_stds[:, channel]
        prior_stds[2 * channel + 1, :] = std_stds[:, channel]

    return prior_means, prior_stds
Пример #15
0
else:
    assert os.path.isfile(path_t1_images), "files does not exist: %s " \
                                        "\nplease make sure the path and the extension are correct" % path_t1_images
    images_to_segment_t1 = [path_t1_images]
    images_to_segment_t2 = [path_t2_images]
    path_predictions = [path_predictions]

# Do the actual work
print('Found %d images' % len(images_to_segment_t1))
for idx, (path_image_t1, path_image_t2, path_prediction) in enumerate(
        zip(images_to_segment_t1, images_to_segment_t2, path_predictions)):
    print('  Working on image %d ' % (idx + 1))
    print('  ' + path_image_t1 + ', ' + path_image_t2)

    im1, aff1, hdr1 = utils.load_volume(path_image_t1,
                                        im_only=False,
                                        dtype='float')
    im1, aff1 = edit_volumes.resample_volume(im1, aff1, [1.0, 1.0, 1.0])
    aff_ref = np.eye(4)
    im1, aff1_mod = edit_volumes.align_volume_to_ref(im1,
                                                     aff1,
                                                     aff_ref=aff_ref,
                                                     return_aff=True,
                                                     n_dims=3)
    im2, aff2, hdr2 = utils.load_volume(path_image_t2,
                                        im_only=False,
                                        dtype='float')
    im2 = edit_volumes.resample_volume_like(im1, aff1_mod, im2, aff2)

    minimum = np.min(im1)
    im1 = im1 - minimum
Пример #16
0
def build_model_inputs(path_label_maps,
                       n_labels,
                       batchsize=1,
                       n_channels=1,
                       generation_classes=None,
                       prior_distributions='uniform',
                       prior_means=None,
                       prior_stds=None,
                       use_specific_stats_for_channel=False,
                       mix_prior_and_random=False):
    """
    This function builds a generator to be fed to the lab2im model. It enables to generate all the required inputs,
    according to the operations performed in the model.
    :param path_label_maps: list of the paths of the input label maps.
    :param n_labels: number of labels in the input label maps.
    :param batchsize: (optional) numbers of images to generate per mini-batch. Default is 1.
    :param n_channels: (optional) number of channels to be synthetised. Default is 1.
    :param generation_classes: (optional) Indices regrouping generation labels into classes of same intensity
    distribution. Regouped labels will thus share the same Gaussian when samling a new image. Can be a sequence or a
    1d numpy array. It should have the same length as generation_labels, and contain values between 0 and K-1, where K
    is the total number of classes. Default is all labels have different classes.
    :param prior_distributions: (optional) type of distribution from which we sample the GMM parameters.
    Can either be 'uniform', or 'normal'. Default is 'uniform'.
    :param prior_means: (optional) hyperparameters controlling the prior distributions of the GMM means. Because
    these prior distributions are uniform or normal, they require by 2 hyperparameters. Thus prior_means can be:
    1) a sequence of length 2, directly defining the two hyperparameters: [min, max] if prior_distributions is
    uniform, [mean, std] if the distribution is normal. The GMM means of are independently sampled at each
    mini_batch from the same distribution.
    2) an array of shape (2, K), where K is the number of classes (K=len(generation_labels) if generation_classes is
    not given). The mean of the Gaussian distribution associated to class k in [0, ...K-1] is sampled at each mini-batch
    from U(prior_means[0,k], prior_means[1,k]) if prior_distributions is uniform, or from
    N(prior_means[0,k], prior_means[1,k]) if prior_distributions is normal.
    3) an array of shape (2*n_mod, K), where each block of two rows is associated to hyperparameters derived
    from different modalities. In this case, if use_specific_stats_for_channel is False, we first randomly select a
    modality from the n_mod possibilities, and we sample the GMM means like in 2).
    If use_specific_stats_for_channel is True, each block of two rows correspond to a different channel
    (n_mod=n_channels), thus we select the corresponding block to each channel rather than randomly drawing it.
    4) the path to such a numpy array.
    Default is None, which corresponds to prior_means = [25, 225].
    :param prior_stds: (optional) same as prior_means but for the standard deviations of the GMM.
    Default is None, which corresponds to prior_stds = [5, 25].
    :param use_specific_stats_for_channel: (optional) whether the i-th block of two rows in the prior arrays must be
    only used to generate the i-th channel. If True, n_mod should be equal to n_channels. Default is False.
    :param mix_prior_and_random: (optional) if prior_means is not None, enables to reset the priors to their default
    values for half of thes cases, and thus generate images of random contrast.
    """

    # get label info
    _, _, n_dims, _, _, _ = utils.get_volume_info(path_label_maps[0])

    # allocate unique class to each label if generation classes is not given
    if generation_classes is None:
        generation_classes = np.arange(n_labels)

    # Generate!
    while True:

        # randomly pick as many images as batchsize
        indices = npr.randint(len(path_label_maps), size=batchsize)

        # initialise input lists
        list_label_maps = []
        list_means = []
        list_stds = []

        for idx in indices:

            # add labels to inputs
            lab = utils.load_volume(path_label_maps[idx], dtype='int', aff_ref=np.eye(4))
            list_label_maps.append(utils.add_axis(lab, axis=[0, -1]))

            # add means and standard deviations to inputs
            means = np.empty((1, n_labels, 0))
            stds = np.empty((1, n_labels, 0))
            for channel in range(n_channels):

                # retrieve channel specific stats if necessary
                if isinstance(prior_means, np.ndarray):
                    if (prior_means.shape[0] > 2) & use_specific_stats_for_channel:
                        if prior_means.shape[0] / 2 != n_channels:
                            raise ValueError("the number of blocks in prior_means does not match n_channels. This "
                                             "message is printed because use_specific_stats_for_channel is True.")
                        tmp_prior_means = prior_means[2 * channel:2 * channel + 2, :]
                    else:
                        tmp_prior_means = prior_means
                else:
                    tmp_prior_means = prior_means
                if (prior_means is not None) & mix_prior_and_random & (npr.uniform() > 0.5):
                    tmp_prior_means = None
                if isinstance(prior_stds, np.ndarray):
                    if (prior_stds.shape[0] > 2) & use_specific_stats_for_channel:
                        if prior_stds.shape[0] / 2 != n_channels:
                            raise ValueError("the number of blocks in prior_stds does not match n_channels. This "
                                             "message is printed because use_specific_stats_for_channel is True.")
                        tmp_prior_stds = prior_stds[2 * channel:2 * channel + 2, :]
                    else:
                        tmp_prior_stds = prior_stds
                else:
                    tmp_prior_stds = prior_stds
                if (prior_stds is not None) & mix_prior_and_random & (npr.uniform() > 0.5):
                    tmp_prior_stds = None

                # draw means and std devs from priors
                tmp_classes_means = utils.draw_value_from_distribution(tmp_prior_means, n_labels, prior_distributions,
                                                                       125., 100., positive_only=True)
                tmp_classes_stds = utils.draw_value_from_distribution(tmp_prior_stds, n_labels, prior_distributions,
                                                                      15., 10., positive_only=True)
                if npr.uniform() > 0.95:  # reset the background to 0 in 10% of cases
                    tmp_classes_means[0] = 0
                    tmp_classes_stds[0] = 0
                tmp_means = utils.add_axis(tmp_classes_means[generation_classes], axis=[0, -1])
                tmp_stds = utils.add_axis(tmp_classes_stds[generation_classes], axis=[0, -1])
                means = np.concatenate([means, tmp_means], axis=-1)
                stds = np.concatenate([stds, tmp_stds], axis=-1)
            list_means.append(means)
            list_stds.append(stds)

        # build list of inputs for generation model
        list_inputs = [list_label_maps, list_means, list_stds]
        if batchsize > 1:  # concatenate each input type if batchsize > 1
            list_inputs = [np.concatenate(item, 0) for item in list_inputs]
        else:
            list_inputs = [item[0] for item in list_inputs]

        yield list_inputs
Пример #17
0
def preprocess_asegs(aseg_dir,
                     lesion_gt_dir,
                     list_incorrect,
                     list_correct,
                     lesion_label_in_gt=77,
                     dilate=2,
                     recompute=False):

    # align asegs to gt dir (cropping to same dimension)
    cropped_dir = aseg_dir + '_cropped'
    edit_volumes.mri_convert_images_in_dir(aseg_dir,
                                           cropped_dir,
                                           interpolation='nearest',
                                           reference_dir=lesion_gt_dir,
                                           recompute=recompute)

    # correct for aseg labels
    corrected_dir = cropped_dir + '_corrected'
    edit_volumes.correct_labels_in_dir(cropped_dir,
                                       list_incorrect,
                                       list_correct,
                                       corrected_dir,
                                       smooth=False,
                                       recompute=recompute)

    # list gt and aseg, and create result dir
    list_lesion_labels = utils.list_images_in_folder(lesion_gt_dir)
    list_aseg_labels = utils.list_images_in_folder(corrected_dir)
    inpainted_dir = corrected_dir + '_lesion_inpainted'
    utils.mkdir(inpainted_dir)

    # loop over subjects
    for path_lesion_label, path_aseg_label in zip(list_lesion_labels,
                                                  list_aseg_labels):
        path_result = os.path.join(inpainted_dir,
                                   os.path.basename(path_aseg_label))
        if (not os.path.isfile(path_result)) | recompute:

            # paste lesion label
            lesions = utils.load_volume(path_lesion_label)
            aseg_label, aff, h = utils.load_volume(path_aseg_label,
                                                   im_only=False)
            lesion_mask = lesions == lesion_label_in_gt
            aseg_label[lesion_mask] = 77
            utils.save_volume(aseg_label, aff, h, path_result)

    # dilate lesion and ventricle
    dilated_dir = inpainted_dir + '_dilated'
    utils.mkdir(dilated_dir)
    list_inpainted_aseg = utils.list_images_in_folder(inpainted_dir)
    for path_aseg in list_inpainted_aseg:

        path_result = os.path.join(dilated_dir, os.path.basename(path_aseg))
        if (not os.path.isfile(path_result)) | recompute:

            # define lesion, WM, and LV masks
            aseg, aff, h = utils.load_volume(path_aseg, im_only=False)
            WM = aseg == 2
            LV = aseg == 4
            lesion = aseg == 77

            # morphological operations to bridge the gaps between lesions and LV
            morph_struct = utils.build_binary_structure(
                dilate, len(aseg.shape))
            dilated_LV_or_lesion = binary_dilation(LV | lesion, morph_struct)
            filled_LV_or_lesion = binary_erosion(dilated_LV_or_lesion,
                                                 morph_struct)
            LV = LV | (filled_LV_or_lesion & WM)
            aseg[LV] = 4

            # save map
            utils.save_volume(aseg, aff, h, path_result)
Пример #18
0
    ]

else:
    assert os.path.isfile(path_images), "files does not exist: %s " \
                                        "\nplease make sure the path and the extension are correct" % path_images
    images_to_segment = [path_images]
    path_predictions = [path_predictions]

# Do the actual work
print('Found %d images' % len(images_to_segment))
for idx, (path_image, path_prediction) in enumerate(
        zip(images_to_segment, path_predictions)):
    print('  Working on image %d ' % (idx + 1))
    print('  ' + path_image)

    im, aff, hdr = utils.load_volume(path_image, im_only=False, dtype='float')
    if args['ct']:
        im[im < 0] = 0
        im[im > 80] = 80
    im, aff = edit_volumes.resample_volume(im, aff, [1.0, 1.0, 1.0])
    aff_ref = np.eye(4)
    im, aff2 = edit_volumes.align_volume_to_ref(im,
                                                aff,
                                                aff_ref=aff_ref,
                                                return_aff=True,
                                                n_dims=3)
    im = im - np.min(im)
    im = im / np.max(im)
    I = im[np.newaxis, ..., np.newaxis]
    W = (np.ceil(np.array(I.shape[1:-1]) / 32.0) * 32).astype('int')
    idx = np.floor((W - I.shape[1:-1]) / 2).astype('int')
Пример #19
0
def postprocess_samseg(list_samseg_dir,
                       list_gt_dir,
                       path_segmentation_labels,
                       incorrect_labels,
                       correct_labels,
                       list_posteriors_dir=None,
                       list_thresholds=None,
                       recompute=False):
    """ This function processes the samseg segmentations: it corrects the labels (right/left and 99 to 77), resamples
    them to the space of gt_dir, and computes the Dice scores for 1) all_subjects vs. testing subjects only, and 2) all
    ROIs vs. lesions only.
    It requires that all segmentations are sorted in three subfolders inside samseg_main_dir: t1, flair, and t1_flair.
    IMPORTANT: Images are expected to have to following naming convention: <subject_id>.samseg.<contrast>.lesion.mgz,
    where <contrast> must either be t1, flair, ***t1_flair***
    :param list_samseg_dir: main samseg dir containing the three subfolders t1, flair, t1_flair
    :param list_gt_dir: folder with the gt label maps for all subjects
    :param path_segmentation_labels: list of segmentation labels
    :param incorrect_labels: list of samseg incorrect labels
    :param correct_labels: list of labels to correct the wrong one with
    :param recompute: whether to recompute files
    """

    if list_posteriors_dir is None:
        list_posteriors_dir = [None] * len(list_samseg_dir)

    for samseg_dir, gt_dir, posteriors_dir, threshold in zip(
            list_samseg_dir, list_gt_dir, list_posteriors_dir,
            list_thresholds):

        # define result directories
        samseg_corrected_dir = samseg_dir + '_corrected'
        samseg_preprocessed_dir = samseg_dir + '_preprocessed'
        if (not os.path.isdir(samseg_preprocessed_dir)) | recompute:

            # regroup right/left labels and change 99 to 77
            edit_volumes.correct_labels_in_dir(samseg_dir,
                                               incorrect_labels,
                                               correct_labels,
                                               samseg_corrected_dir,
                                               recompute=recompute)

            # resample to gt format
            edit_volumes.mri_convert_images_in_dir(samseg_corrected_dir,
                                                   samseg_preprocessed_dir,
                                                   interpolation='nearest',
                                                   reference_dir=gt_dir,
                                                   recompute=recompute)

        # replace lesions by thresholded lesion posteriors
        if posteriors_dir is not None:

            # resample posteriors to gt format
            posteriors_preprocessed_dir = posteriors_dir + '_preprocessed'
            edit_volumes.mri_convert_images_in_dir(posteriors_dir,
                                                   posteriors_preprocessed_dir,
                                                   reference_dir=gt_dir,
                                                   recompute=recompute)

            # list hard segmentations and posteriors
            samseg_postprocessed_dir = samseg_dir + '_postprocessed'
            utils.mkdir(samseg_postprocessed_dir)
            path_segs = [
                path for path in utils.list_images_in_folder(
                    samseg_preprocessed_dir)
            ]
            path_posteriors = [
                path for path in utils.list_images_in_folder(
                    posteriors_preprocessed_dir)
            ]

            for subject_idx, (path_seg, path_post) in enumerate(
                    zip(path_segs, path_posteriors)):
                path_result = os.path.join(samseg_postprocessed_dir,
                                           os.path.basename(path_seg))
                if (not os.path.isfile(path_result)) | recompute:

                    # replace segmented lesions by thresholded posteriors
                    seg, aff, h = utils.load_volume(path_seg, im_only=False)
                    posteriors = utils.load_volume(path_post)
                    seg[seg == 77] = 2
                    seg[posteriors > threshold] = 77
                    utils.save_volume(seg, aff, h, path_result)

        else:
            samseg_postprocessed_dir = samseg_preprocessed_dir

        # compute dice scores with
        path_dice_testing = os.path.join(samseg_postprocessed_dir, 'dice.npy')
        path_dice_lesions_testing = os.path.join(samseg_postprocessed_dir,
                                                 'dice_lesions.npy')
        if (not os.path.isfile(path_dice_testing)) | recompute:
            dice_evaluation(gt_dir, samseg_postprocessed_dir,
                            path_segmentation_labels, path_dice_testing)
        if (not os.path.isfile(path_dice_lesions_testing)) | recompute:
            dice = np.load(path_dice_testing)
            np.save(path_dice_lesions_testing, dice[4, :])
Пример #20
0
def build_model_inputs(path_label_maps,
                       n_labels,
                       batchsize=1,
                       n_channels=1,
                       generation_classes=None,
                       prior_distributions='uniform',
                       prior_means=None,
                       prior_stds=None,
                       use_specific_stats_for_channel=False,
                       mix_prior_and_random=False,
                       apply_linear_trans=True,
                       scaling_bounds=None,
                       rotation_bounds=None,
                       shearing_bounds=None,
                       background_paths=None):
    """
    This function builds a generator to be fed to the lab2im model. It enables to generate all the required inputs,
    according to the operations performed in the model.
    :param path_label_maps: list of the paths of the input label maps.
    :param n_labels: number of labels in the input label maps.
    :param batchsize: (optional) numbers of images to generate per mini-batch. Default is 1.
    :param n_channels: (optional) number of channels to be synthetised. Default is 1.
    :param generation_classes: (optional) Indices regrouping generation labels into classes of same intensity
    distribution. Regouped labels will thus share the same Gaussian when samling a new image. Can be a sequence or a
    1d numpy array. It should have the same length as generation_labels, and contain values between 0 and K-1, where K
    is the total number of classes. Default is all labels have different classes.
    :param prior_distributions: (optional) type of distribution from which we sample the GMM parameters.
    Can either be 'uniform', or 'normal'. Default is 'uniform'.
    :param prior_means: (optional) hyperparameters controlling the prior distributions of the GMM means. Because
    these prior distributions are uniform or normal, they require by 2 hyperparameters. Thus prior_means can be:
    1) a sequence of length 2, directly defining the two hyperparameters: [min, max] if prior_distributions is
    uniform, [mean, std] if the distribution is normal. The GMM means of are independently sampled at each
    mini_batch from the same distribution.
    2) an array of shape (2, K), where K is the number of classes (K=len(generation_labels) if generation_classes is
    not given). The mean of the Gaussian distribution associated to class k in [0, ...K-1] is sampled at each mini-batch
    from U(prior_means[0,k], prior_means[1,k]) if prior_distributions is uniform, or from
    N(prior_means[0,k], prior_means[1,k]) if prior_distributions is normal.
    3) an array of shape (2*n_mod, K), where each block of two rows is associated to hyperparameters derived
    from different modalities. In this case, if use_specific_stats_for_channel is False, we first randomly select a
    modality from the n_mod possibilities, and we sample the GMM means like in 2).
    If use_specific_stats_for_channel is True, each block of two rows correspond to a different channel
    (n_mod=n_channels), thus we select the corresponding block to each channel rather than randomly drawing it.
    4) the path to such a numpy array.
    Default is None, which corresponds to prior_means = [25, 225].
    :param prior_stds: (optional) same as prior_means but for the standard deviations of the GMM.
    Default is None, which corresponds to prior_stds = [5, 25].
    :param use_specific_stats_for_channel: (optional) whether the i-th block of two rows in the prior arrays must be
    only used to generate the i-th channel. If True, n_mod should be equal to n_channels. Default is False.
    :param mix_prior_and_random: (optional) if prior_means is not None, enables to reset the priors to their default
    values for half of thes cases, and thus generate images of random contrast.
    :param apply_linear_trans: (optional) whether to apply affine deformation. Default is True.
    :param scaling_bounds: (optional) if apply_linear_trans is True, the scaling factor for each dimension is
    sampled from a uniform distribution of predefined bounds. Can either be:
    1) a number, in which case the scaling factor is independently sampled from the uniform distribution of bounds
    (1-scaling_bounds, 1+scaling_bounds) for each dimension.
    2) a sequence, in which case the scaling factor is sampled from the uniform distribution of bounds
    (1-scaling_bounds[i], 1+scaling_bounds[i]) for the i-th dimension.
    3) a numpy array of shape (2, n_dims), in which case the scaling factor is sampled from the uniform distribution
     of bounds (scaling_bounds[0, i], scaling_bounds[1, i]) for the i-th dimension.
    If None (default), scaling_range = 0.15
    :param rotation_bounds: (optional) same as scaling bounds but for the rotation angle, except that for cases 1
    and 2, the bounds are centred on 0 rather than 1, i.e. (0+rotation_bounds[i], 0-rotation_bounds[i]).
    If None (default), rotation_bounds = 15.
    :param shearing_bounds: (optional) same as scaling bounds. If None (default), shearing_bounds = 0.01.
    :param background_paths: (optional) list of paths of label maps to replace the soft brain tissues (label 258) with.
    """

    # get label info
    _, _, n_dims, _, _, _ = utils.get_volume_info(path_label_maps[0])

    # allocate unique class to each label if generation classes is not given
    if generation_classes is None:
        generation_classes = np.arange(n_labels)

    # Generate!
    while True:

        # randomly pick as many images as batchsize
        indices = npr.randint(len(path_label_maps), size=batchsize)

        # initialise input lists
        list_label_maps = []
        list_means = []
        list_stds = []
        list_affine_transforms = []

        for idx in indices:

            # add labels to inputs
            y = utils.load_volume(path_label_maps[idx], dtype='int', aff_ref=np.eye(4))
            if background_paths is not None:
                idx_258 = np.where(y == 258)
                if np.any(idx_258):
                    background = utils.load_volume(background_paths[npr.randint(len(background_paths))],
                                                   dtype='int', aff_ref=np.eye(4))
                    background_shape = background.shape
                    if np.all(np.array(background_shape) == background_shape[0]):  # flip if same dimensions
                        background = np.flip(background, tuple([i for i in range(3) if np.random.normal() > 0]))
                    assert background.shape == y.shape, 'background patches should have same shape than training ' \
                                                        'labels. Had {0} and {1}'.format(background.shape, y.shape)
                    y[idx_258] = background[idx_258]
            list_label_maps.append(utils.add_axis(y, axis=-2))

            # add means and standard deviations to inputs
            means = np.empty((n_labels, 0))
            stds = np.empty((n_labels, 0))
            for channel in range(n_channels):

                # retrieve channel specific stats if necessary
                if isinstance(prior_means, np.ndarray):
                    if (prior_means.shape[0] > 2) & use_specific_stats_for_channel:
                        if prior_means.shape[0] / 2 != n_channels:
                            raise ValueError("the number of blocks in prior_means does not match n_channels. This "
                                             "message is printed because use_specific_stats_for_channel is True.")
                        tmp_prior_means = prior_means[2 * channel:2 * channel + 2, :]
                    else:
                        tmp_prior_means = prior_means
                else:
                    tmp_prior_means = prior_means
                if (prior_means is not None) & mix_prior_and_random & (npr.uniform() > 0.5):
                    tmp_prior_means = None
                if isinstance(prior_stds, np.ndarray):
                    if (prior_stds.shape[0] > 2) & use_specific_stats_for_channel:
                        if prior_stds.shape[0] / 2 != n_channels:
                            raise ValueError("the number of blocks in prior_stds does not match n_channels. This "
                                             "message is printed because use_specific_stats_for_channel is True.")
                        tmp_prior_stds = prior_stds[2 * channel:2 * channel + 2, :]
                    else:
                        tmp_prior_stds = prior_stds
                else:
                    tmp_prior_stds = prior_stds
                if (prior_stds is not None) & mix_prior_and_random & (npr.uniform() > 0.5):
                    tmp_prior_stds = None

                # draw means and std devs from priors
                tmp_classes_means = utils.draw_value_from_distribution(tmp_prior_means, n_labels, prior_distributions,
                                                                       125., 100., positive_only=True)
                tmp_classes_stds = utils.draw_value_from_distribution(tmp_prior_stds, n_labels, prior_distributions,
                                                                      15., 10., positive_only=True)
                tmp_means = utils.add_axis(tmp_classes_means[generation_classes], -1)
                tmp_stds = utils.add_axis(tmp_classes_stds[generation_classes], -1)
                means = np.concatenate([means, tmp_means], axis=1)
                stds = np.concatenate([stds, tmp_stds], axis=1)
            list_means.append(utils.add_axis(means))
            list_stds.append(utils.add_axis(stds))

            # add linear transform to inputs
            if apply_linear_trans:
                # get affine transformation: rotate, scale, shear (translation done during random cropping)
                scaling = utils.draw_value_from_distribution(scaling_bounds, size=n_dims, centre=1, default_range=.15)
                if n_dims == 2:
                    rotation = utils.draw_value_from_distribution(rotation_bounds, default_range=15.0)
                else:
                    rotation = utils.draw_value_from_distribution(rotation_bounds, size=n_dims, default_range=15.0)
                shearing = utils.draw_value_from_distribution(shearing_bounds, size=n_dims**2-n_dims, default_range=.01)
                affine_transform = utils.create_affine_transformation_matrix(n_dims, scaling, rotation, shearing)
                list_affine_transforms.append(utils.add_axis(affine_transform))

        # build list of inputs of augmentation model
        list_inputs = [list_label_maps, list_means, list_stds]
        if apply_linear_trans:
            list_inputs.append(list_affine_transforms)

        # concatenate individual input types if batchsize > 1
        if batchsize > 1:
            list_inputs = [np.concatenate(item, 0) for item in list_inputs]
        else:
            list_inputs = [item[0] for item in list_inputs]

        yield list_inputs
Пример #21
0
def evaluation(gt_dir,
               seg_dir,
               label_list,
               mask_dir=None,
               compute_score_whole_structure=False,
               path_dice=None,
               path_hausdorff=None,
               path_hausdorff_99=None,
               path_hausdorff_95=None,
               path_mean_distance=None,
               crop_margin_around_gt=10,
               list_incorrect_labels=None,
               list_correct_labels=None,
               use_nearest_label=False,
               recompute=True,
               verbose=True):
    """This function computes Dice scores, as well as surface distances, between two sets of labels maps in gt_dir
    (ground truth) and seg_dir (typically predictions). Labels maps in both folders are matched by sorting order.
    The resulting scores are saved at the specified locations.
    :param gt_dir: path of directory with gt label maps
    :param seg_dir: path of directory with label maps to compare to gt_dir. Matched to gt label maps by sorting order.
    :param label_list: list of label values for which to compute evaluation metrics. Can be a sequence, a 1d numpy
    array, or the path to such array.
    :param mask_dir: (optional) path of directory with masks of areas to ignore for each evaluated segmentation.
    Matched to gt label maps by sorting order. Default is None, where nothing is masked.
    :param compute_score_whole_structure: (optional) whether to also compute the selected scores for the whole segmented
    structure (i.e. scores are computed for a single structure obtained by regrouping all non-zero values). If True, the
    resulting scores are added as an extra row to the result matrices. Default is False.
    :param path_dice: path where the resulting Dice will be writen as numpy array.
    Default is None, where the array is not saved.
    :param path_hausdorff: path where the resulting Hausdorff distances will be writen as numpy array (only if
    compute_distances is True). Default is None, where the array is not saved.
    :param path_hausdorff_99: same as for path_hausdorff but for the 99th percentile of the boundary distance.
    :param path_hausdorff_95: same as for path_hausdorff but for the 95th percentile of the boundary distance.
    :param path_mean_distance: path where the resulting mean distances will be writen as numpy array (only if
    compute_distances is True). Default is None, where the array is not saved.
    :param crop_margin_around_gt: (optional) margin by which to crop around the gt volumes, in order to copute the
    scores more efficiently. If None, no cropping is performed.
    :param list_incorrect_labels: (optional) this option enables to replace some label values in the maps in seg_dir by
    other label values. Can be a list, a 1d numpy array, or the path to such an array.
    The incorrect labels can then be replaced either by specified values, or by the nearest value (see below).
    :param list_correct_labels: (optional) list of values to correct the labels specified in list_incorrect_labels.
    Correct values must have the same order as their corresponding value in list_incorrect_labels.
    :param use_nearest_label: (optional) whether to correct the incorrect lavel values with the nearest labels.
    :param recompute: (optional) whether to recompute the already existing results. Default is True.
    :param verbose: (optional) whether to print out info about the remaining number of cases.
    """

    # check whether to recompute
    compute_dice = not os.path.isfile(path_dice) if (path_dice
                                                     is not None) else True
    compute_hausdorff = not os.path.isfile(path_hausdorff) if (
        path_hausdorff is not None) else False
    compute_hausdorff_99 = not os.path.isfile(path_hausdorff_99) if (
        path_hausdorff_99 is not None) else False
    compute_hausdorff_95 = not os.path.isfile(path_hausdorff_95) if (
        path_hausdorff_95 is not None) else False
    compute_mean_dist = not os.path.isfile(path_mean_distance) if (
        path_mean_distance is not None) else False
    compute_hd = [
        compute_hausdorff, compute_hausdorff_99, compute_hausdorff_95
    ]

    if compute_dice | any(compute_hd) | compute_mean_dist | recompute:

        # get list label maps to compare
        path_gt_labels = utils.list_images_in_folder(gt_dir)
        path_segs = utils.list_images_in_folder(seg_dir)
        path_gt_labels = utils.reformat_to_list(path_gt_labels,
                                                length=len(path_segs))
        if len(path_gt_labels) != len(path_segs):
            print(
                'gt and segmentation folders must have the same amount of label maps.'
            )
        if mask_dir is not None:
            path_masks = utils.list_images_in_folder(mask_dir)
            if len(path_masks) != len(path_segs):
                print('not the same amount of masks and segmentations.')
        else:
            path_masks = [None] * len(path_segs)

        # load labels list
        label_list, _ = utils.get_list_labels(label_list=label_list,
                                              FS_sort=True,
                                              labels_dir=gt_dir)
        n_labels = len(label_list)
        max_label = np.max(label_list) + 1

        # initialise result matrices
        if compute_score_whole_structure:
            max_dists = np.zeros((n_labels + 1, len(path_segs), 3))
            mean_dists = np.zeros((n_labels + 1, len(path_segs)))
            dice_coefs = np.zeros((n_labels + 1, len(path_segs)))
        else:
            max_dists = np.zeros((n_labels, len(path_segs), 3))
            mean_dists = np.zeros((n_labels, len(path_segs)))
            dice_coefs = np.zeros((n_labels, len(path_segs)))

        # loop over segmentations
        loop_info = utils.LoopInfo(len(path_segs),
                                   10,
                                   'evaluating',
                                   print_time=True)
        for idx, (path_gt, path_seg, path_mask) in enumerate(
                zip(path_gt_labels, path_segs, path_masks)):
            if verbose:
                loop_info.update(idx)

            # load gt labels and segmentation
            gt_labels = utils.load_volume(path_gt, dtype='int')
            seg = utils.load_volume(path_seg, dtype='int')
            if path_mask is not None:
                mask = utils.load_volume(path_mask, dtype='bool')
                gt_labels[mask] = max_label
                seg[mask] = max_label

            # crop images
            if crop_margin_around_gt is not None:
                gt_labels, cropping = edit_volumes.crop_volume_around_region(
                    gt_labels, margin=crop_margin_around_gt)
                seg = edit_volumes.crop_volume_with_idx(seg, cropping)

            if list_incorrect_labels is not None:
                seg = edit_volumes.correct_label_map(seg,
                                                     list_incorrect_labels,
                                                     list_correct_labels,
                                                     use_nearest_label)

            # compute Dice scores
            dice_coefs[:n_labels, idx] = fast_dice(gt_labels, seg, label_list)

            # compute Dice scores for whole structures
            if compute_score_whole_structure:
                temp_gt = (gt_labels > 0) * 1
                temp_seg = (seg > 0) * 1
                dice_coefs[-1, idx] = dice(temp_gt, temp_seg)
            else:
                temp_gt = temp_seg = None

            # compute average and Hausdorff distances
            if any(compute_hd) | compute_mean_dist:

                # compute unique label values
                unique_gt_labels = np.unique(gt_labels)
                unique_seg_labels = np.unique(seg)

                # compute max/mean surface distances for all labels
                for index, label in enumerate(label_list):
                    if (label in unique_gt_labels) & (label
                                                      in unique_seg_labels):
                        mask_gt = np.where(gt_labels == label, True, False)
                        mask_seg = np.where(seg == label, True, False)
                        tmp_max_dists, mean_dists[index,
                                                  idx] = surface_distances(
                                                      mask_gt, mask_seg,
                                                      [100, 99, 95])
                        max_dists[index, idx, :] = np.array(tmp_max_dists)
                    else:
                        mean_dists[index, idx] = max(gt_labels.shape)
                        max_dists[index, idx, :] = np.array(
                            [max(gt_labels.shape)] * 3)

                # compute max/mean distances for whole structure
                if compute_score_whole_structure:
                    tmp_max_dists, mean_dists[-1, idx] = surface_distances(
                        temp_gt, temp_seg, [100, 99, 95])
                    max_dists[-1, idx, :] = np.array(tmp_max_dists)

        # write results
        if path_dice is not None:
            utils.mkdir(os.path.dirname(path_dice))
            np.save(path_dice, dice_coefs)
        if path_hausdorff is not None:
            utils.mkdir(os.path.dirname(path_hausdorff))
            np.save(path_hausdorff, max_dists[..., 0])
        if path_hausdorff_99 is not None:
            utils.mkdir(os.path.dirname(path_hausdorff_99))
            np.save(path_hausdorff_99, max_dists[..., 1])
        if path_hausdorff_95 is not None:
            utils.mkdir(os.path.dirname(path_hausdorff_95))
            np.save(path_hausdorff_95, max_dists[..., 2])
        if path_mean_distance is not None:
            utils.mkdir(os.path.dirname(path_mean_distance))
            np.save(path_mean_distance, max_dists[..., 2])
Пример #22
0
    def __init__(self,
                 labels_dir,
                 generation_labels=None,
                 output_labels=None,
                 n_neutral_labels=None,
                 padding_margin=None,
                 batchsize=1,
                 n_channels=1,
                 target_res=None,
                 output_shape=None,
                 output_div_by_n=None,
                 prior_distributions='uniform',
                 generation_classes=None,
                 prior_means=None,
                 prior_stds=None,
                 use_specific_stats_for_channel=False,
                 flipping=True,
                 apply_linear_trans=True,
                 scaling_bounds=None,
                 rotation_bounds=None,
                 shearing_bounds=None,
                 apply_nonlin_trans=True,
                 nonlin_std=3.,
                 nonlin_shape_factor=0.0625,
                 blur_background=True,
                 data_res=None,
                 thickness=None,
                 downsample=False,
                 blur_range=1.15,
                 crop_channel_2=None,
                 apply_bias_field=True,
                 bias_field_std=0.3,
                 bias_shape_factor=0.025):
        """
        This class is wrapper around the labels_to_image_model model. It contains the GPU model that generates images
        from labels maps, and a python generator that suplies the input data for this model.
        To generate pairs of image/labels you can just call the method generate_image() on an object of this class.

        :param labels_dir: path of folder with all input label maps, or to a single label map.

        # IMPORTANT !!!
        # Each time we provide a parameter with separate values for each axis (e.g. with a numpy array or a sequence),
        # these values refer to the RAS axes.

        # label maps-related parameters
        :param generation_labels: (optional) list of all possible label values in the input label maps.
        Default is None, where the label values are directly gotten from the provided label maps.
        If not None, can be a sequence or a 1d numpy array, or the path to a 1d numpy array.
        If flipping is true (i.e. right/left flipping is enabled), generation_labels should be organised as follows:
        background label first, then non-sided labels (e.g. CSF, brainstem, etc.), then all the structures of the same
        hemisphere (can be left or right), and finally all the corresponding contralateral structures in the same order.
        :param output_labels: (optional) list of all the label values to keep in the output label maps (in no particular
        order). Should be a subset of the values contained in generation_labels.
        Label values that are in generation_labels but not in output_labels are reset to zero.
        Can be a sequence, a 1d numpy array, or the path to a 1d numpy array.
        By default output labels are equal to generation labels.
        :param n_neutral_labels: (optional) number of non-sided generation labels.
        Default is total number of label values.
        :param padding_margin: (optional) margin by which to pad the input labels with zeros.
        Padding is applied prior to any other operation.
        Can be an integer (same padding in all dimensions), a sequence, a 1d numpy array, or the path to a 1d numpy
        array. Default is no padding.

        # output-related parameters
        :param batchsize: (optional) numbers of images to generate per mini-batch. Default is 1.
        :param n_channels: (optional) number of channels to be synthetised. Default is 1.
        :param target_res: (optional) target resolution of the generated images and corresponding label maps.
        If None, the outputs will have the same resolution as the input label maps.
        Can be a number (isotropic resolution), a sequence, a 1d numpy array, or the path to a 1d numpy array.
        :param output_shape: (optional) shape of the output image, obtained by randomly cropping the generated image.
        Can be an integer (same size in all dimensions), a sequence, a 1d numpy array, or the path to a 1d numpy array.
        :param output_div_by_n: (optional) forces the output shape to be divisible by this value. It overwrites
        output_shape if necessary. Can be an integer (same size in all dimensions), a sequence, a 1d numpy array, or
        the path to a 1d numpy array.

        # GMM-sampling parameters
        :param generation_classes: (optional) Indices regrouping generation labels into classes of same intensity
        distribution. Regouped labels will thus share the same Gaussian when samling a new image. Can be a sequence, a
        1d numpy array, or the path to a 1d numpy array. It should have the same length as generation_labels, and
        contain values between 0 and K-1, where K is the total number of classes.
        Default is all labels have different classes (K=len(generation_labels)).
        :param prior_distributions: (optional) type of distribution from which we sample the GMM parameters.
        Can either be 'uniform', or 'normal'. Default is 'uniform'.
        :param prior_means: (optional) hyperparameters controlling the prior distributions of the GMM means. Because
        these prior distributions are uniform or normal, they require by 2 hyperparameters. Thus prior_means can be:
        1) a sequence of length 2, directly defining the two hyperparameters: [min, max] if prior_distributions is
        uniform, [mean, std] if the distribution is normal. The GMM means of are independently sampled at each
        mini_batch from the same distribution.
        2) an array of shape (2, K), where K is the number of classes (K=len(generation_labels) if generation_classes is
        not given). The mean of the Gaussian distribution associated to class k in [0, ...K-1] is sampled at each
        mini-batch from U(prior_means[0,k], prior_means[1,k]) if prior_distributions is uniform, and from
        N(prior_means[0,k], prior_means[1,k]) if prior_distributions is normal.
        3) an array of shape (2*n_mod, K), where each block of two rows is associated to hyperparameters derived
        from different modalities. In this case, if use_specific_stats_for_channel is False, we first randomly select a
        modality from the n_mod possibilities, and we sample the GMM means like in 2).
        If use_specific_stats_for_channel is True, each block of two rows correspond to a different channel
        (n_mod=n_channels), thus we select the corresponding block to each channel rather than randomly drawing it.
        4) the path to such a numpy array.
        Default is None, which corresponds to prior_means = [25, 225].
        :param prior_stds: (optional) same as prior_means but for the standard deviations of the GMM.
        Default is None, which corresponds to prior_stds = [5, 25].
        :param use_specific_stats_for_channel: (optional) whether the i-th block of two rows in the prior arrays must be
        only used to generate the i-th channel. If True, n_mod should be equal to n_channels. Default is False.

        # spatial deformation parameters
        :param flipping: (optional) whether to introduce right/left random flipping. Default is True.
        :param apply_linear_trans: (optional) whether to apply affine deformation. Default is True.
        :param scaling_bounds: (optional) if apply_linear_trans is True, the scaling factor for each dimension is
        sampled from a uniform distribution of predefined bounds. Can either be:
        1) a number, in which case the scaling factor is independently sampled from the uniform distribution of bounds
        (1-scaling_bounds, 1+scaling_bounds) for each dimension.
        2) a sequence, in which case the scaling factor is sampled from the uniform distribution of bounds
        (1-scaling_bounds[i], 1+scaling_bounds[i]) for the i-th dimension.
        3) a numpy array of shape (2, n_dims), in which case the scaling factor is sampled from the uniform distribution
         of bounds (scaling_bounds[0, i], scaling_bounds[1, i]) for the i-th dimension.
        4) the path to such a numpy array.
        If None (default), scaling_range = 0.15
        :param rotation_bounds: (optional) same as scaling bounds but for the rotation angle, except that for cases 1
        and 2, the bounds are centred on 0 rather than 1, i.e. (0+rotation_bounds[i], 0-rotation_bounds[i]).
        If None (default), rotation_bounds = 15.
        :param shearing_bounds: (optional) same as scaling bounds. If None (default), shearing_bounds = 0.01.
        :param apply_nonlin_trans: (optional) whether to apply non linear elastic deformation.
        If true, a diffeomorphic deformation field is obtained by first sampling a small tensor from the normal
        distribution, resizing it to image size, and integrationg it. Default is True.
        :param nonlin_std: (optional) If apply_nonlin_trans is True, maximum value for the standard deviation of the
        normal distribution from which we sample the first tensor for synthesising the deformation field.
        :param nonlin_shape_factor: (optional) If apply_nonlin_trans is True, ratio between the size of the input label
        maps and the size of the sampled tensor for synthesising the deformation field.

        # blurring/resampling parameters
        :param blur_background: (optional) whether to produce an unrealistic background or not.
        If True, the background is generated/blurred with the other labels, according to the values of prior_means and
        prior_stds. Also, it is reset to zero-background with a probability of 0.2.
        If False, the background is reset to zero, or can be replaced by a low-intensity background with a probability
        of 0.5. Additionally we correct for edge blurring effects.
        Default is True.
        :param data_res: (optional) acquisition resolution to mimick. If provided, the images sampled from the GMM are
        blurred to mimick data that would be: 1) acquired at the given acquisition resolution, and 2) resample at
        target_resolution.
        Default is None, where images are isotropically blurred to introduce some spatial correlation between voxels.
        If the generated images are uni-modal, data_res can be a number (isotropic acquisition resolution), a sequence,
        a 1d numpy array, or the path to a 1d numy array. In the multi-modal case, it should be given as a numpy array
        (or a path) of size (n_mod, n_dims), where each row is the acquisition resolution of the correspionding chanel.
        :param thickness: (optional) if data_res is provided, we can further specify the slice thickness of the low
        resolution images to mimick.
        If the generated images are uni-modal, data_res can be a number (isotropic acquisition resolution), a sequence,
        a 1d numpy array, or the path to a 1d numy array. In the multi-modal case, it should be given as a numpy array
        (or a path) of size (n_mod, n_dims), where each row is the acquisition resolution of the correspionding chanel.
        :param downsample: (optional) whether to actually downsample the volume image to data_res.
        Default is False, except when thickness is provided, and thickness < data_res.
        :param blur_range: (optional) Randomise the standard deviation of the blurring kernels, (whether data_res is
        given or not). At each mini_batch, the standard deviation of the blurring kernels are multiplied by a
        coefficient sampled from a uniform distribution with bounds [1/blur_range, blur_range].
        If None, no randomisation. Default is 1.15.
        :param crop_channel_2: (optional) stats for cropping second channel along the anterior-posterior axis.
        Should be a vector of length 4, with bounds of uniform distribution for cropping the front and back of the image
        (in percentage). None is no croppping.

        # bias field parameters
        :param apply_bias_field: (optional) whether to apply a bias field to the final image. Default is True.
        If True, the bias field is obtained by sampling a first tensor from normal distribution, resizing it to image
        size, and rescaling the values to positive number by taking the voxel-wise exponential. Default is True.
        :param bias_field_std: (optional) If apply_nonlin_trans is True, maximum value for the standard deviation of the
        normal distribution from which we sample the first tensor for synthesising the bias field.
        :param bias_shape_factor: (optional) If apply_bias_field is True, ratio between the size of the input
        label maps and the size of the sampled tensor for synthesising the bias field.
        """

        # prepare data files
        if ('.nii.gz' in labels_dir) | ('.nii' in labels_dir) | (
                '.mgz' in labels_dir) | ('.npz' in labels_dir):
            self.labels_paths = [labels_dir]
        else:
            self.labels_paths = utils.list_images_in_folder(labels_dir)
        assert len(self.labels_paths) > 0, "Could not find any training data"

        # generation parameters
        _, self.aff, self.header = utils.load_volume(self.labels_paths[0],
                                                     im_only=False)
        self.labels_shape, _, self.n_dims, _, _, self.atlas_res = utils.get_volume_info(
            self.labels_paths[0], aff_ref=np.eye(4))
        self.n_channels = n_channels
        if generation_labels is not None:
            self.generation_labels = utils.load_array_if_path(
                generation_labels)
        else:
            self.generation_labels = utils.get_list_labels(
                labels_dir=labels_dir)
        if output_labels is not None:
            self.output_labels = utils.load_array_if_path(output_labels)
        else:
            self.output_labels = self.generation_labels
        if n_neutral_labels is not None:
            self.n_neutral_labels = n_neutral_labels
        else:
            self.n_neutral_labels = self.generation_labels.shape[0]
        self.target_res = utils.load_array_if_path(target_res)
        self.batchsize = batchsize
        # preliminary operations
        self.padding_margin = utils.load_array_if_path(padding_margin)
        self.flipping = flipping
        self.output_shape = utils.load_array_if_path(output_shape)
        self.output_div_by_n = output_div_by_n
        # GMM parameters
        self.prior_distributions = prior_distributions
        if generation_classes is not None:
            self.generation_classes = utils.load_array_if_path(
                generation_classes)
            assert self.generation_classes.shape == self.generation_labels.shape, \
                'if provided, generation labels should have the same shape as generation_labels'
            unique_classes = np.unique(self.generation_classes)
            assert np.array_equal(unique_classes, np.arange(np.max(unique_classes)+1)), \
                'generation_classes should a linear range between 0 and its maximum value.'
        else:
            self.generation_classes = np.arange(
                self.generation_labels.shape[0])
        self.prior_means = utils.load_array_if_path(prior_means)
        self.prior_stds = utils.load_array_if_path(prior_stds)
        self.use_specific_stats_for_channel = use_specific_stats_for_channel
        # linear transformation parameters
        self.apply_linear_trans = apply_linear_trans
        self.scaling_bounds = utils.load_array_if_path(scaling_bounds)
        self.rotation_bounds = utils.load_array_if_path(rotation_bounds)
        self.shearing_bounds = utils.load_array_if_path(shearing_bounds)
        # elastic transformation parameters
        self.apply_nonlin_trans = apply_nonlin_trans
        self.nonlin_std = nonlin_std
        self.nonlin_shape_factor = nonlin_shape_factor
        # blurring parameters
        self.blur_background = blur_background
        self.data_res = utils.load_array_if_path(data_res)
        self.thickness = utils.load_array_if_path(thickness)
        self.downsample = downsample
        self.blur_range = blur_range
        self.crop_second_channel = utils.load_array_if_path(crop_channel_2)
        # bias field parameters
        self.apply_bias_field = apply_bias_field
        self.bias_field_std = bias_field_std
        self.bias_shape_factor = bias_shape_factor

        # build transformation model
        self.labels_to_image_model, self.model_output_shape = self._build_labels_to_image_model(
        )

        # build generator for model inputs
        self.model_inputs_generator = self._build_model_inputs_generator()

        # build brain generator
        self.brain_generator = self._build_brain_generator()