from pycocotools import mask as mask_utils
from collections import defaultdict
import numpy as np
from .data import Data
from . import functions as f
class APDataObject:
"""
Stores all the information necessary to calculate the AP for one IoU and one class.
Note: I type annotated this because why not.
"""
def __init__(self):
self.data_points = {}
self.false_negatives = set()
self.num_gt_positives = 0
self.curve = None
def apply_qualifier(self, kept_preds:set, kept_gts:set) -> object:
""" Makes a new data object where we remove the ids in the pred and gt lists. """
obj = APDataObject()
num_gt_removed = 0
for pred_id in self.data_points:
score, is_true, info = self.data_points[pred_id]
if is_true and info['matched_with'] not in kept_gts:
num_gt_removed += 1
continue
if pred_id in kept_preds:
obj.data_points[pred_id] = self.data_points[pred_id]
obj.false_negatives = self.false_negatives.intersection(kept_gts)
num_gt_removed += (len(self.false_negatives) - len(obj.false_negatives))
obj.num_gt_positives = self.num_gt_positives - num_gt_removed
return obj
def push(self, id:int, score:float, is_true:bool, info:dict={}):
self.data_points[id] = (score, is_true, info)
def push_false_negative(self, id:int):
self.false_negatives.add(id)
def add_gt_positives(self, num_positives:int):
""" Call this once per image. """
self.num_gt_positives += num_positives
def is_empty(self) -> bool:
return len(self.data_points) == 0 and self.num_gt_positives == 0
def get_pr_curve(self) -> tuple:
if self.curve is None:
self.get_ap()
return self.curve
def get_ap(self) -> float:
""" Warning: result not cached. """
if self.num_gt_positives == 0:
return 0
data_points = list(self.data_points.values())
data_points.sort(key=lambda x: -x[0])
precisions = []
recalls = []
num_true = 0
num_false = 0
for datum in data_points:
if datum[1]: num_true += 1
else: num_false += 1
precision = num_true / (num_true + num_false)
recall = num_true / self.num_gt_positives
precisions.append(precision)
recalls.append(recall)
for i in range(len(precisions)-1, 0, -1):
if precisions[i] > precisions[i-1]:
precisions[i-1] = precisions[i]
resolution = 100
y_range = [0] * (resolution + 1)
x_range = np.array([x / resolution for x in range(resolution + 1)])
recalls = np.array(recalls)
indices = np.searchsorted(recalls, x_range, side='left')
for bar_idx, precision_idx in enumerate(indices):
if precision_idx < len(precisions):
y_range[bar_idx] = precisions[precision_idx]
self.curve = (x_range, y_range)
return sum(y_range) / len(y_range) * 100
class ClassedAPDataObject:
""" Stores an APDataObject for each class in the dataset. """
def __init__(self):
self.objs = defaultdict(lambda: APDataObject())
def apply_qualifier(self, pred_dict:dict, gt_dict:dict) -> object:
ret = ClassedAPDataObject()
for _class, obj in self.objs.items():
pred_list = pred_dict[_class] if _class in pred_dict else set()
gt_list = gt_dict[_class] if _class in gt_dict else set()
ret.objs[_class] = obj.apply_qualifier(pred_list, gt_list)
return ret
def push(self, class_:int, id:int, score:float, is_true:bool, info:dict={}):
self.objs[class_].push(id, score, is_true, info)
def push_false_negative(self, class_:int, id:int):
self.objs[class_].push_false_negative(id)
def add_gt_positives(self, class_:int, num_positives:int):
self.objs[class_].add_gt_positives(num_positives)
def get_mAP(self) -> float:
aps = [x.get_ap() for x in self.objs.values() if not x.is_empty()]
return sum(aps) / len(aps)
def get_gt_positives(self) -> dict:
return {k: v.num_gt_positives for k, v in self.objs.items()}
def get_pr_curve(self, cat_id:int=None) -> tuple:
if cat_id is None:
curves = [x.get_pr_curve() for x in list(self.objs.values())]
x_range = curves[0][0]
y_range = [0] * len(curves[0][1])
for x, y in curves:
for i in range(len(y)):
y_range[i] += y[i]
for i in range(len(y_range)):
y_range[i] /= len(curves)
else:
x_range, y_range = self.objs[cat_id].get_pr_curve()
return x_range, y_range
class APEval:
"""
A class that computes the AP of some dataset.
Note that TIDE doesn't use this internally.
This is here so you can get a look at how the AP calculation process works.
"""
def __init__(self):
self.iou_thresholds = [x / 100 for x in range(50, 100, 5)]
self.ap_data = defaultdict(lambda: defaultdict(lambda: APDataObject()))
def _eval_image(self, preds:list, gt:list, type_str:str='box'):
data_type = 'segmentation' if type_str == 'mask' else 'bbox'
preds_data = [x[data_type] for x in preds]
gt_new = []
gt_crowd = []
for x in gt:
if x['iscrowd']:
gt_crowd.append(x)
else:
gt_new.append(x)
gt = gt_new
num_pred = len(preds)
num_gt = len(gt)
num_crowd = len(gt_crowd)
iou_cache = mask_utils.iou(
preds_data,
[x[data_type] for x in gt],
[False] * num_gt)
if num_crowd > 0:
crowd_iou_cache = mask_utils.iou(
preds_data,
[x[data_type] for x in gt_crowd],
[True] * num_crowd)
indices = list(range(num_pred))
indices.sort(key=lambda i: -preds[i]['score'])
classes = [x['category_id'] for x in preds]
gt_classes = [x['category_id'] for x in gt]
crowd_classes = [x['category_id'] for x in gt_crowd]
for _class in set(classes + gt_classes):
ap_per_iou = []
num_gt_for_class = sum([1 for x in gt_classes if x == _class])
for iouIdx in range(len(self.iou_thresholds)):
iou_threshold = self.iou_thresholds[iouIdx]
gt_used = [False] * len(gt_classes)
ap_obj = self.ap_data[iouIdx][_class]
ap_obj.add_gt_positives(num_gt_for_class)
for i in indices:
if classes[i] != _class:
continue
max_iou_found = iou_threshold
max_match_idx = -1
for j in range(num_gt):
if gt_used[j] or gt_classes[j] != _class:
continue
iou = iou_cache[i][j]
if iou > max_iou_found:
max_iou_found = iou
max_match_idx = j
if max_match_idx >= 0:
gt_used[max_match_idx] = True
ap_obj.push(preds[i]['score'], True)
else:
matched_crowd = False
if num_crowd > 0:
for j in range(len(crowd_classes)):
if crowd_classes[j] != _class:
continue
iou = crowd_iou_cache[i][j]
if iou > iou_threshold:
matched_crowd = True
break
if not matched_crowd:
ap_obj.push(preds[i]['score'], False)
def evaluate(self, preds:Data, gt:Data, type_str:str='box'):
for id in gt.ids:
x = preds.get(id)
y = gt.get(id)
self._eval_image(x, y, type_str)
def compute_mAP(self):
num_threshs = len(self.ap_data)
thresh_APs = []
for thresh, classes in self.ap_data.items():
num_classes = len([x for x in classes.values() if not x.is_empty()])
ap = 0
if num_classes > 0:
class_APs = [x.get_ap() for x in classes.values() if not x.is_empty()]
ap = sum(class_APs) / num_classes
thresh_APs.append(ap)
return round(sum(thresh_APs) / num_threshs * 100, 2)