| from math import log10 |
| import numpy as np |
| import torch |
| from sklearn.metrics import confusion_matrix |
| import logging |
|
|
|
|
| def PSNR(mse, peak=1.): |
| return 10 * log10((peak ** 2) / mse) |
|
|
|
|
| class SegMetric: |
| def __init__(self, values=0.): |
| assert isinstance(values, dict) |
| self.miou = values.miou |
| self.oa = values.get('oa', None) |
| self.miou = values.miou |
| self.miou = values.miou |
|
|
|
|
| def better_than(self, other): |
| if self.acc > other.acc: |
| return True |
| else: |
| return False |
|
|
| def state_dict(self): |
| _dict = dict() |
| _dict['acc'] = self.acc |
| return _dict |
|
|
|
|
| class AverageMeter(object): |
| """Computes and stores the average and current value""" |
| def __init__(self): |
| self.reset() |
|
|
| def reset(self): |
| self.val = 0 |
| self.avg = 0 |
| self.sum = 0 |
| self.count = 0 |
|
|
| def update(self, val, n=1): |
| self.val = val |
| self.sum += val * n |
| self.count += n |
| self.avg = self.sum / self.count |
|
|
|
|
| class ConfusionMatrix: |
| """Accumulate a confusion matrix for a classification task. |
| ignore_index only supports index <0, or > num_classes |
| """ |
|
|
| def __init__(self, num_classes, ignore_index=None): |
| self.value = 0 |
| self.num_classes = num_classes |
| self.virtual_num_classes = num_classes + 1 if ignore_index is not None else num_classes |
| self.ignore_index = ignore_index |
|
|
| @torch.no_grad() |
| def update(self, pred, true): |
| """Update the confusion matrix with the given predictions.""" |
| true = true.flatten() |
| pred = pred.flatten() |
| if self.ignore_index is not None: |
| if (true == self.ignore_index).sum() > 0: |
| pred[true == self.ignore_index] = self.virtual_num_classes -1 |
| true[true == self.ignore_index] = self.virtual_num_classes -1 |
| unique_mapping = true.flatten() * self.virtual_num_classes + pred.flatten() |
| bins = torch.bincount(unique_mapping, minlength=self.virtual_num_classes**2) |
| self.value += bins.view(self.virtual_num_classes, self.virtual_num_classes)[:self.num_classes, :self.num_classes] |
|
|
| def reset(self): |
| """Reset all accumulated values.""" |
| self.value = 0 |
|
|
| @property |
| def tp(self): |
| """Get the true positive samples per-class.""" |
| return self.value.diag() |
| |
| @property |
| def actual(self): |
| """Get the false negative samples per-class.""" |
| return self.value.sum(dim=1) |
|
|
| @property |
| def predicted(self): |
| """Get the false negative samples per-class.""" |
| return self.value.sum(dim=0) |
| |
| @property |
| def fn(self): |
| """Get the false negative samples per-class.""" |
| return self.actual - self.tp |
|
|
| @property |
| def fp(self): |
| """Get the false positive samples per-class.""" |
| return self.predicted - self.tp |
|
|
| @property |
| def tn(self): |
| """Get the true negative samples per-class.""" |
| actual = self.actual |
| predicted = self.predicted |
| return actual.sum() + self.tp - (actual + predicted) |
|
|
| @property |
| def count(self): |
| """Get the number of samples per-class.""" |
| |
| return self.value.sum(dim=1) |
|
|
| @property |
| def frequency(self): |
| """Get the per-class frequency.""" |
| |
| |
| count = self.value.sum(dim=1) |
| return count / count.sum().clamp(min=1) |
|
|
| @property |
| def total(self): |
| """Get the total number of samples.""" |
| return self.value.sum() |
|
|
| @property |
| def overall_accuray(self): |
| return self.tp.sum() / self.total |
|
|
| @property |
| def union(self): |
| return self.value.sum(dim=0) + self.value.sum(dim=1) - self.value.diag() |
|
|
| def all_acc(self): |
| return self.cal_acc(self.tp, self.count) |
|
|
| @staticmethod |
| def cal_acc(tp, count): |
| acc_per_cls = tp / count.clamp(min=1) * 100 |
| over_all_acc = tp.sum() / count.sum() * 100 |
| macc = torch.mean(acc_per_cls) |
| return macc.item(), over_all_acc.item(), acc_per_cls.cpu().numpy() |
|
|
| @staticmethod |
| def print_acc(accs): |
| out = '\n Class ' + ' Acc ' |
| for i, values in enumerate(accs): |
| out += '\n' + str(i).rjust(8) + f'{values.item():.2f}'.rjust(8) |
| out += '\n' + '-' * 20 |
| out += '\n' + ' Mean ' + f'{torch.mean(accs).item():.2f}'.rjust(8) |
| logging.info(out) |
|
|
| def all_metrics(self): |
| tp, fp, fn = self.tp, self.fp, self.fn, |
| |
| iou_per_cls = tp / (tp + fp + fn).clamp(min=1) * 100 |
| acc_per_cls = tp / self.count.clamp(min=1) * 100 |
| over_all_acc = tp.sum() / self.total * 100 |
|
|
| miou = torch.mean(iou_per_cls) |
| macc = torch.mean(acc_per_cls) |
| return miou.item(), macc.item(), over_all_acc.item(), iou_per_cls.cpu().numpy(), acc_per_cls.cpu().numpy() |
|
|
|
|
| def get_mious(tp, union, count): |
| iou_per_cls = (tp + 1e-10) / (union + 1e-10) * 100 |
| acc_per_cls = (tp + 1e-10) / (count + 1e-10) * 100 |
| over_all_acc = tp.sum() / count.sum() * 100 |
|
|
| miou = torch.mean(iou_per_cls) |
| macc = torch.mean(acc_per_cls) |
| return miou.item(), macc.item(), over_all_acc.item(), iou_per_cls.cpu().numpy(), acc_per_cls.cpu().numpy() |
|
|
|
|
| def partnet_metrics(num_classes, num_parts, objects, preds, targets): |
| """ |
| |
| Args: |
| num_classes: |
| num_parts: |
| objects: [int] |
| preds:[(num_parts,num_points)] |
| targets: [(num_points)] |
| |
| Returns: |
| |
| """ |
| shape_iou_tot = [0.0] * num_classes |
| shape_iou_cnt = [0] * num_classes |
| part_intersect = [np.zeros((num_parts[o_l]), dtype=np.float32) for o_l in range(num_classes)] |
| part_union = [np.zeros((num_parts[o_l]), dtype=np.float32) + 1e-6 for o_l in range(num_classes)] |
|
|
| for obj, cur_pred, cur_gt in zip(objects, preds, targets): |
| cur_num_parts = num_parts[obj] |
| cur_pred = np.argmax(cur_pred[1:, :], axis=0) + 1 |
| cur_pred[cur_gt == 0] = 0 |
| cur_shape_iou_tot = 0.0 |
| cur_shape_iou_cnt = 0 |
| for j in range(1, cur_num_parts): |
| cur_gt_mask = (cur_gt == j) |
| cur_pred_mask = (cur_pred == j) |
|
|
| has_gt = (np.sum(cur_gt_mask) > 0) |
| has_pred = (np.sum(cur_pred_mask) > 0) |
|
|
| if has_gt or has_pred: |
| intersect = np.sum(cur_gt_mask & cur_pred_mask) |
| union = np.sum(cur_gt_mask | cur_pred_mask) |
| iou = intersect / union |
|
|
| cur_shape_iou_tot += iou |
| cur_shape_iou_cnt += 1 |
|
|
| part_intersect[obj][j] += intersect |
| part_union[obj][j] += union |
| if cur_shape_iou_cnt > 0: |
| cur_shape_miou = cur_shape_iou_tot / cur_shape_iou_cnt |
| shape_iou_tot[obj] += cur_shape_miou |
| shape_iou_cnt[obj] += 1 |
|
|
| msIoU = [shape_iou_tot[o_l] / shape_iou_cnt[o_l] for o_l in range(num_classes)] |
| part_iou = [np.divide(part_intersect[o_l][1:], part_union[o_l][1:]) for o_l in range(num_classes)] |
| mpIoU = [np.mean(part_iou[o_l]) for o_l in range(num_classes)] |
|
|
| |
| mmsIoU = np.mean(np.array(msIoU)) |
| mmpIoU = np.mean(mpIoU) |
|
|
| return msIoU, mpIoU, mmsIoU, mmpIoU |
|
|
|
|
| def IoU_from_confusions(confusions): |
| """ |
| Computes IoU from confusion matrices. |
| :param confusions: ([..., n_c, n_c] np.int32). Can be any dimension, the confusion matrices should be described by |
| the last axes. n_c = number of classes |
| :param ignore_unclassified: (bool). True if the the first class should be ignored in the results |
| :return: ([..., n_c] np.float32) IoU score |
| """ |
|
|
| |
| |
| TP = np.diagonal(confusions, axis1=-2, axis2=-1) |
| TP_plus_FN = np.sum(confusions, axis=-1) |
| TP_plus_FP = np.sum(confusions, axis=-2) |
|
|
| |
| IoU = TP / (TP_plus_FP + TP_plus_FN - TP + 1e-6) |
|
|
| |
| mask = TP_plus_FN < 1e-3 |
| counts = np.sum(1 - mask, axis=-1, keepdims=True) |
| miou = np.sum(IoU, axis=-1, keepdims=True) / (counts + 1e-6) |
|
|
| |
| IoU += mask * miou |
|
|
| return IoU |
|
|
|
|
| def shapenetpart_metrics(num_classes, num_parts, objects, preds, targets, masks): |
| """ |
| Args: |
| num_classes: |
| num_parts: |
| objects: [int] |
| preds:[(num_parts,num_points)] |
| targets: [(num_points)] |
| masks: [(num_points)] |
| """ |
| total_correct = 0.0 |
| total_seen = 0.0 |
| Confs = [] |
| for obj, cur_pred, cur_gt, cur_mask in zip(objects, preds, targets, masks): |
| obj = int(obj) |
| cur_num_parts = num_parts[obj] |
| cur_pred = np.argmax(cur_pred, axis=0) |
| cur_pred = cur_pred[cur_mask] |
| cur_gt = cur_gt[cur_mask] |
| correct = np.sum(cur_pred == cur_gt) |
| total_correct += correct |
| total_seen += cur_pred.shape[0] |
| parts = [j for j in range(cur_num_parts)] |
| Confs += [confusion_matrix(cur_gt, cur_pred, labels=parts)] |
|
|
| Confs = np.array(Confs) |
| obj_mious = [] |
| objects = np.asarray(objects) |
| for l in range(num_classes): |
| obj_inds = np.where(objects == l)[0] |
| obj_confs = np.stack(Confs[obj_inds]) |
| obj_IoUs = IoU_from_confusions(obj_confs) |
| obj_mious += [np.mean(obj_IoUs, axis=-1)] |
|
|
| objs_average = [np.mean(mious) for mious in obj_mious] |
| instance_average = np.mean(np.hstack(obj_mious)) |
| class_average = np.mean(objs_average) |
| acc = total_correct / total_seen |
|
|
| print('Objs | Inst | Air Bag Cap Car Cha Ear Gui Kni Lam Lap Mot Mug Pis Roc Ska Tab') |
| print('-----|------|--------------------------------------------------------------------------------') |
|
|
| s = '{:4.1f} | {:4.1f} | '.format(100 * class_average, 100 * instance_average) |
| for Amiou in objs_average: |
| s += '{:4.1f} '.format(100 * Amiou) |
| print(s + '\n') |
| return acc, objs_average, class_average, instance_average |
|
|