| | import os, sys, argparse |
| | from copy import deepcopy |
| | import numpy as np |
| | import torch |
| | from evaluation.utils_3d import get_instances |
| |
|
| | parser = argparse.ArgumentParser() |
| | parser.add_argument('--pred_path', required=True, help='path to directory of predicted .txt files') |
| | parser.add_argument('--gt_path', required=True, help='path to directory of ground truth .txt files') |
| | parser.add_argument('--dataset', required=True, help='type of dataset, e.g. matterport3d, scannet, etc.') |
| | parser.add_argument('--output_file', default='', help='path to output file') |
| | parser.add_argument('--no_class', action='store_true', help='class agnostic evaluation') |
| | opt = parser.parse_args() |
| |
|
| | |
| | from evaluation.constants import MATTERPORT_LABELS, MATTERPORT_IDS, SCANNET_LABELS, SCANNET_IDS, SCANNETPP_LABELS, SCANNETPP_IDS |
| |
|
| | if opt.dataset == 'matterport3d': |
| | CLASS_LABELS = MATTERPORT_LABELS |
| | VALID_CLASS_IDS = MATTERPORT_IDS |
| | elif opt.dataset == 'scannet': |
| | CLASS_LABELS = SCANNET_LABELS |
| | VALID_CLASS_IDS = SCANNET_IDS |
| | elif opt.dataset == 'scannetpp': |
| | CLASS_LABELS = SCANNETPP_LABELS |
| | VALID_CLASS_IDS = SCANNETPP_IDS |
| |
|
| |
|
| | if opt.output_file == '': |
| | opt.output_file = os.path.join(f'data/evaluation/{opt.dataset}', opt.pred_path.split('/')[-1] + '.txt') |
| | os.makedirs(os.path.dirname(opt.output_file), exist_ok=True) |
| | if opt.no_class: |
| | if 'class_agnostic' not in opt.output_file: |
| | opt.output_file = opt.output_file.replace('.txt', '_class_agnostic.txt') |
| |
|
| | ID_TO_LABEL = {} |
| | LABEL_TO_ID = {} |
| | for i in range(len(VALID_CLASS_IDS)): |
| | LABEL_TO_ID[CLASS_LABELS[i]] = VALID_CLASS_IDS[i] |
| | ID_TO_LABEL[VALID_CLASS_IDS[i]] = CLASS_LABELS[i] |
| |
|
| | |
| | |
| | opt.overlaps = np.append(np.arange(0.5,0.95,0.05), 0.25) |
| | |
| | opt.min_region_sizes = np.array( [ 100 ] ) |
| | |
| | opt.distance_threshes = np.array( [ float('inf') ] ) |
| | |
| | opt.distance_confs = np.array( [ -float('inf') ] ) |
| |
|
| |
|
| | def evaluate_matches(matches): |
| | overlaps = opt.overlaps |
| | min_region_sizes = [ opt.min_region_sizes[0] ] |
| | dist_threshes = [ opt.distance_threshes[0] ] |
| | dist_confs = [ opt.distance_confs[0] ] |
| | |
| | |
| | ap = np.zeros( (len(dist_threshes) , len(CLASS_LABELS) , len(overlaps)) , float ) |
| | for di, (min_region_size, distance_thresh, distance_conf) in enumerate(zip(min_region_sizes, dist_threshes, dist_confs)): |
| | for oi, overlap_th in enumerate(overlaps): |
| | pred_visited = {} |
| | for m in matches: |
| | for p in matches[m]['pred']: |
| | for label_name in CLASS_LABELS: |
| | for p in matches[m]['pred'][label_name]: |
| | if 'filename' in p: |
| | pred_visited[p['filename']] = False |
| | for li, label_name in enumerate(CLASS_LABELS): |
| | y_true = np.empty(0) |
| | y_score = np.empty(0) |
| | hard_false_negatives = 0 |
| | has_gt = False |
| | has_pred = False |
| | for m in matches: |
| | pred_instances = matches[m]['pred'][label_name] |
| | gt_instances = matches[m]['gt'][label_name] |
| | |
| | gt_instances = [ gt for gt in gt_instances if gt['instance_id']>=1000 and gt['vert_count']>=min_region_size and gt['med_dist']<=distance_thresh and gt['dist_conf']>=distance_conf ] |
| | if gt_instances: |
| | has_gt = True |
| | if pred_instances: |
| | has_pred = True |
| |
|
| | cur_true = np.ones ( len(gt_instances) ) |
| | cur_score = np.ones ( len(gt_instances) ) * (-float("inf")) |
| | cur_match = np.zeros( len(gt_instances) , dtype=bool ) |
| | |
| | for (gti,gt) in enumerate(gt_instances): |
| | found_match = False |
| | num_pred = len(gt['matched_pred']) |
| | for pred in gt['matched_pred']: |
| | |
| | if pred_visited[pred['filename']]: |
| | continue |
| | overlap = float(pred['intersection']) / (gt['vert_count']+pred['vert_count']-pred['intersection']) |
| | if overlap > overlap_th: |
| | confidence = pred['confidence'] |
| | |
| | |
| | if cur_match[gti]: |
| | max_score = max( cur_score[gti] , confidence ) |
| | min_score = min( cur_score[gti] , confidence ) |
| | cur_score[gti] = max_score |
| | |
| | cur_true = np.append(cur_true,0) |
| | cur_score = np.append(cur_score,min_score) |
| | cur_match = np.append(cur_match,True) |
| | |
| | else: |
| | found_match = True |
| | cur_match[gti] = True |
| | cur_score[gti] = confidence |
| | pred_visited[pred['filename']] = True |
| | |
| |
|
| | if not found_match: |
| | hard_false_negatives += 1 |
| | |
| | cur_true = cur_true [ cur_match==True ] |
| | cur_score = cur_score[ cur_match==True ] |
| |
|
| | |
| | for pred in pred_instances: |
| | found_gt = False |
| | for gt in pred['matched_gt']: |
| | overlap = float(gt['intersection']) / (gt['vert_count']+pred['vert_count']-gt['intersection']) |
| | if overlap > overlap_th: |
| | found_gt = True |
| | break |
| | if not found_gt: |
| | num_ignore = pred['void_intersection'] |
| | for gt in pred['matched_gt']: |
| | |
| | if gt['instance_id'] < 1000: |
| | num_ignore += gt['intersection'] |
| | |
| | if gt['vert_count'] < min_region_size or gt['med_dist']>distance_thresh or gt['dist_conf']<distance_conf: |
| | num_ignore += gt['intersection'] |
| | proportion_ignore = float(num_ignore)/pred['vert_count'] |
| | |
| | if proportion_ignore <= overlap_th: |
| | cur_true = np.append(cur_true,0) |
| | confidence = pred["confidence"] |
| | cur_score = np.append(cur_score,confidence) |
| | |
| | y_true = np.append(y_true,cur_true) |
| | y_score = np.append(y_score,cur_score) |
| | |
| | |
| | if has_gt and has_pred: |
| | if len(y_score) == 0: |
| | ap_current = 0.0 |
| | else: |
| | |
| |
|
| | |
| | score_arg_sort = np.argsort(y_score) |
| | y_score_sorted = y_score[score_arg_sort] |
| | y_true_sorted = y_true[score_arg_sort] |
| | y_true_sorted_cumsum = np.cumsum(y_true_sorted) |
| |
|
| | |
| | (thresholds,unique_indices) = np.unique( y_score_sorted , return_index=True ) |
| | num_prec_recall = len(unique_indices) + 1 |
| |
|
| | |
| | num_examples = len(y_score_sorted) |
| | num_true_examples = y_true_sorted_cumsum[-1] |
| | precision = np.zeros(num_prec_recall) |
| | recall = np.zeros(num_prec_recall) |
| |
|
| | |
| | y_true_sorted_cumsum = np.append( y_true_sorted_cumsum , 0 ) |
| | |
| | for idx_res,idx_scores in enumerate(unique_indices): |
| | cumsum = y_true_sorted_cumsum[idx_scores-1] |
| | tp = num_true_examples - cumsum |
| | fp = num_examples - idx_scores - tp |
| | fn = cumsum + hard_false_negatives |
| | p = float(tp)/(tp+fp) |
| | r = float(tp)/(tp+fn) |
| | precision[idx_res] = p |
| | recall [idx_res] = r |
| |
|
| | |
| | precision[-1] = 1. |
| | recall [-1] = 0. |
| |
|
| | |
| | recall_for_conv = np.copy(recall) |
| | recall_for_conv = np.append(recall_for_conv[0], recall_for_conv) |
| | recall_for_conv = np.append(recall_for_conv, 0.) |
| |
|
| | stepWidths = np.convolve(recall_for_conv,[-0.5,0,0.5],'valid') |
| | |
| | ap_current = np.dot(precision, stepWidths) |
| | elif has_gt: |
| | ap_current = 0.0 |
| | else: |
| | ap_current = float('nan') |
| | |
| | ap[di,li,oi] = ap_current |
| | return ap |
| |
|
| | def compute_averages(aps): |
| | d_inf = 0 |
| | o50 = np.where(np.isclose(opt.overlaps,0.5)) |
| | o25 = np.where(np.isclose(opt.overlaps,0.25)) |
| | oAllBut25 = np.where(np.logical_not(np.isclose(opt.overlaps,0.25))) |
| | avg_dict = {} |
| | |
| | avg_dict['all_ap'] = np.nanmean(aps[ d_inf,:,oAllBut25]) |
| | avg_dict['all_ap_50%'] = np.nanmean(aps[ d_inf,:,o50]) |
| | avg_dict['all_ap_25%'] = np.nanmean(aps[ d_inf,:,o25]) |
| | avg_dict["classes"] = {} |
| | for (li,label_name) in enumerate(CLASS_LABELS): |
| | avg_dict["classes"][label_name] = {} |
| | |
| | avg_dict["classes"][label_name]["ap"] = np.average(aps[ d_inf,li,oAllBut25]) |
| | avg_dict["classes"][label_name]["ap50%"] = np.average(aps[ d_inf,li,o50]) |
| | avg_dict["classes"][label_name]["ap25%"] = np.average(aps[ d_inf,li,o25]) |
| | return avg_dict |
| |
|
| | def read_pridiction_npz(path): |
| | pred_info = {} |
| | pred = np.load(path) |
| |
|
| | num_instance = len(pred['pred_score']) |
| | mask = torch.from_numpy(pred['pred_masks']).cuda() |
| | for i in range(num_instance): |
| | pred_info[path.split('/')[-1] + '_' +str(i)] = { |
| | 'mask': mask[:, i].cpu().numpy(), |
| | 'label_id': pred['pred_classes'][i], |
| | 'conf': pred['pred_score'][i] |
| | } |
| | return pred_info |
| |
|
| | def get_gt_tensor(gt_ids, gt_instances): |
| | ''' |
| | return a dict of gt_tensor |
| | ''' |
| | gt_tensor_dict = {} |
| | point_num = len(gt_ids) |
| | for label in gt_instances: |
| | gt_instance_num = len(gt_instances[label]) |
| | gt_tensor = torch.zeros((point_num, gt_instance_num), dtype=torch.bool).cuda() |
| | for i, gt_instance_info in enumerate(gt_instances[label]): |
| | gt_tensor[:, i] = torch.from_numpy(gt_ids == gt_instance_info['instance_id']) |
| | gt_tensor_dict[label] = gt_tensor |
| | return gt_tensor_dict |
| |
|
| | def assign_instances_for_scan(pred_file, gt_file): |
| | ''' |
| | if intersection > 0, then the prediction is considered a match |
| | ''' |
| | pred_info = read_pridiction_npz(os.path.join(pred_file)) |
| | gt_ids = np.loadtxt(gt_file) |
| | |
| | if opt.no_class: |
| | gt_ids = gt_ids % 1000 + VALID_CLASS_IDS[0] * 1000 |
| |
|
| | |
| | gt_instances = get_instances(gt_ids, VALID_CLASS_IDS, CLASS_LABELS, ID_TO_LABEL) |
| | |
| | gt2pred = deepcopy(gt_instances) |
| | for label in gt2pred: |
| | for gt in gt2pred[label]: |
| | gt['matched_pred'] = [] |
| | pred2gt = {} |
| | for label in CLASS_LABELS: |
| | pred2gt[label] = [] |
| | num_pred_instances = 0 |
| | |
| | bool_void = np.logical_not(np.in1d(gt_ids//1000, VALID_CLASS_IDS)) |
| |
|
| | gt_tensor_dict = get_gt_tensor(gt_ids, gt_instances) |
| |
|
| | |
| | for pred_mask_file in (pred_info): |
| | if opt.no_class: |
| | label_id = VALID_CLASS_IDS[0] |
| | else: |
| | label_id = int(pred_info[pred_mask_file]['label_id']) |
| | conf = pred_info[pred_mask_file]['conf'] |
| | if not label_id in ID_TO_LABEL: |
| | continue |
| | label_name = ID_TO_LABEL[label_id] |
| | |
| | pred_mask = pred_info[pred_mask_file]['mask'] |
| |
|
| | if len(pred_mask) != len(gt_ids): |
| | print('wrong number of lines in ' + pred_mask_file + '(%d) vs #mesh vertices (%d), please double check and/or re-download the mesh' % (len(pred_mask), len(gt_ids))) |
| | raise NotImplementedError |
| |
|
| | |
| | pred_mask = np.not_equal(pred_mask, 0) |
| | num = np.count_nonzero(pred_mask) |
| | if num < opt.min_region_sizes[0]: |
| | continue |
| |
|
| | pred_instance = {} |
| | pred_instance['filename'] = pred_mask_file |
| | pred_instance['pred_id'] = num_pred_instances |
| | pred_instance['label_id'] = label_id |
| | pred_instance['vert_count'] = num |
| | pred_instance['confidence'] = conf |
| | pred_instance['void_intersection'] = np.count_nonzero(np.logical_and(bool_void, pred_mask)) |
| |
|
| | |
| | matched_gt = [] |
| | gt_tensor = gt_tensor_dict[label_name] |
| | intersection = torch.sum(gt_tensor & torch.from_numpy(pred_mask).cuda().reshape(-1, 1), dim=0) |
| | intersect_ids = torch.nonzero(intersection).cpu().numpy().reshape(-1) |
| | for gt_id in intersect_ids: |
| | gt_copy = gt_instances[label_name][gt_id].copy() |
| | pred_copy = pred_instance.copy() |
| | intersection_num = intersection[gt_id].item() |
| | gt_copy['intersection'] = intersection_num |
| | pred_copy['intersection'] = intersection_num |
| | matched_gt.append(gt_copy) |
| | gt2pred[label_name][gt_id]['matched_pred'].append(pred_copy) |
| | |
| | pred_instance['matched_gt'] = matched_gt |
| | num_pred_instances += 1 |
| | pred2gt[label_name].append(pred_instance) |
| |
|
| | return gt2pred, pred2gt |
| |
|
| | def print_results(avgs): |
| | sep = "" |
| | col1 = ":" |
| | lineLen = 64 |
| |
|
| | print ("") |
| | print ("#"*lineLen) |
| | line = "" |
| | line += "{:<15}".format("what" ) + sep + col1 |
| | line += "{:>15}".format("AP" ) + sep |
| | line += "{:>15}".format("AP_50%" ) + sep |
| | line += "{:>15}".format("AP_25%" ) + sep |
| | print (line) |
| | print ("#"*lineLen) |
| |
|
| | for (li,label_name) in enumerate(CLASS_LABELS): |
| | ap_avg = avgs["classes"][label_name]["ap"] |
| | if np.isnan(ap_avg): |
| | continue |
| | ap_50o = avgs["classes"][label_name]["ap50%"] |
| | ap_25o = avgs["classes"][label_name]["ap25%"] |
| | line = "{:<15}".format(label_name) + sep + col1 |
| | line += sep + "{:>15.3f}".format(ap_avg ) + sep |
| | line += sep + "{:>15.3f}".format(ap_50o ) + sep |
| | line += sep + "{:>15.3f}".format(ap_25o ) + sep |
| | print (line) |
| |
|
| | all_ap_avg = avgs["all_ap"] |
| | all_ap_50o = avgs["all_ap_50%"] |
| | all_ap_25o = avgs["all_ap_25%"] |
| |
|
| | print ("-"*lineLen) |
| | line = "{:<15}".format("average") + sep + col1 |
| | line += "{:>15.3f}".format(all_ap_avg) + sep |
| | line += "{:>15.3f}".format(all_ap_50o) + sep |
| | line += "{:>15.3f}".format(all_ap_25o) + sep |
| | print (line) |
| | print ("") |
| |
|
| | def write_result_file(avgs, filename): |
| | _SPLITTER = ',' |
| | with open(filename, 'w') as f: |
| | f.write(_SPLITTER.join(['class', 'class id', 'ap', 'ap50', 'ap25']) + '\n') |
| | for i in range(len(VALID_CLASS_IDS)): |
| | class_name = CLASS_LABELS[i] |
| | class_id = VALID_CLASS_IDS[i] |
| | ap = avgs["classes"][class_name]["ap"] |
| | ap50 = avgs["classes"][class_name]["ap50%"] |
| | ap25 = avgs["classes"][class_name]["ap25%"] |
| | f.write(_SPLITTER.join([str(x) for x in [class_name, class_id, ap, ap50, ap25]]) + '\n') |
| | f.write(_SPLITTER.join([str(x) for x in [avgs["all_ap"], avgs["all_ap_50%"], avgs["all_ap_25%"]]]) + '\n') |
| |
|
| | def evaluate(pred_files, gt_files, pred_path, output_file): |
| | print ('evaluating', len(pred_files), 'scans...') |
| | matches = {} |
| | for i in range(len(pred_files)): |
| | matches_key = os.path.abspath(gt_files[i]) |
| | |
| | gt2pred, pred2gt = assign_instances_for_scan(pred_files[i], gt_files[i]) |
| | matches[matches_key] = {} |
| | matches[matches_key]['gt'] = gt2pred |
| | matches[matches_key]['pred'] = pred2gt |
| | sys.stdout.write("\rscans processed: {}".format(i+1)) |
| | sys.stdout.flush() |
| | ap_scores = evaluate_matches(matches) |
| | avgs = compute_averages(ap_scores) |
| |
|
| | |
| | print_results(avgs) |
| | write_result_file(avgs, output_file) |
| |
|
| | def main(): |
| | print('start evaluating:', opt.pred_path.split('/')[-1]) |
| | pred_files = [f for f in sorted(os.listdir(opt.pred_path)) if f.endswith('.npz') and not f.startswith('semantic_instance_evaluation')] |
| | gt_files = [] |
| |
|
| | for i in range(len(pred_files)): |
| | gt_file = os.path.join(opt.gt_path, pred_files[i].replace('.npz', '.txt')) |
| | if not os.path.isfile(gt_file): |
| | print('Result file {} does not match any gt file'.format(pred_files[i])) |
| | raise NotImplementedError |
| |
|
| | gt_files.append(gt_file) |
| | pred_files[i] = os.path.join(opt.pred_path, pred_files[i]) |
| |
|
| | evaluate(pred_files, gt_files, opt.pred_path, opt.output_file) |
| | print('save results to', opt.output_file) |
| |
|
| | if __name__ == '__main__': |
| | main() |
| |
|