diff --git a/evaluation/create_algorithm_performances.py b/evaluation/create_algorithm_performances.py new file mode 100644 index 0000000..bc1ba3a --- /dev/null +++ b/evaluation/create_algorithm_performances.py @@ -0,0 +1,135 @@ +import os +import numpy as np +import pandas as pd +from glob import glob + +from joblib import Parallel, delayed +from tqdm import tqdm + +from evaluation.evaluation_settings import Teams +from statistics.dice_calculations import compute_dice_coefficient_per_instance +from plots.example_image import InstanceImage + + +def find_files(path, pattern): + files = [] + print("Finding files") + for dir, _, _ in os.walk(path): + files.extend(glob(os.path.join(dir, pattern))) + return files + + +def load_image(path, empty_image_dim=[540, 960]): + + if "human" in path: + image = InstanceImage(path, "", do_cleaning=True).get_image() + else: + # load image + if os.path.isfile(path): + image = InstanceImage(path, "", do_cleaning=False) + + else: # otherwise create empty image + image = np.zeros(empty_image_dim).astype('int') + + # create output dict + output = dict( + path=path, + image=image + ) + return output + + +def participant_to_groundtruth(path_participant): + global path_groundtruth, pattern_participants, pattern_groundtruth, path_participants + image_path = path_participant.replace(path_participants, "").split("/")[-5:] + image_path = "/".join(image_path).replace(pattern_participants, pattern_groundtruth) + image_path = os.path.join(path_groundtruth, image_path) + return image_path + +if __name__ == "__main__": + path_groundtruth = "/media/rosst/ssd_2tb/datasets/ROBUST-MIS/Testing" + path_participants = "/media/rosst/ssd_2tb/datasets/ROBUST-MIS/Results/9614273" + pattern_groundtruth = "instrument_instances.png" + pattern_participants = "output.png" + file_results = "saved_all_image_performances.pkl" + + try: + df = pd.read_pickle(file_results) + print(f"Loading file {file_results} was successful") + except: + print(f"Loading file {file_results} was not successful, create results") + + # get groundtruth files + files_groundtruth = find_files(path_groundtruth, pattern=pattern_groundtruth) + files_participants = find_files(path_participants, pattern=pattern_participants) + + # load files + files_to_load = files_groundtruth + files_participants + files_loaded = Parallel(n_jobs=12)(delayed(load_image)(file) for file in tqdm(files_to_load)) + + # convert to a dict + images = dict() + for file in files_loaded: + images[file["path"]] = file["image"] + del files_loaded + + # participant image to groundtruth + comparison_pairs = list() + for file in files_participants: + gt_path = participant_to_groundtruth(file) + if gt_path in images: + prediction = images[file] + gt = images[gt_path] + comparison_pairs.append(dict( + mask_pred=prediction, + mask_gt=gt, + path_gt=gt_path, + path_pred=file, + )) + + + def get_n_instances(mask_gt, mask_pred): + n_instances_gt = len(np.unique(mask_gt)) - 1, + n_instances_pred = len(np.unique(mask_pred)) - 1, + return dict(n_instances_gt=n_instances_gt, n_instances_pred=n_instances_pred) + + + # calculate dice values + dce_values = Parallel(n_jobs=12)( + delayed(compute_dice_coefficient_per_instance)(pair["mask_gt"], pair["mask_pred"]) for pair in + tqdm(comparison_pairs)) + n_instances = Parallel(n_jobs=12)( + delayed(get_n_instances)(pair["mask_gt"], pair["mask_pred"]) for pair in tqdm(comparison_pairs)) + + # images to instances + values_to_images = list() + dce_values_counter = 0 + for file in tqdm(files_participants): + gt_path = participant_to_groundtruth(file) + if gt_path in images: + keys = list(dce_values[dce_values_counter].keys()) + instruments = [key for key in keys if "instrument" in key] + instance_dce_values = list(dce_values[dce_values_counter].values()) + mi_dsc = np.array(instance_dce_values)[1:].mean() + + team, stage, or_type, or_nr, frame_nr = file.replace(path_participants, "").split("/")[1:-1] + team = Teams.get_team_by_synid(team) + values_to_images.append(dict( + file_gt=gt_path, + file_participant=file, + team=team, + stage=stage, + or_type=or_type, + or_nr=or_nr, + frame_nr=frame_nr, + MI_DSC=mi_dsc, + dce_values_raw=dce_values[dce_values_counter], + **n_instances[dce_values_counter] + )) + dce_values_counter += 1 + + # to pandas dataframe + df = pd.DataFrame(values_to_images) + df["n_instances_gt"] = df["n_instances_gt"].apply(lambda x: x[0]) + df["n_instances_pred"] = df["n_instances_pred"].apply(lambda x: x[0]) + df.to_pickle("saved_all_image_performances.pkl") \ No newline at end of file diff --git a/evaluation/evaluation_settings.py b/evaluation/evaluation_settings.py new file mode 100644 index 0000000..b8f6c0a --- /dev/null +++ b/evaluation/evaluation_settings.py @@ -0,0 +1,67 @@ +from enum import Enum, auto + + +class EVALUATION_SETTINGS: + PATH_TESTING_DATA = "/media/rosst/ssd_2tb/datasets/ROBUST-MIS/Testing" + PATH_OUTPUT_FOLDER = "/media/rosst/ssd_2tb/datasets/ROBUST-MIS/Results" + PATH_TRAINING_DATA = "/media/rosst/hdd_data/datasets/robustmis2019/release/instance_segmentation/release_instance_segmentation_30092019/Training" + PATH_SHELL_SCRIPT = "/media/rosst/hdd_data/development/robust_mis_2019/evaluation/run_docker.sh" + DOCKER_MAX_EXECUTION_TIME_IN_S = 8 * 60 * 60 # 8h + MOCKUP = False + + +class Testcase(Enum): + BINARY_SEGMENTATION = 9614245 + MULTIPLE_INSTANCE_DETECTION = 9614272 + MULTIPLE_INSTANCE_SEGMENTATION = 9614273 + + @staticmethod + def string_to_enum(input_str): + if not isinstance(input_str, str): + output = input_str + else: + output = eval(input_str) + return output + + +class Teams: + TEAMS = { + "syn20824431": "fisensee", + "syn20824432": "fisensee", + "syn20821332": "NCT", + "syn20812165": "Djh", + "syn20821674": "Djh", + "syn20821710": "Djh", + "syn20819950": "www", + "syn20812156": "www", + "syn20784332": "www", + "syn20805359": "CASIA_SRL", + "syn20812106": "CASIA_SRL", + "syn20806431": "Prometheus", + "syn20820446": "Uniandes", + "syn20820448": "Uniandes", + "syn20820447": "Uniandes", + "syn20821333": "Raabid", + "syn20758210": "Caresyntax", + "syn20814896": "Caresyntax", + "syn20814856": "Caresyntax", + "syn20818289": "VIE", + "syn20818338": "VIE", + "syn20818337": "VIE", + "syn20780161": "SQUASH", + "syn20780293": "SQUASH", + "syn20780295": "SQUASH", + "syn20823093": "haoyun", + + + } + + @staticmethod + def get_team_by_synid(syn_id): + syn_id = syn_id.replace("docker_synapse_org_", "") + syn_id = syn_id.split("_")[0] + try: + team_name = Teams.TEAMS[syn_id] + except KeyError: + team_name = syn_id + return team_name diff --git a/plots/example_image.py b/plots/example_image.py new file mode 100644 index 0000000..e51ceea --- /dev/null +++ b/plots/example_image.py @@ -0,0 +1,291 @@ +import os +import numpy as np + +import cv2 +from imageio import imread +from scipy.ndimage import binary_dilation + +from evaluation.evaluation_settings import EVALUATION_SETTINGS, Testcase, Teams +from statistics.dice_calculations import compute_dice_coefficient_per_instance, compute_dice_coefficient + + +class ExampleImage: + + def __init__(self, image_path, task, human_flag="human"): + # define variables + self.image_path = image_path + self.participants = ParticipantImages(image_path, task, human_flag=human_flag) + self.image_contours = [] + self.colors = [] + + # settings + self.contour_size = 5 + + # run init functions + self.read_images() + self.define_contour_colors() + + def define_contour_colors(self): + colors = [ + (0, 255, 0), + (255, 20, 147), + (255, 255, 0), + (0, 255, 255), + (153, 101, 21), + (102, 102, 0), + (204, 255, 229), + (160, 160, 160), + (0, 128, 255), + (255, 153, 255), + ] + self.colors = colors + + def read_images(self): + # create paths + path_img_instances = os.path.join(EVALUATION_SETTINGS.PATH_TESTING_DATA, self.image_path, + "instrument_instances.png") + path_img_raw = os.path.join(EVALUATION_SETTINGS.PATH_TESTING_DATA, self.image_path, "raw.png") + + # read images + self.image_raw = imread(path_img_raw) + self.image_raw = np.array(self.image_raw) + self.image_instances = InstanceImage(path_img_instances, "groundtruth") + + def create_image_with_contours(self): + # create image + image = self.image_raw.copy() + + # get contours + contours = self.image_instances.get_contours() + + # draw contours + for i, contour in enumerate(contours): + cv2.drawContours(image, contour, -1, self.colors[i], self.contour_size) + + return image + + def save_image_with_contours(self, path): + contour_image = self.create_image_with_contours() + contour_image.imsave(path, contour_image) + + def get_statistics(self): + participant_images = self.participants.get_images() + participant_statistics = [] + for participant_prediction_img in participant_images: + # get image info + image_info = dict( + path_pred=participant_prediction_img.path, + path_gt=self.image_instances.path, + team=participant_prediction_img.get_name() + ) + + # get images + participant_prediction_img = participant_prediction_img.image_instances + ground_truth_img = self.image_instances.image_instances + + # CASE BINARY SEGMENTATION + ground_truth_img_binary_seg = np.copy(ground_truth_img) + participant_prediction_img_binary_seg = np.copy(participant_prediction_img) + + ground_truth_img_binary_seg[ground_truth_img_binary_seg > 0] = 1 + participant_prediction_img_binary_seg[participant_prediction_img_binary_seg > 0] = 1 + + dice_coefficient = \ + compute_dice_coefficient(mask_gt=ground_truth_img_binary_seg, + mask_pred=participant_prediction_img_binary_seg) + + # multiple instance dice + instrument_dices = compute_dice_coefficient_per_instance(ground_truth_img, + participant_prediction_img) + instrument_list = list(instrument_dices.keys()) + instrument_list.remove("background") + dice_instances = np.array([instrument_dices[key] for key in instrument_list]) + + statistics = dict( + instance_dices_coefficients=dice_instances, + dice_coefficient = [dice_coefficient], + **image_info + ) + + participant_statistics.append(statistics) + return participant_statistics + + def get_participant_images(self): + return self.participants.get_images() + + def get_participant_team_names(self): + return sorted(self.participants.get_team_names()) + + +class InstanceImage: + VALID_INSTRUMENT_AREA = 0.03 + + def __init__(self, path, name, do_cleaning=False): + self.name = name + self.path = path + self.image_instances = None + self.image_contours = [] + self.empty_image_dim = [540, 960] + self.do_cleaning = do_cleaning + + self.load_object() + + def load_object(self): + self.imload() + self.make_instance_nr_continues() + self.calculate_contours() + self.calculate_area_of_instances() + + if self.do_cleaning: + self.remove_small_parts() + + # update contours and areas + self.calculate_contours() + self.calculate_area_of_instances() + + def imload(self): + # if image exists load image + if os.path.isfile(self.path): + output = imread(self.path) + + else: # otherwise create empty image + output = np.zeros(self.empty_image_dim).astype('int') + + self.image_instances = output + + def get_instances(self): + instances = np.unique(self.image_instances) + instances = instances[1:] # remove background + return instances + + def get_number_of_instances(self): + n_instances = len(self.get_instances()) + return n_instances + + def calculate_area_of_instances(self): + self.instance_areas = [] + n_instances = np.unique(self.image_instances) + n_instances = n_instances[1:] # remove background + + for current_instance_nr in n_instances: + # create current binary image + current_instance = np.copy(self.image_instances) + current_instance[self.image_instances == current_instance_nr] = 1 + current_instance[self.image_instances != current_instance_nr] = 0 + instance_area = np.sum(current_instance) + self.instance_areas.append(instance_area) + + def calculate_contours(self): + self.image_contours = [] + n_instances = np.unique(self.image_instances) + n_instances = n_instances[1:] # remove background + + for current_instance_nr in n_instances: + # create current binary image + current_instance = np.copy(self.image_instances) + current_instance[self.image_instances == current_instance_nr] = 1 + current_instance[self.image_instances != current_instance_nr] = 0 + + # get contours + # imgray = cv2.cvtColor(current_instance, cv2.COLOR_BGR2GRAY) + # ret, thresh = cv2.threshold(imgray, 127, 255, 0) + contours, _ = cv2.findContours(current_instance, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + self.image_contours.append(contours) + + def get_contours(self): + return self.image_contours + + def get_area_of_instances(self): + return self.instance_areas + + def get_image(self): + return np.array(self.image_instances) + + def get_name(self): + return self.name + + def make_instance_nr_continues(self): + # get instance numbers + instances = self.get_instances() + + # make instances continues + for instance_nr_new, instance in enumerate(instances): + self.image_instances[self.image_instances == instance] = instance_nr_new + 1 # because 0 is bg + + def remove_small_parts(self): + self.image_raw = self.image_instances.copy() + + # get areas of all instances + instance_areas = np.array(self.get_area_of_instances()) + if self.get_number_of_instances()>1: + + # get instances + instances = self.get_instances() + + # calculate percentage of all instances with respect to the biggest one + areas_percentages = instance_areas / np.max(instance_areas) + + # remove regions that are smaller than 3% of the biggest area + small_regions_idx = np.where(areas_percentages<InstanceImage.VALID_INSTRUMENT_AREA)[0] + if len(small_regions_idx>0): + for small_region_idx in small_regions_idx: + # get current instance nr of small region + current_idx = instances[small_region_idx] + mask_image = self.image_instances==current_idx + + # find connected regions + dilated_instance = binary_dilation(mask_image) + idx_in_element = np.unique(self.image_instances[dilated_instance])[1:] + + if len(idx_in_element)>1: + # get biggest connected region if it exists + idx_biggest_area = np.argmax(instance_areas[idx_in_element-1]) + new_instance_nr = instances[idx_biggest_area] + else: + # if no biggest connected region exists, merge into bg + new_instance_nr = 0 + + # set new instance nr + self.image_instances[self.image_instances==current_idx] = new_instance_nr + + +class ParticipantImages: + + def __init__(self, image, task, human_flag = "human"): + self.image = image + self.task = Testcase.string_to_enum(task) + self.folder_participants = [] + self.read_images() + + def get_participant_files(self): + evaluation_folder = os.path.join(EVALUATION_SETTINGS.PATH_OUTPUT_FOLDER, str(self.task.value)) + participants = os.listdir(evaluation_folder) + participant_images = [] + for participant in participants: + if ".csv" not in participant: + path_participant = os.path.join(evaluation_folder, participant) + path_participant_img = os.path.join(path_participant, self.image, "output.png") + participant_images.append(dict(file=path_participant_img, participant=participant)) + return participant_images + + def read_images(self): + paths_participant_image = self.get_participant_files() + participant_images = [] + for participant_path in paths_participant_image: + if "human" in participant_path["file"]: + participant_img = InstanceImage(participant_path["file"], participant_path["participant"], + do_cleaning=True) + else: + participant_img = InstanceImage(participant_path["file"], participant_path["participant"]) + participant_images.append(participant_img) + + self.participant_images = participant_images + + def get_images(self): + return self.participant_images + + def get_team_names(self): + evaluation_folder = os.path.join(EVALUATION_SETTINGS.PATH_OUTPUT_FOLDER, str(self.task.value)) + participants = os.listdir(evaluation_folder) + participants_names = [Teams.get_team_by_synid(participant) for participant in participants] + return participants_names diff --git a/statistics/__init__.py b/statistics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/evaluation/dice_calculations.py b/statistics/dice_calculations.py similarity index 97% rename from evaluation/dice_calculations.py rename to statistics/dice_calculations.py index e28bd79..84174be 100644 --- a/evaluation/dice_calculations.py +++ b/statistics/dice_calculations.py @@ -1,94 +1,98 @@ import numpy as np from scipy.optimize import linear_sum_assignment as hungarian_algorithm def compute_dice_coefficient(mask_gt, mask_pred): """Compute soerensen-dice coefficient. compute the soerensen-dice coefficient between the ground truth mask `mask_gt` and the predicted mask `mask_pred`. Args: mask_gt: 3-dim Numpy array of type bool. The ground truth mask. mask_pred: 3-dim Numpy array of type bool. The predicted mask. Returns: the dice coeffcient as float. If both masks are empty, the result is NaN """ + # binarize image + mask_gt[mask_gt>0] = 1 + mask_pred[mask_pred>0] = 1 + # calculate volume volume_sum = mask_gt.sum() + mask_pred.sum() if volume_sum == 0: return np.NaN volume_intersect = (mask_gt & mask_pred).sum() return 2 * volume_intersect / volume_sum def compute_dice_coefficient_per_instance(mask_gt, mask_pred): """Compute instance soerensen-dice coefficient. compute the soerensen-dice coefficient between the ground truth mask `mask_gt` and the predicted mask `mask_pred` for multiple instances. Args: mask_gt: 3-dim Numpy array of type int. The ground truth image, where 0 means background and 1-N is an instrument instance. mask_pred: 3-dim Numpy array of type int. The predicted mask, where 0 means background and 1-N is an instrument instance. Returns: a instance dictionary with the dice coeffcient as float. """ # get number of labels in image instances_gt = np.unique(mask_gt) instances_pred = np.unique(mask_pred) # create performance matrix performance_matrix = np.zeros((len(instances_gt), len(instances_pred))) masks = [] # calculate dice score for each ground truth to predicted instance for counter_gt, instance_gt in enumerate(instances_gt): # create binary mask for current gt instance gt = mask_gt.copy() gt[mask_gt != instance_gt] = 0 gt[mask_gt == instance_gt] = 1 masks_row = [] for counter_pred, instance_pred in enumerate(instances_pred): # make binary mask for current predicted instance prediction = mask_pred.copy() prediction[mask_pred != instance_pred] = 0 prediction[mask_pred == instance_pred] = 1 # calculate dice performance_matrix[counter_gt, counter_pred] = compute_dice_coefficient(gt, prediction) masks_row.append([gt, prediction]) masks.append(masks_row) # assign instrument instances according to hungarian algorithm label_assignment = hungarian_algorithm(performance_matrix * -1) label_nr_gt, label_nr_pred = label_assignment # get performance per instance image_performance = [] for i in range(len(label_nr_gt)): instance_dice = performance_matrix[label_nr_gt[i], label_nr_pred[i]] image_performance.append(instance_dice) missing_pred = np.absolute(len(instances_pred) - len(image_performance)) missing_gt = np.absolute(len(instances_gt) - len(image_performance)) n_missing = np.max([missing_gt, missing_pred]) if n_missing > 0: for i in range(n_missing): image_performance.append(0) output = dict() for i, performance in enumerate(image_performance): if i > 0: output["instrument_{}".format(i - 1)] = performance else: output["background"] = performance return output diff --git a/evaluation/distance_calculations.py b/statistics/distance_calculations.py similarity index 90% rename from evaluation/distance_calculations.py rename to statistics/distance_calculations.py index 1545566..a2121b7 100644 --- a/evaluation/distance_calculations.py +++ b/statistics/distance_calculations.py @@ -1,475 +1,544 @@ import numpy as np import scipy.ndimage +from scipy.optimize import linear_sum_assignment as hungarian_algorithm # neighbour_code_to_normals is a lookup table. # For every binary neighbour code # (2x2x2 neighbourhood = 8 neighbours = 8 bits = 256 codes) # it contains the surface normals of the triangles (called "surfel" for # "surface element" in the following). The length of the normal # vector encodes the surfel area. # # created by compute_surface_area_lookup_table.ipynb using the # marching_cube algorithm, see e.g. https://en.wikipedia.org/wiki/Marching_cubes # neighbour_code_to_normals = [ [[0,0,0]], [[0.125,0.125,0.125]], [[-0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[0.25,0.25,-0.0]], [[0.125,-0.125,0.125]], [[-0.25,-0.0,-0.25],[0.25,0.0,0.25]], [[0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[0.5,0.0,-0.0],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[-0.125,0.125,0.125]], [[0.125,0.125,0.125],[-0.125,0.125,0.125]], [[-0.25,0.0,0.25],[-0.25,0.0,0.25]], [[0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.5,0.0,0.0],[0.25,-0.25,0.25],[-0.125,0.125,-0.125]], [[-0.5,0.0,0.0],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[0.5,0.0,0.0],[0.5,0.0,0.0]], [[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25]], [[-0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.5,0.0],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,0.0,-0.5],[0.25,0.25,0.25],[-0.125,-0.125,-0.125]], [[-0.125,-0.125,0.125],[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25],[-0.125,0.125,0.125]], [[-0.25,0.0,0.25],[-0.25,0.0,0.25],[0.125,-0.125,-0.125]], [[0.125,0.125,0.125],[0.375,0.375,0.375],[0.0,-0.25,0.25],[-0.25,0.0,0.25]], [[0.125,-0.125,-0.125],[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.375,0.375,0.375],[0.0,0.25,-0.25],[-0.125,-0.125,-0.125],[-0.25,0.25,0.0]], [[-0.5,0.0,0.0],[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25],[0.125,0.125,0.125]], [[-0.5,0.0,0.0],[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25]], [[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,0.25,-0.25]], [[0.0,-0.5,0.0],[0.125,0.125,-0.125],[0.25,0.25,-0.25]], [[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.125,-0.125,0.125],[-0.25,-0.0,-0.25],[0.25,0.0,0.25]], [[0.0,-0.25,0.25],[0.0,0.25,-0.25],[0.125,-0.125,0.125]], [[-0.375,-0.375,0.375],[-0.0,0.25,0.25],[0.125,0.125,-0.125],[-0.25,-0.0,-0.25]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[-0.0,0.0,0.5],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.25,0.25,-0.25],[0.25,0.25,-0.25],[0.125,0.125,-0.125],[-0.125,-0.125,0.125]], [[0.125,-0.125,0.125],[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.5,0.0,0.0],[0.25,-0.25,0.25],[-0.125,0.125,-0.125],[0.125,-0.125,0.125]], [[0.0,0.25,-0.25],[0.375,-0.375,-0.375],[-0.125,0.125,0.125],[0.25,0.25,0.0]], [[-0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.25,-0.25,0.0],[-0.25,0.25,0.0]], [[0.0,0.5,0.0],[-0.25,0.25,0.25],[0.125,-0.125,-0.125]], [[0.0,0.5,0.0],[0.125,-0.125,0.125],[-0.25,0.25,-0.25]], [[0.0,0.5,0.0],[0.0,-0.5,0.0]], [[0.25,-0.25,0.0],[-0.25,0.25,0.0],[0.125,-0.125,0.125]], [[-0.375,-0.375,-0.375],[-0.25,0.0,0.25],[-0.125,-0.125,-0.125],[-0.25,0.25,0.0]], [[0.125,0.125,0.125],[0.0,-0.5,0.0],[-0.25,-0.25,-0.25],[-0.125,-0.125,-0.125]], [[0.0,-0.5,0.0],[-0.25,-0.25,-0.25],[-0.125,-0.125,-0.125]], [[-0.125,0.125,0.125],[0.25,-0.25,0.0],[-0.25,0.25,0.0]], [[0.0,0.5,0.0],[0.25,0.25,-0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.375,0.375,-0.375],[-0.25,-0.25,0.0],[-0.125,0.125,-0.125],[-0.25,0.0,0.25]], [[0.0,0.5,0.0],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[0.25,-0.25,0.0],[-0.25,0.25,0.0],[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[-0.25,-0.25,0.0],[-0.25,-0.25,0.0],[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[-0.25,-0.25,0.0],[-0.25,-0.25,0.0]], [[-0.25,-0.25,0.0],[-0.25,-0.25,0.0]], [[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[-0.25,-0.25,0.0],[0.25,0.25,-0.0]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25]], [[0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.375,-0.375,0.375],[0.0,-0.25,-0.25],[-0.125,0.125,-0.125],[0.25,0.25,0.0]], [[-0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[-0.25,0.0,0.25],[-0.25,0.0,0.25]], [[0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.0,0.5,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125]], [[-0.25,0.25,-0.25],[-0.25,0.25,-0.25],[-0.125,0.125,-0.125],[-0.125,0.125,-0.125]], [[-0.25,0.0,-0.25],[0.375,-0.375,-0.375],[0.0,0.25,-0.25],[-0.125,0.125,0.125]], [[0.5,0.0,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125]], [[-0.25,0.0,0.25],[0.25,0.0,-0.25]], [[-0.0,0.0,0.5],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[-0.25,0.0,0.25],[0.25,0.0,-0.25]], [[-0.25,-0.0,-0.25],[-0.375,0.375,0.375],[-0.25,-0.25,0.0],[-0.125,0.125,0.125]], [[0.0,0.0,-0.5],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[-0.0,0.0,0.5],[0.0,0.0,0.5]], [[0.125,0.125,0.125],[0.125,0.125,0.125],[0.25,0.25,0.25],[0.0,0.0,0.5]], [[0.125,0.125,0.125],[0.25,0.25,0.25],[0.0,0.0,0.5]], [[-0.25,0.0,0.25],[0.25,0.0,-0.25],[-0.125,0.125,0.125]], [[-0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[-0.25,0.0,0.25],[-0.25,0.0,0.25],[-0.25,0.0,0.25],[0.25,0.0,-0.25]], [[0.125,-0.125,0.125],[0.25,0.0,0.25],[0.25,0.0,0.25]], [[0.25,0.0,0.25],[-0.375,-0.375,0.375],[-0.25,0.25,0.0],[-0.125,-0.125,0.125]], [[-0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.25,0.0,0.25],[0.25,0.0,0.25]], [[0.25,0.0,0.25],[0.25,0.0,0.25]], [[-0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[0.0,-0.25,0.25],[0.0,0.25,-0.25]], [[0.0,-0.5,0.0],[0.125,0.125,-0.125],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25],[0.125,-0.125,0.125]], [[0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25],[0.0,-0.25,0.25],[0.0,0.25,-0.25]], [[0.0,0.25,0.25],[0.0,0.25,0.25],[0.125,-0.125,-0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,-0.125,0.125],[0.125,0.125,0.125]], [[-0.0,0.0,0.5],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[-0.0,0.5,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,-0.125]], [[0.5,0.0,-0.0],[0.25,-0.25,-0.25],[0.125,-0.125,-0.125]], [[-0.25,0.25,0.25],[-0.125,0.125,0.125],[-0.25,0.25,0.25],[0.125,-0.125,-0.125]], [[0.375,-0.375,0.375],[0.0,0.25,0.25],[-0.125,0.125,-0.125],[-0.25,0.0,0.25]], [[0.0,-0.5,0.0],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[-0.375,-0.375,0.375],[0.25,-0.25,0.0],[0.0,0.25,0.25],[-0.125,-0.125,0.125]], [[-0.125,0.125,0.125],[-0.25,0.25,0.25],[0.0,0.0,0.5]], [[0.125,0.125,0.125],[0.0,0.25,0.25],[0.0,0.25,0.25]], [[0.0,0.25,0.25],[0.0,0.25,0.25]], [[0.5,0.0,-0.0],[0.25,0.25,0.25],[0.125,0.125,0.125],[0.125,0.125,0.125]], [[0.125,-0.125,0.125],[-0.125,-0.125,0.125],[0.125,0.125,0.125]], [[-0.25,-0.0,-0.25],[0.25,0.0,0.25],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[0.25,0.25,-0.0],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,0.125,0.125]], [[0.125,0.125,0.125]], [[0.125,0.125,0.125]], [[0.125,0.125,0.125],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[0.25,0.25,-0.0],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125]], [[-0.25,-0.0,-0.25],[0.25,0.0,0.25],[0.125,0.125,0.125]], [[0.125,-0.125,0.125],[-0.125,-0.125,0.125],[0.125,0.125,0.125]], [[0.5,0.0,-0.0],[0.25,0.25,0.25],[0.125,0.125,0.125],[0.125,0.125,0.125]], [[0.0,0.25,0.25],[0.0,0.25,0.25]], [[0.125,0.125,0.125],[0.0,0.25,0.25],[0.0,0.25,0.25]], [[-0.125,0.125,0.125],[-0.25,0.25,0.25],[0.0,0.0,0.5]], [[-0.375,-0.375,0.375],[0.25,-0.25,0.0],[0.0,0.25,0.25],[-0.125,-0.125,0.125]], [[0.0,-0.5,0.0],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[0.375,-0.375,0.375],[0.0,0.25,0.25],[-0.125,0.125,-0.125],[-0.25,0.0,0.25]], [[-0.25,0.25,0.25],[-0.125,0.125,0.125],[-0.25,0.25,0.25],[0.125,-0.125,-0.125]], [[0.5,0.0,-0.0],[0.25,-0.25,-0.25],[0.125,-0.125,-0.125]], [[0.125,0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25],[0.125,0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[-0.0,0.5,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[-0.0,0.0,0.5],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,-0.125,0.125],[0.125,0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[0.0,0.25,0.25],[0.0,0.25,0.25],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25],[0.0,0.25,0.25],[0.0,0.25,0.25]], [[0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25],[0.125,-0.125,0.125]], [[0.0,-0.5,0.0],[0.125,0.125,-0.125],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[0.0,-0.25,0.25],[0.0,0.25,-0.25]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.25,0.0,0.25],[0.25,0.0,0.25]], [[0.125,0.125,0.125],[0.25,0.0,0.25],[0.25,0.0,0.25]], [[-0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125]], [[0.25,0.0,0.25],[-0.375,-0.375,0.375],[-0.25,0.25,0.0],[-0.125,-0.125,0.125]], [[0.125,-0.125,0.125],[0.25,0.0,0.25],[0.25,0.0,0.25]], [[-0.25,-0.0,-0.25],[0.25,0.0,0.25],[0.25,0.0,0.25],[0.25,0.0,0.25]], [[-0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[-0.25,0.0,0.25],[0.25,0.0,-0.25],[-0.125,0.125,0.125]], [[0.125,0.125,0.125],[0.25,0.25,0.25],[0.0,0.0,0.5]], [[0.125,0.125,0.125],[0.125,0.125,0.125],[0.25,0.25,0.25],[0.0,0.0,0.5]], [[-0.0,0.0,0.5],[0.0,0.0,0.5]], [[0.0,0.0,-0.5],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[-0.25,-0.0,-0.25],[-0.375,0.375,0.375],[-0.25,-0.25,0.0],[-0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[-0.25,0.0,0.25],[0.25,0.0,-0.25]], [[-0.0,0.0,0.5],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[-0.25,0.0,0.25],[0.25,0.0,-0.25]], [[0.5,0.0,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125]], [[-0.25,0.0,-0.25],[0.375,-0.375,-0.375],[0.0,0.25,-0.25],[-0.125,0.125,0.125]], [[-0.25,0.25,-0.25],[-0.25,0.25,-0.25],[-0.125,0.125,-0.125],[-0.125,0.125,-0.125]], [[-0.0,0.5,0.0],[-0.25,0.25,-0.25],[0.125,-0.125,0.125]], [[0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,-0.125,0.125],[-0.25,0.0,0.25],[-0.25,0.0,0.25]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[0.375,-0.375,0.375],[0.0,-0.25,-0.25],[-0.125,0.125,-0.125],[0.25,0.25,0.0]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.0,0.0,0.5],[0.25,-0.25,0.25],[0.125,-0.125,0.125]], [[0.0,-0.25,0.25],[0.0,-0.25,0.25]], [[-0.125,-0.125,0.125],[-0.25,-0.25,0.0],[0.25,0.25,-0.0]], [[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[-0.25,-0.25,0.0]], [[0.125,0.125,0.125],[-0.25,-0.25,0.0],[-0.25,-0.25,0.0]], [[-0.25,-0.25,0.0],[-0.25,-0.25,0.0],[-0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[-0.25,-0.25,0.0],[-0.25,-0.25,0.0],[0.25,0.25,-0.0]], [[0.0,0.5,0.0],[0.25,0.25,-0.25],[-0.125,-0.125,0.125]], [[-0.375,0.375,-0.375],[-0.25,-0.25,0.0],[-0.125,0.125,-0.125],[-0.25,0.0,0.25]], [[0.0,0.5,0.0],[0.25,0.25,-0.25],[-0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.125,0.125,0.125],[0.25,-0.25,0.0],[-0.25,0.25,0.0]], [[0.0,-0.5,0.0],[-0.25,-0.25,-0.25],[-0.125,-0.125,-0.125]], [[0.125,0.125,0.125],[0.0,-0.5,0.0],[-0.25,-0.25,-0.25],[-0.125,-0.125,-0.125]], [[-0.375,-0.375,-0.375],[-0.25,0.0,0.25],[-0.125,-0.125,-0.125],[-0.25,0.25,0.0]], [[0.25,-0.25,0.0],[-0.25,0.25,0.0],[0.125,-0.125,0.125]], [[0.0,0.5,0.0],[0.0,-0.5,0.0]], [[0.0,0.5,0.0],[0.125,-0.125,0.125],[-0.25,0.25,-0.25]], [[0.0,0.5,0.0],[-0.25,0.25,0.25],[0.125,-0.125,-0.125]], [[0.25,-0.25,0.0],[-0.25,0.25,0.0]], [[-0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.0,0.25,-0.25],[0.375,-0.375,-0.375],[-0.125,0.125,0.125],[0.25,0.25,0.0]], [[0.5,0.0,0.0],[0.25,-0.25,0.25],[-0.125,0.125,-0.125],[0.125,-0.125,0.125]], [[0.125,-0.125,0.125],[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.25,0.25,-0.25],[0.25,0.25,-0.25],[0.125,0.125,-0.125],[-0.125,-0.125,0.125]], [[-0.0,0.0,0.5],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[0.125,0.125,0.125],[0.125,-0.125,0.125],[-0.125,0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,0.125]], [[-0.375,-0.375,0.375],[-0.0,0.25,0.25],[0.125,0.125,-0.125],[-0.25,-0.0,-0.25]], [[0.0,-0.25,0.25],[0.0,0.25,-0.25],[0.125,-0.125,0.125]], [[0.125,-0.125,0.125],[-0.25,-0.0,-0.25],[0.25,0.0,0.25]], [[0.125,-0.125,0.125],[0.125,-0.125,0.125]], [[0.0,-0.5,0.0],[0.125,0.125,-0.125],[0.25,0.25,-0.25]], [[0.0,-0.25,0.25],[0.0,0.25,-0.25]], [[0.125,0.125,0.125],[0.125,-0.125,0.125]], [[0.125,-0.125,0.125]], [[-0.5,0.0,0.0],[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25]], [[-0.5,0.0,0.0],[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25],[0.125,0.125,0.125]], [[0.375,0.375,0.375],[0.0,0.25,-0.25],[-0.125,-0.125,-0.125],[-0.25,0.25,0.0]], [[0.125,-0.125,-0.125],[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.125,0.125,0.125],[0.375,0.375,0.375],[0.0,-0.25,0.25],[-0.25,0.0,0.25]], [[-0.25,0.0,0.25],[-0.25,0.0,0.25],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25],[-0.125,0.125,0.125]], [[-0.125,0.125,0.125],[0.125,-0.125,-0.125]], [[-0.125,-0.125,-0.125],[-0.25,-0.25,-0.25],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,0.0,-0.5],[0.25,0.25,0.25],[-0.125,-0.125,-0.125]], [[0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.5,0.0],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[-0.125,-0.125,0.125],[0.125,-0.125,-0.125]], [[0.0,-0.25,-0.25],[0.0,0.25,0.25]], [[0.125,-0.125,-0.125]], [[0.5,0.0,0.0],[0.5,0.0,0.0]], [[-0.5,0.0,0.0],[-0.25,0.25,0.25],[-0.125,0.125,0.125]], [[0.5,0.0,0.0],[0.25,-0.25,0.25],[-0.125,0.125,-0.125]], [[0.25,-0.25,0.0],[0.25,-0.25,0.0]], [[0.5,0.0,0.0],[-0.25,-0.25,0.25],[-0.125,-0.125,0.125]], [[-0.25,0.0,0.25],[-0.25,0.0,0.25]], [[0.125,0.125,0.125],[-0.125,0.125,0.125]], [[-0.125,0.125,0.125]], [[0.5,0.0,-0.0],[0.25,0.25,0.25],[0.125,0.125,0.125]], [[0.125,-0.125,0.125],[-0.125,-0.125,0.125]], [[-0.25,-0.0,-0.25],[0.25,0.0,0.25]], [[0.125,-0.125,0.125]], [[-0.25,-0.25,0.0],[0.25,0.25,-0.0]], [[-0.125,-0.125,0.125]], [[0.125,0.125,0.125]], [[0,0,0]]] def compute_surface_distances(mask_gt, mask_pred, spacing_mm): """Compute closest distances from all surface points to the other surface. Finds all surface elements "surfels" in the ground truth mask `mask_gt` and the predicted mask `mask_pred`, computes their area in mm^2 and the distance to the closest point on the other surface. It returns two sorted lists of distances together with the corresponding surfel areas. If one of the masks is empty, the corresponding lists are empty and all distances in the other list are `inf` Args: mask_gt: 3-dim Numpy array of type bool. The ground truth mask. mask_pred: 3-dim Numpy array of type bool. The predicted mask. spacing_mm: 3-element list-like structure. Voxel spacing in x0, x1 and x2 direction Returns: A dict with "distances_gt_to_pred": 1-dim numpy array of type float. The distances in mm from all ground truth surface elements to the predicted surface, sorted from smallest to largest "distances_pred_to_gt": 1-dim numpy array of type float. The distances in mm from all predicted surface elements to the ground truth surface, sorted from smallest to largest "surfel_areas_gt": 1-dim numpy array of type float. The area in mm^2 of the ground truth surface elements in the same order as distances_gt_to_pred "surfel_areas_pred": 1-dim numpy array of type float. The area in mm^2 of the predicted surface elements in the same order as distances_pred_to_gt """ # compute the area for all 256 possible surface elements # (given a 2x2x2 neighbourhood) according to the spacing_mm neighbour_code_to_surface_area = np.zeros([256]) for code in range(256): normals = np.array(neighbour_code_to_normals[code]) sum_area = 0 for normal_idx in range(normals.shape[0]): # normal vector n = np.zeros([3]) n[0] = normals[normal_idx, 0] * spacing_mm[1] * spacing_mm[2] n[1] = normals[normal_idx, 1] * spacing_mm[0] * spacing_mm[2] n[2] = normals[normal_idx, 2] * spacing_mm[0] * spacing_mm[1] area = np.linalg.norm(n) sum_area += area neighbour_code_to_surface_area[code] = sum_area # compute the bounding box of the masks to trim # the volume to the smallest possible processing subvolume mask_all = mask_gt | mask_pred bbox_min = np.zeros(3, np.int64) bbox_max = np.zeros(3, np.int64) # max projection to the x0-axis proj_0 = np.max(np.max(mask_all, axis=2), axis=1) idx_nonzero_0 = np.nonzero(proj_0)[0] if len(idx_nonzero_0) == 0: return {"distances_gt_to_pred": np.array([]), "distances_pred_to_gt": np.array([]), "surfel_areas_gt": np.array([]), "surfel_areas_pred": np.array([])} bbox_min[0] = np.min(idx_nonzero_0) bbox_max[0] = np.max(idx_nonzero_0) # max projection to the x1-axis proj_1 = np.max(np.max(mask_all, axis=2), axis=0) idx_nonzero_1 = np.nonzero(proj_1)[0] bbox_min[1] = np.min(idx_nonzero_1) bbox_max[1] = np.max(idx_nonzero_1) # max projection to the x2-axis proj_2 = np.max(np.max(mask_all, axis=1), axis=0) idx_nonzero_2 = np.nonzero(proj_2)[0] bbox_min[2] = np.min(idx_nonzero_2) bbox_max[2] = np.max(idx_nonzero_2) - print("bounding box min = {}".format(bbox_min)) - print("bounding box max = {}".format(bbox_max)) + # print("bounding box min = {}".format(bbox_min)) + # print("bounding box max = {}".format(bbox_max)) # crop the processing subvolume. # we need to zeropad the cropped region with 1 voxel at the lower, # the right and the back side. This is required to obtain the "full" # convolution result with the 2x2x2 kernel cropmask_gt = np.zeros((bbox_max - bbox_min) + 2, np.uint8) cropmask_pred = np.zeros((bbox_max - bbox_min) + 2, np.uint8) cropmask_gt[0:-1, 0:-1, 0:-1] = mask_gt[bbox_min[0]:bbox_max[0] + 1, bbox_min[1]:bbox_max[1] + 1, bbox_min[2]:bbox_max[2] + 1] cropmask_pred[0:-1, 0:-1, 0:-1] = mask_pred[bbox_min[0]:bbox_max[0] + 1, bbox_min[1]:bbox_max[1] + 1, bbox_min[2]:bbox_max[2] + 1] # compute the neighbour code (local binary pattern) for each voxel # the resultsing arrays are spacially shifted by minus half a voxel in each axis. # i.e. the points are located at the corners of the original voxels kernel = np.array([[[128, 64], [32, 16]], [[8, 4], [2, 1]]]) neighbour_code_map_gt = scipy.ndimage.filters.correlate(cropmask_gt.astype(np.uint8), kernel, mode="constant", cval=0) neighbour_code_map_pred = scipy.ndimage.filters.correlate(cropmask_pred.astype(np.uint8), kernel, mode="constant", cval=0) # create masks with the surface voxels borders_gt = ((neighbour_code_map_gt != 0) & (neighbour_code_map_gt != 255)) borders_pred = ((neighbour_code_map_pred != 0) & (neighbour_code_map_pred != 255)) # compute the distance transform (closest distance of each voxel to the surface voxels) if borders_gt.any(): distmap_gt = scipy.ndimage.morphology.distance_transform_edt(~borders_gt, sampling=spacing_mm) else: distmap_gt = np.Inf * np.ones(borders_gt.shape) if borders_pred.any(): distmap_pred = scipy.ndimage.morphology.distance_transform_edt(~borders_pred, sampling=spacing_mm) else: distmap_pred = np.Inf * np.ones(borders_pred.shape) # compute the area of each surface element surface_area_map_gt = neighbour_code_to_surface_area[neighbour_code_map_gt] surface_area_map_pred = neighbour_code_to_surface_area[neighbour_code_map_pred] # create a list of all surface elements with distance and area distances_gt_to_pred = distmap_pred[borders_gt] distances_pred_to_gt = distmap_gt[borders_pred] surfel_areas_gt = surface_area_map_gt[borders_gt] surfel_areas_pred = surface_area_map_pred[borders_pred] # sort them by distance if distances_gt_to_pred.shape != (0,): sorted_surfels_gt = np.array(sorted(zip(distances_gt_to_pred, surfel_areas_gt))) distances_gt_to_pred = sorted_surfels_gt[:, 0] surfel_areas_gt = sorted_surfels_gt[:, 1] if distances_pred_to_gt.shape != (0,): sorted_surfels_pred = np.array(sorted(zip(distances_pred_to_gt, surfel_areas_pred))) distances_pred_to_gt = sorted_surfels_pred[:, 0] surfel_areas_pred = sorted_surfels_pred[:, 1] return {"distances_gt_to_pred": distances_gt_to_pred, "distances_pred_to_gt": distances_pred_to_gt, "surfel_areas_gt": surfel_areas_gt, "surfel_areas_pred": surfel_areas_pred} def compute_average_surface_distance(surface_distances): distances_gt_to_pred = surface_distances["distances_gt_to_pred"] distances_pred_to_gt = surface_distances["distances_pred_to_gt"] surfel_areas_gt = surface_distances["surfel_areas_gt"] surfel_areas_pred = surface_distances["surfel_areas_pred"] average_distance_gt_to_pred = np.sum(distances_gt_to_pred * surfel_areas_gt) / np.sum(surfel_areas_gt) average_distance_pred_to_gt = np.sum(distances_pred_to_gt * surfel_areas_pred) / np.sum(surfel_areas_pred) return (average_distance_gt_to_pred, average_distance_pred_to_gt) def compute_robust_hausdorff(surface_distances, percent): distances_gt_to_pred = surface_distances["distances_gt_to_pred"] distances_pred_to_gt = surface_distances["distances_pred_to_gt"] surfel_areas_gt = surface_distances["surfel_areas_gt"] surfel_areas_pred = surface_distances["surfel_areas_pred"] if len(distances_gt_to_pred) > 0: surfel_areas_cum_gt = np.cumsum(surfel_areas_gt) / np.sum(surfel_areas_gt) idx = np.searchsorted(surfel_areas_cum_gt, percent / 100.0) perc_distance_gt_to_pred = distances_gt_to_pred[min(idx, len(distances_gt_to_pred) - 1)] else: perc_distance_gt_to_pred = np.Inf if len(distances_pred_to_gt) > 0: surfel_areas_cum_pred = np.cumsum(surfel_areas_pred) / np.sum(surfel_areas_pred) idx = np.searchsorted(surfel_areas_cum_pred, percent / 100.0) perc_distance_pred_to_gt = distances_pred_to_gt[min(idx, len(distances_pred_to_gt) - 1)] else: perc_distance_pred_to_gt = np.Inf return max(perc_distance_gt_to_pred, perc_distance_pred_to_gt) def compute_surface_overlap_at_tolerance(surface_distances, tolerance_mm): distances_gt_to_pred = surface_distances["distances_gt_to_pred"] distances_pred_to_gt = surface_distances["distances_pred_to_gt"] surfel_areas_gt = surface_distances["surfel_areas_gt"] surfel_areas_pred = surface_distances["surfel_areas_pred"] rel_overlap_gt = np.sum(surfel_areas_gt[distances_gt_to_pred <= tolerance_mm]) / np.sum(surfel_areas_gt) rel_overlap_pred = np.sum(surfel_areas_pred[distances_pred_to_gt <= tolerance_mm]) / np.sum(surfel_areas_pred) return (rel_overlap_gt, rel_overlap_pred) def compute_surface_dice_at_tolerance(surface_distances, tolerance_mm): distances_gt_to_pred = surface_distances["distances_gt_to_pred"] distances_pred_to_gt = surface_distances["distances_pred_to_gt"] surfel_areas_gt = surface_distances["surfel_areas_gt"] surfel_areas_pred = surface_distances["surfel_areas_pred"] overlap_gt = np.sum(surfel_areas_gt[distances_gt_to_pred <= tolerance_mm]) overlap_pred = np.sum(surfel_areas_pred[distances_pred_to_gt <= tolerance_mm]) surface_dice = (overlap_gt + overlap_pred) / ( np.sum(surfel_areas_gt) + np.sum(surfel_areas_pred)) - return surface_dice \ No newline at end of file + return surface_dice + + +def compute_multi_instance_surface_dice_at_tolerance(mask_gt, mask_pred, spacing_mm=[1, 1, 1], tolerance=13): + # get number of labels in image + instances_gt = np.unique(mask_gt) + instances_pred = np.unique(mask_pred) + + # create performance matrix + performance_matrix = np.zeros((len(instances_gt), len(instances_pred))) + masks = [] + + # calculate dice score for each ground truth to predicted instance + for counter_gt, instance_gt in enumerate(instances_gt): + + # create binary mask for current gt instance + gt = mask_gt.copy() + gt[mask_gt != instance_gt] = 0 + gt[mask_gt == instance_gt] = 1 + + masks_row = [] + for counter_pred, instance_pred in enumerate(instances_pred): + # make binary mask for current predicted instance + prediction = mask_pred.copy() + prediction[mask_pred != instance_pred] = 0 + prediction[mask_pred == instance_pred] = 1 + + # add additional dimension + if prediction.ndim < 3: + prediction = np.expand_dims(prediction, axis=2) + if gt.ndim < 3: + gt = np.expand_dims(gt, axis=2) + + # calculate surfaces + surface_distances = compute_surface_distances(mask_gt=gt, + mask_pred=prediction, + spacing_mm=spacing_mm) + + # calculate dice + performance_matrix[counter_gt, counter_pred] = compute_surface_dice_at_tolerance(surface_distances, tolerance) + masks_row.append([gt, prediction]) + masks.append(masks_row) + + # assign instrument instances according to hungarian algorithm + label_assignment = hungarian_algorithm(performance_matrix * -1) + label_nr_gt, label_nr_pred = label_assignment + + # get performance per instance + image_performance = [] + for i in range(len(label_nr_gt)): + instance_dice = performance_matrix[label_nr_gt[i], label_nr_pred[i]] + image_performance.append(instance_dice) + + missing_pred = np.absolute(len(instances_pred) - len(image_performance)) + missing_gt = np.absolute(len(instances_gt) - len(image_performance)) + n_missing = np.max([missing_gt, missing_pred]) + + if n_missing > 0: + for i in range(n_missing): + image_performance.append(0) + + output = dict() + for i, performance in enumerate(image_performance): + if i > 0: + output["instrument_{}".format(i - 1)] = performance + else: + output["background"] = performance + + return output \ No newline at end of file diff --git a/evaluation/mean_average_precision_calculations.py b/statistics/mean_average_precision_calculations.py similarity index 79% rename from evaluation/mean_average_precision_calculations.py rename to statistics/mean_average_precision_calculations.py index 5de1cd2..c144dd5 100644 --- a/evaluation/mean_average_precision_calculations.py +++ b/statistics/mean_average_precision_calculations.py @@ -1,183 +1,200 @@ +import time + import numpy as np from pandas import DataFrame from scipy.optimize import linear_sum_assignment as hungarian_algorithm +import matplotlib.pyplot as plt def compute_iou(mask_gt, mask_pred): """ Compute the intersection over union (https://en.wikipedia.org/wiki/Jaccard_index) compute the intersectin over union between the ground truth mask `mask_gt` and the predicted mask `mask_pred`. Args: mask_gt: 3-dim Numpy array of type bool. The ground truth mask. mask_pred: 3-dim Numpy array of type bool. The predicted mask. Returns: the iou coeffcient as float. If both masks are empty, the result is 0 """ mask_gt = mask_gt.astype('bool') mask_pred = mask_pred.astype('bool') overlap = mask_gt * mask_pred # Logical AND union = mask_gt + mask_pred # Logical OR iou = overlap.sum() / float(union.sum()) # Treats "True" as 1, return iou -def compute_statistics(mask_gt, mask_pred): +def compute_statistics(mask_gt, mask_pred, min_iou_for_match=0.03): """ Compute Statistic compute statistics (TP, FP, FN, precision, recall) between the ground truth mask `mask_gt` and the predicted mask `mask_pred`. TP = True positive (defined as an iou>=0.03) FP = False positive FN = False negative precision = true_positive / (true_positive + false_positive) recall = true_positive / (true_positive + false_negative) Args: mask_gt: 3-dim Numpy array of type bool. The ground truth mask. mask_pred: 3-dim Numpy array of type bool. The predicted mask. Returns: output = dict( true_positive=true_positive, false_positive=false_positive, false_negative=false_negative, precision=precision, recall=recall ) """ - # define constants - min_iou_for_match = 0.03 # get number of labels in image instances_gt = list(np.unique(mask_gt)) instances_pred = list(np.unique(mask_pred)) # remove background instances_gt = instances_gt[1:] instances_pred = instances_pred[1:] # create performance matrix performance_matrix = np.zeros((len(instances_gt), len(instances_pred))) masks = [] # calculate dice score for each ground truth to predicted instance for counter_gt, instance_gt in enumerate(instances_gt): # create binary mask for current gt instance gt = mask_gt.copy() gt[mask_gt != instance_gt] = 0 gt[mask_gt == instance_gt] = 1 masks_row = [] for counter_pred, instance_pred in enumerate(instances_pred): # make binary mask for current predicted instance prediction = mask_pred.copy() prediction[mask_pred != instance_pred] = 0 prediction[mask_pred == instance_pred] = 1 # calculate iou # show_image(gt, prediction) iou = compute_iou(gt, prediction) performance_matrix[counter_gt, counter_pred] = iou masks_row.append([gt, prediction]) masks.append(masks_row) # delete all matches smaller than threshold performance_matrix[performance_matrix < min_iou_for_match] = 0 # assign instrument instances according to hungarian algorithm label_assignment = hungarian_algorithm(performance_matrix * -1) label_nr_gt, label_nr_pred = label_assignment # get performance per instance - true_positive_list = [] for i in range(len(label_nr_gt)): instance_iou = performance_matrix[label_nr_gt[i], label_nr_pred[i]] true_positive_list.append(instance_iou) true_positive_list = list(filter(lambda a: a != 0, true_positive_list)) # delete all 0s assigned to a label true_positive = len(true_positive_list) false_negative = len(instances_gt) - true_positive false_positive = len(instances_pred) - true_positive try: precision = true_positive / (true_positive + false_positive) except ZeroDivisionError: precision = 0 try: recall = true_positive / (true_positive + false_negative) except ZeroDivisionError: recall = 0 output = dict( true_positive=true_positive, false_positive=false_positive, false_negative=false_negative, precision=precision, recall=recall ) return output -def compute_mean_average_precision(statistic_list): +def compute_mean_average_precision(statistic_list, plot_curve=False): """ Compute the mean average precision: (https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Mean_average_precision) We define average precision as Area under Curve AUC) https://medium.com/@jonathan_hui/map-mean-average-precision-for-object-detection-45c121a31173 Args: statistic_list: 1-dim list, containing statistics dicts (dict definition, see function compute_statistics). Returns: the area_under_curve as float ) """ # create data frame data_frame = DataFrame(columns=["true_positive", "false_positive", "false_negative", "precision", "recall"]) # add data data_frame = data_frame.append(statistic_list) data_frame = data_frame.reset_index() # interpolate precision with highest recall for precision - data_frame = data_frame.sort_values(by="recall", ascending=False) + data_frame = data_frame.sort_values(by="recall", ascending=True) # changed from false to true! precision_interpolated = [] current_highest_value = 0 for index, row in data_frame.iterrows(): if row.precision > current_highest_value: current_highest_value = row.precision precision_interpolated.append(current_highest_value) data_frame['precision_interpolated'] = precision_interpolated # get changes in interpolated precision curve data_frame_grouped = data_frame.groupby("recall") changes = [] for item in data_frame_grouped.groups.items(): current_recall = item[0] idx_precision = item[1][0] current_precision_interpolated = data_frame.loc[idx_precision].precision_interpolated - change = dict(recall=current_recall, precision_interpolated=current_precision_interpolated) + current_precision = data_frame.loc[idx_precision].precision + change = dict(recall=current_recall, precision_interpolated=current_precision_interpolated, precision=current_precision) changes.append(change) # add end and starting point if changes[0]["recall"] != 0.0: - changes.insert(0, dict(recall=0, precision_interpolated=changes[0]["precision_interpolated"])) + changes.insert(0, dict(recall=0, precision_interpolated=changes[0]["precision_interpolated"], precision=changes[0]["precision"])) if current_recall < 1: - changes.append(dict(recall=1, precision_interpolated=current_precision_interpolated)) + changes.append(dict(recall=1, precision_interpolated=current_precision_interpolated, precision=current_precision)) + + if plot_curve: + plot_precision = [change['precision'] for change in changes] + plot_precision_interpolated = [change['precision_interpolated'] for change in changes] + plot_recall = [change['recall'] for change in changes] + plt.figure() + plt.plot(plot_recall[::-1], plot_precision, '-o') + plt.plot(plot_recall[::-1], plot_precision_interpolated, '-o') + time.time() + plt.savefig(f'/media/rosst/hdd_data/development/robust_mis_2019/precision_recall_curves/{time.time()}.png') + # calculate area under curve area_under_curve = 0 - for i in range(1, len(changes)): - precision_area = (changes[i]["recall"] - changes[i - 1]["recall"]) * changes[i]["precision_interpolated"] + auc_recall = [change['recall'] for change in changes] + auc_recall = auc_recall[::-1] + auc_precision = [change['precision'] for change in changes] + # for i in range(1, len(changes)): # start at one because we use the previous element + # precision_area = (changes[i]["recall"] - changes[i - 1]["recall"]) * changes[i]["precision_interpolated"] + # area_under_curve += precision_area + for i in range(1, len(auc_recall)): + precision_area = (auc_recall[i-1]- auc_recall[i]) * auc_precision[i] area_under_curve += precision_area - return area_under_curve diff --git a/evaluation/test/images/img1/instrument_instances.png b/statistics/test/images/img1/instrument_instances.png similarity index 100% rename from evaluation/test/images/img1/instrument_instances.png rename to statistics/test/images/img1/instrument_instances.png diff --git a/evaluation/test/images/img1/raw.png b/statistics/test/images/img1/raw.png similarity index 100% rename from evaluation/test/images/img1/raw.png rename to statistics/test/images/img1/raw.png diff --git a/evaluation/test/images/img2/instrument_instances.png b/statistics/test/images/img2/instrument_instances.png similarity index 100% rename from evaluation/test/images/img2/instrument_instances.png rename to statistics/test/images/img2/instrument_instances.png diff --git a/evaluation/test/images/img2/raw.png b/statistics/test/images/img2/raw.png similarity index 100% rename from evaluation/test/images/img2/raw.png rename to statistics/test/images/img2/raw.png diff --git a/evaluation/test/images/img3/instrument_instances.png b/statistics/test/images/img3/instrument_instances.png similarity index 100% rename from evaluation/test/images/img3/instrument_instances.png rename to statistics/test/images/img3/instrument_instances.png diff --git a/evaluation/test/images/img3/raw.png b/statistics/test/images/img3/raw.png similarity index 100% rename from evaluation/test/images/img3/raw.png rename to statistics/test/images/img3/raw.png diff --git a/evaluation/test/images/test_map/annotation_1.png b/statistics/test/images/test_map/annotation_1.png similarity index 100% rename from evaluation/test/images/test_map/annotation_1.png rename to statistics/test/images/test_map/annotation_1.png diff --git a/evaluation/test/images/test_map/annotation_2.png b/statistics/test/images/test_map/annotation_2.png similarity index 100% rename from evaluation/test/images/test_map/annotation_2.png rename to statistics/test/images/test_map/annotation_2.png diff --git a/evaluation/test/images/test_map/annotation_3.png b/statistics/test/images/test_map/annotation_3.png similarity index 100% rename from evaluation/test/images/test_map/annotation_3.png rename to statistics/test/images/test_map/annotation_3.png diff --git a/evaluation/test/images/test_map/gt_1.png b/statistics/test/images/test_map/gt_1.png similarity index 100% rename from evaluation/test/images/test_map/gt_1.png rename to statistics/test/images/test_map/gt_1.png diff --git a/evaluation/test/images/test_map/gt_2.png b/statistics/test/images/test_map/gt_2.png similarity index 100% rename from evaluation/test/images/test_map/gt_2.png rename to statistics/test/images/test_map/gt_2.png diff --git a/evaluation/test/images/test_map/gt_3.png b/statistics/test/images/test_map/gt_3.png similarity index 100% rename from evaluation/test/images/test_map/gt_3.png rename to statistics/test/images/test_map/gt_3.png diff --git a/evaluation/test/test_detection_metric.py b/statistics/test/test_detection_metric.py similarity index 98% rename from evaluation/test/test_detection_metric.py rename to statistics/test/test_detection_metric.py index 2759407..f9b2595 100644 --- a/evaluation/test/test_detection_metric.py +++ b/statistics/test/test_detection_metric.py @@ -1,333 +1,333 @@ import unittest import numpy as np -from evaluation.mean_average_precision_calculations import compute_mean_average_precision, compute_iou, \ - compute_statistics +from statistics.mean_average_precision_calculations import compute_iou, compute_statistics, \ + compute_mean_average_precision class TestMAPCalculation(unittest.TestCase): def test_intersection_over_union(self): # define ground truth gt_1 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) gt_2 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) # full intersection expected_iou_1 = 1 pred_1 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) # one missing expected_iou_2 = 0.8888888 pred_2 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 0, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) # just one intersected expected_iou_3 = 0.11111 pred_3 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) # no intersection expected_iou_4 = 0 pred_4 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 1, 1, 1], [0, 0, 0, 0, 1, 1, 1]], np.uint8) # empty prediction expected_iou_5 = 0 pred_5 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) delta = 0.0005 self.assertAlmostEqual(compute_iou(mask_gt=gt_1, mask_pred=pred_1), expected_iou_1, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_1, mask_pred=pred_2), expected_iou_2, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_1, mask_pred=pred_3), expected_iou_3, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_1, mask_pred=pred_4), expected_iou_4, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_1, mask_pred=pred_5), expected_iou_5, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_2, mask_pred=pred_4), expected_iou_4, delta=delta) self.assertAlmostEqual(compute_iou(mask_gt=gt_2, mask_pred=pred_5), expected_iou_5, delta=delta) self.assertTrue(np.isnan(compute_iou(mask_gt=pred_5, mask_pred=pred_5))) def test_detection_statistics(self): # define images img_1 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) img_2 = np.array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) img_3 = np.array([[1, 1, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 2, 2, 0], [0, 0, 0, 0, 2, 2, 0], [0, 0, 0, 0, 0, 0, 0]], np.uint8) img_4 = np.array([[1, 1, 0, 0, 0, 0, 0], [1, 1, 0, 0, 3, 3, 0], [0, 0, 0, 0, 3, 3, 0], [0, 4, 4, 0, 0, 0, 0], [0, 4, 4, 0, 2, 2, 0], [5, 5, 0, 0, 2, 2, 0], [5, 5, 0, 0, 0, 0, 0]], np.uint8) # statistics # precision = true_positive / (true_positive + false_positive) # recall = true_positive / (true_positive + false_negative) result_test_case_1 = compute_statistics(mask_gt=img_1, mask_pred=img_1) expectation_test_case_1 = dict( true_positive=0, false_positive=0, false_negative=0, precision=0, recall=0 ) result_test_case_2 = compute_statistics(mask_gt=img_1, mask_pred=img_2) expectation_test_case_2 = dict( true_positive=0, false_positive=1, false_negative=0, precision=0, recall=0 ) result_test_case_3 = compute_statistics(mask_gt=img_2, mask_pred=img_1) expectation_test_case_3 = dict( true_positive=0, false_positive=0, false_negative=1, precision=0, recall=0 ) result_test_case_4 = compute_statistics(mask_gt=img_3, mask_pred=img_2) expectation_test_case_4 = dict( true_positive=0, false_positive=1, false_negative=2, precision=0, recall=0 ) result_test_case_5 = compute_statistics(mask_gt=img_3, mask_pred=img_4) expectation_test_case_5 = dict( true_positive=2, false_positive=3, false_negative=0, precision=2 / (2 + 3), recall=2 / (2 + 0) ) result_test_case_6 = compute_statistics(mask_gt=img_4, mask_pred=img_4) # expect fp=0, tp=2, fn=3 expectation_test_case_6 = dict( true_positive=5, false_positive=0, false_negative=0, precision=1, recall=1 ) self.assertDictEqual(result_test_case_1, expectation_test_case_1) self.assertDictEqual(result_test_case_2, expectation_test_case_2) self.assertDictEqual(result_test_case_3, expectation_test_case_3) self.assertDictEqual(result_test_case_4, expectation_test_case_4) self.assertDictEqual(result_test_case_5, expectation_test_case_5) self.assertDictEqual(result_test_case_6, expectation_test_case_6) def test_mean_average_precision(self): statistics_list_1 = [ dict(true_positive=0, false_positive=0, false_negative=0, precision=1.0, recall=0.2), dict(true_positive=0, false_positive=0, false_negative=0, precision=1.0, recall=0.4), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.67, recall=0.4), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.4), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.4, recall=0.4), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.6), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.57, recall=0.8), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.8), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.44, recall=0.8), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=1.0) ] statistics_list_2 = [ dict(true_positive=0, false_positive=0, false_negative=0, precision=1.0, recall=0.090909), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.090909), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.666667, recall=0.166667), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.75, recall=0.230769), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.6, recall=0.230769), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.666667, recall=0.285714), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.714286, recall=0.33333), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.75, recall=0.375), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.66667, recall=0.375), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.7, recall=0.411765), ] statistics_list_3 = [ dict(true_positive=0, false_positive=0, false_negative=0, precision=1.0, recall=0.33), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.33), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.67, recall=0.67), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=0.67), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.4, recall=0.67), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.5, recall=1.0), dict(true_positive=0, false_positive=0, false_negative=0, precision=0.43, recall=1.0), ] delta = 0.0005 self.assertAlmostEqual(compute_mean_average_precision(statistics_list_1), (0.4*1.0+0.4*0.57+0.2*0.5), delta=delta) self.assertAlmostEqual(compute_mean_average_precision(statistics_list_2), (0.09*1+0.285*0.75+0.625*0.7), delta=delta) self.assertAlmostEqual(compute_mean_average_precision(statistics_list_3), 0.33*1+0.34*0.67+0.33*0.5, delta=delta) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/evaluation/test/test_distances.py b/statistics/test/test_distances.py similarity index 98% rename from evaluation/test/test_distances.py rename to statistics/test/test_distances.py index 3177894..f1da3b5 100644 --- a/evaluation/test/test_distances.py +++ b/statistics/test/test_distances.py @@ -1,147 +1,147 @@ import unittest import numpy as np from imageio import imread # single pixels, 2mm away -from evaluation.dice_calculations import compute_dice_coefficient -from evaluation.distance_calculations import compute_surface_distances, compute_surface_dice_at_tolerance, \ +from statistics.dice_calculations import compute_dice_coefficient +from statistics.distance_calculations import compute_surface_distances, compute_surface_dice_at_tolerance, \ compute_average_surface_distance, compute_robust_hausdorff, compute_surface_overlap_at_tolerance class TestDiceCalculation(unittest.TestCase): def setUp(self): self.delta = 0.0005 def test_surface_dice(self): path = "images\\img{}\\instrument_instances.png".format(3) # read image x = imread(path) # make image binary x[x < 0.5] = 0 x[x >= 0.5] = 1 mask_gt = np.reshape(x,x.shape + (1,)) surface_distances = compute_surface_distances(mask_gt, mask_gt, (1,1,1)) surface_dice = compute_surface_dice_at_tolerance(surface_distances, 1) self.assertAlmostEqual(surface_dice, 1.0, delta=self.delta) def test_single_pixels_2mm_away(self): mask_gt = np.zeros((128, 128, 128), np.uint8) mask_pred = np.zeros((128, 128, 128), np.uint8) mask_gt[50, 60, 70] = 1 mask_pred[50, 60, 72] = 1 surface_distances = compute_surface_distances(mask_gt, mask_pred, spacing_mm=(3, 2, 1)) surface_dice_1mm = compute_surface_dice_at_tolerance(surface_distances, 1) volumetric_dice = compute_dice_coefficient(mask_gt, mask_pred) print("surface dice at 1mm: {}".format(surface_dice_1mm)) print("volumetric dice: {}".format(volumetric_dice)) self.assertAlmostEqual(surface_dice_1mm, 0.5, delta=self.delta) self.assertAlmostEqual(volumetric_dice, 0.0, delta=self.delta) def test_two_cubes(self): # two cubes. cube 1 is 100x100x100 mm^3 and cube 2 is 102x100x100 mm^3 mask_gt = np.zeros((100, 100, 100), np.uint8) mask_pred = np.zeros((100, 100, 100), np.uint8) spacing_mm = (2, 1, 1) mask_gt[0:50, :, :] = 1 mask_pred[0:51, :, :] = 1 surface_distances = compute_surface_distances(mask_gt, mask_pred, spacing_mm) expected_average_distance_gt_to_pred = 0.836145008498 expected_volumetric_dice = 2. * 100 * 100 * 100 / (100 * 100 * 100 + 102 * 100 * 100) surface_dice_1mm = compute_surface_dice_at_tolerance(surface_distances, 1) volumetric_dice = compute_dice_coefficient(mask_gt, mask_pred) print("surface dice at 1mm: {}".format(compute_surface_dice_at_tolerance(surface_distances, 1))) print("volumetric dice: {}".format(compute_dice_coefficient(mask_gt, mask_pred))) self.assertAlmostEqual(surface_dice_1mm, expected_average_distance_gt_to_pred, delta=self.delta) self.assertAlmostEqual(volumetric_dice, expected_volumetric_dice, delta=self.delta) def test_empty_mask_in_pred(self): # test empty mask in prediction mask_gt = np.zeros((128, 128, 128), np.uint8) mask_pred = np.zeros((128, 128, 128), np.uint8) mask_gt[50, 60, 70] = 1 # mask_pred[50,60,72] = 1 surface_distances = compute_surface_distances(mask_gt, mask_pred, spacing_mm=(3, 2, 1)) average_surface_distance = compute_average_surface_distance(surface_distances) hausdorf_100 = compute_robust_hausdorff(surface_distances, 100) hausdorf_95 = compute_robust_hausdorff(surface_distances, 95) surface_overlap_1_mm = compute_surface_overlap_at_tolerance(surface_distances, 1) surface_dice_1mm = compute_surface_dice_at_tolerance(surface_distances, 1) volumetric_dice = compute_dice_coefficient(mask_gt, mask_pred) print("average surface distance: {} mm".format(average_surface_distance)) print("hausdorff (100%): {} mm".format(hausdorf_100)) print("hausdorff (95%): {} mm".format(hausdorf_95)) print("surface overlap at 1mm: {}".format(surface_overlap_1_mm)) print("surface dice at 1mm: {}".format(surface_dice_1mm)) print("volumetric dice: {}".format(volumetric_dice)) self.assertAlmostEqual(surface_dice_1mm, 0.0, delta=self.delta) self.assertAlmostEqual(volumetric_dice, 0.0, delta=self.delta) def test_empty_mask_in_gt(self): # test empty mask in ground truth mask_gt = np.zeros((128, 128, 128), np.uint8) mask_pred = np.zeros((128, 128, 128), np.uint8) # mask_gt[50,60,70] = 1 mask_pred[50, 60, 72] = 1 surface_distances = compute_surface_distances(mask_gt, mask_pred, spacing_mm=(3, 2, 1)) average_surface_distance = compute_average_surface_distance(surface_distances) hausdorf_100 = compute_robust_hausdorff(surface_distances, 100) hausdorf_95 = compute_robust_hausdorff(surface_distances, 95) surface_overlap_1_mm = compute_surface_overlap_at_tolerance(surface_distances, 1) surface_dice_1mm = compute_surface_dice_at_tolerance(surface_distances, 1) volumetric_dice = compute_dice_coefficient(mask_gt, mask_pred) print("average surface distance: {} mm".format(average_surface_distance)) print("hausdorff (100%): {} mm".format(hausdorf_100)) print("hausdorff (95%): {} mm".format(hausdorf_95)) print("surface overlap at 1mm: {}".format(surface_overlap_1_mm)) print("surface dice at 1mm: {}".format(surface_dice_1mm)) print("volumetric dice: {}".format(volumetric_dice)) self.assertAlmostEqual(surface_dice_1mm, 0.0, delta=self.delta) self.assertAlmostEqual(volumetric_dice, 0.0, delta=self.delta) def test_empty_mask_in_gt_and_pred(self): # test both masks empty mask_gt = np.zeros((128, 128, 128), np.uint8) mask_pred = np.zeros((128, 128, 128), np.uint8) # mask_gt[50,60,70] = 1 # mask_pred[50,60,72] = 1 surface_distances = compute_surface_distances(mask_gt, mask_pred, spacing_mm=(3, 2, 1)) average_surface_distance = compute_average_surface_distance(surface_distances) hausdorf_100 = compute_robust_hausdorff(surface_distances, 100) hausdorf_95 = compute_robust_hausdorff(surface_distances, 95) surface_overlap_1_mm = compute_surface_overlap_at_tolerance(surface_distances, 1) surface_dice_1mm = compute_surface_dice_at_tolerance(surface_distances, 1) volumetric_dice = compute_dice_coefficient(mask_gt, mask_pred) print("average surface distance: {} mm".format(average_surface_distance)) print("hausdorff (100%): {} mm".format(hausdorf_100)) print("hausdorff (95%): {} mm".format(hausdorf_95)) print("surface overlap at 1mm: {}".format(surface_overlap_1_mm)) print("surface dice at 1mm: {}".format(surface_dice_1mm)) print("volumetric dice: {}".format(volumetric_dice)) self.assertTrue(np.isnan(surface_dice_1mm)) self.assertTrue(np.isnan(volumetric_dice)) if __name__ == '__main__': unittest.main() diff --git a/evaluation/test/test_instance_dice.py b/statistics/test/test_instance_dice.py similarity index 96% rename from evaluation/test/test_instance_dice.py rename to statistics/test/test_instance_dice.py index d764f3b..43b9427 100644 --- a/evaluation/test/test_instance_dice.py +++ b/statistics/test/test_instance_dice.py @@ -1,54 +1,55 @@ import unittest from imageio import imread -from evaluation.dice_calculations import compute_dice_coefficient, compute_dice_coefficient_per_instance + +from statistics.dice_calculations import compute_dice_coefficient, compute_dice_coefficient_per_instance class TestDiceCalculation(unittest.TestCase): def test_dice_coefficient(self): # paths x_path = "images/img{}/instrument_instances.png".format(1) y_path = "images/img{}/instrument_instances.png".format(2) # read images x = imread(x_path) y = imread(y_path) # make images binary x[x < 0.5] = 0 x[x >= 0.5] = 1 y[y < 0.5] = 0 y[y >= 0.5] = 1 # calculate dice dice = compute_dice_coefficient(x, y) # check if correct expected_dice = 0.011 delta = 0.0005 self.assertAlmostEqual(dice, expected_dice, delta=delta) def test_multiple_instance_dice_coefficient(self): # paths x_path = "images/img{}/instrument_instances.png".format(2) y_path = "images/img{}/instrument_instances.png".format(3) # read images x = imread(x_path) y = imread(y_path) # calculate instance dice instance_dice_scores = compute_dice_coefficient_per_instance(x, y) # check if correct expected_dice_scores = dict(background=0.8789, instrument_0=0, instrument_1=0.1676) delta = 0.0005 for dice_key, expected_dice_key in zip(instance_dice_scores, expected_dice_scores): dice = instance_dice_scores[dice_key] expected_dice = expected_dice_scores[expected_dice_key] self.assertAlmostEqual(dice, expected_dice, delta=delta) if __name__ == '__main__': unittest.main()