Maximum int value for your class label. In COCO dataset we only used 80 classes, but the maxium
value of the class label is 90. In this case num_classes should be 90.
required
Returns:
Type
Description
Mean Average Precision.
Source code in fastestimator\fastestimator\trace\metric\mean_average_precision.py
classMeanAveragePrecision(Trace):"""Calculate COCO mean average precision. Args: num_classes: Maximum `int` value for your class label. In COCO dataset we only used 80 classes, but the maxium value of the class label is `90`. In this case `num_classes` should be `90`. Returns: Mean Average Precision. """def__init__(self,num_classes:int,true_key='bbox',pred_key:str='pred',mode:str="eval",output_name=("mAP","AP50","AP75"))->None:super().__init__(inputs=(true_key,pred_key),outputs=output_name,mode=mode)assertlen(self.outputs)==3,'MeanAvgPrecision trace adds 3 fields mAP AP50 AP75 to state dict'self.iou_thres=np.linspace(.5,0.95,np.round((0.95-.5)/.05).astype(np.int)+1,endpoint=True)self.recall_thres=np.linspace(.0,1.00,np.round((1.00-.0)/.01).astype(np.int)+1,endpoint=True)self.categories=range(1,num_classes+1)# MSCOCO style class label starts from 1self.max_detection=100self.image_ids=[]# evalself.evalimgs={}self.eval={}self.ids_in_epoch=0# reset per epoch# reset per batchself.gt=defaultdict(list)# gt for evaluationself.det=defaultdict(list)self.batch_image_ids=[]# img_ids per batchself.ious=defaultdict(list)self.ids_unique=[]self.ids_batch_to_epoch={}self.counter=0# REMOVE@propertydeftrue_key(self)->str:returnself.inputs[0]@propertydefpred_key(self)->str:returnself.inputs[1]def_get_id_in_epoch(self,idx_in_batch:int)->int:"""Get unique image id in epoch. Id starts from 1. Args: idx_in_batch: Image id within a batch. Returns: Global unique id within one epoch. """# for this batchnum_unique_id_previous=len(np.unique(self.ids_unique))self.ids_unique.append(idx_in_batch)num_unique_id=len(np.unique(self.ids_unique))ifnum_unique_id>num_unique_id_previous:# for epochself.ids_in_epoch+=1self.ids_batch_to_epoch[idx_in_batch]=self.ids_in_epochreturnself.ids_in_epochdefon_epoch_begin(self,data:Data):"""Reset instance variables."""self.image_ids=[]# append all the image ids coming from each iterationself.evalimgs={}self.eval={}self.ids_in_epoch=0defon_batch_begin(self,data:Data):"""Reset instance variables."""self.gt=defaultdict(list)# gt for evaluationself.det=defaultdict(list)# det for evaluationself.batch_image_ids=[]# img_ids per batchself.ious=defaultdict(list)self.ids_unique=[]self.ids_batch_to_epoch={}@staticmethoddef_reshape_gt(gt_array:np.ndarray)->np.ndarray:"""Reshape ground truth and add local image id within batch. The input ground truth array has shape (batch_size, num_bbox, 5). The 5 is [x1, y1, w, h, label] for each bounding box. For output we drop all padded bounding boxes (all zeros), and flatten the batch dimension. The output shape is (batch_size * num_bbox, 6). The 6 is [id_in_batch, x1, y1, w, h, label]. Args: gt_array: Ground truth with shape (batch_size, num_bbox, 5). Returns: Ground truth with shape (batch_size * num_bbox, 6). """local_ids=np.repeat(range(gt_array.shape[0]),gt_array.shape[1],axis=None)local_ids=np.expand_dims(local_ids,axis=-1)gt_with_id=np.concatenate([local_ids,gt_array.reshape(-1,5)],axis=1)keep=gt_with_id[...,-1]>0returngt_with_id[keep]@staticmethoddef_reshape_pred(pred:List[np.ndarray])->np.ndarray:"""Reshape predicted bounding boxes and add local image id within batch. The input prediction array is a list of batch_size elements. For each element inside the list, it has shape (num_bbox, 6). The 6 is [x1, y1, w, h, label, score] for each bounding box. For output we flatten the batch dimension. The output shape is (total_num_bbox_in_batch, 7). The 7 is [id_in_batch, x1, y1, w, h, label, score]. Args: pred: List of predected bounding boxes for each image. Each element in the list has shape (num_bbox, 6). Returns: Predected bounding boxes with shape (total_num_bbox_in_batch, 7). """pred_with_id=[]forindex,iteminenumerate(pred):local_ids=np.repeat([index],item.shape[0],axis=None)local_ids=np.expand_dims(local_ids,axis=-1)pred_with_id.append(np.concatenate([local_ids,item],axis=1))pred_with_id=np.concatenate(pred_with_id,axis=0)returnpred_with_iddefon_batch_end(self,data:Data):# begin of reading det and gtpred=list(map(to_number,data[self.pred_key]))# pred is list (batch, ) of np.ndarray (?, 6)pred=self._reshape_pred(pred)gt=to_number(data[self.true_key])# gt is np.array (batch, box, 5), box dimension is paddedgt=self._reshape_gt(gt)ground_truth_bb=[]forgt_itemingt:idx_in_batch,x1,y1,w,h,label=gt_itemlabel=int(label)id_epoch=self._get_id_in_epoch(idx_in_batch)self.batch_image_ids.append(id_epoch)self.image_ids.append(id_epoch)tmp_dict={'idx':id_epoch,'x1':x1,'y1':y1,'w':w,'h':h,'label':label}ground_truth_bb.append(tmp_dict)predicted_bb=[]forpred_iteminpred:idx_in_batch,x1,y1,w,h,label,score=pred_itemlabel=int(label)id_epoch=self.ids_batch_to_epoch[idx_in_batch]self.image_ids.append(id_epoch)tmp_dict={'idx':id_epoch,'x1':x1,'y1':y1,'w':w,'h':h,'label':label,'score':score}predicted_bb.append(tmp_dict)fordict_eleminground_truth_bb:self.gt[dict_elem['idx'],dict_elem['label']].append(dict_elem)fordict_eleminpredicted_bb:self.det[dict_elem['idx'],dict_elem['label']].append(dict_elem)# end of reading det and gt# compute iou matrix, matrix index is (img_id, cat_id), each element in matrix has shape (num_det, num_gt)self.ious={(img_id,cat_id):self.compute_iou(self.det[img_id,cat_id],self.gt[img_id,cat_id])forimg_idinself.batch_image_idsforcat_idinself.categories}forcat_idinself.categories:forimg_idinself.batch_image_ids:self.evalimgs[(cat_id,img_id)]=self.evaluate_img(cat_id,img_id)defon_epoch_end(self,data:Data):self.accumulate()mean_ap=self.summarize()ap50=self.summarize(iou=0.5)ap75=self.summarize(iou=0.75)data[self.outputs[0]]=mean_apdata[self.outputs[1]]=ap50data[self.outputs[2]]=ap75defevaluate_img(self,cat_id:int,img_id:int)->Dict:"""Find gt matches for det given one image and one category. Args: cat_id: img_id: Returns: """det=self.det[img_id,cat_id]gt=self.gt[img_id,cat_id]num_det=len(det)num_gt=len(gt)ifnum_gt==0andnum_det==0:returnNone# sort detections, is ths necessary?det_index=np.argsort([-d['score']fordindet],kind='mergesort')# cap to max_detectiondet=[det[i]foriindet_index[0:self.max_detection]]# get iou matrix for given (img_id, cat_id), the output has shape (num_det, num_gt)iou_mat=self.ious[img_id,cat_id]num_iou_thresh=len(self.iou_thres)det_match=np.zeros((num_iou_thresh,num_det))gt_match=np.zeros((num_iou_thresh,num_gt))iflen(iou_mat)!=0:# loop through each iou threshforthres_idx,thres_valueinenumerate(self.iou_thres):# loop through each detection, for each detection, match only one gtfordet_idx,_inenumerate(det):m=-1iou_threshold=min([thres_value,1-1e-10])# loop through each gt, find the gt gives max iouforgt_idx,_inenumerate(gt):ifgt_match[thres_idx,gt_idx]>0:continueifiou_mat[det_idx,gt_idx]>=iou_threshold:iou_threshold=iou_mat[det_idx,gt_idx]m=gt_idxifm!=-1:det_match[thres_idx,det_idx]=gt[m]['idx']gt_match[thres_idx,m]=1return{'image_id':img_id,'category_id':cat_id,'gtIds':[g['idx']forgingt],'dtMatches':det_match,# shape (num_iou_thresh, num_det), value is zero or GT index'gtMatches':gt_match,# shape (num_iou_thresh, num_gt), value 1 or zero'dtScores':[d['score']fordindet],'num_gt':num_gt,}defaccumulate(self)->None:"""Generate precision-recall curve."""key_list=sorted(self.evalimgs)# key format (cat_id, img_id)eval_list=[self.evalimgs[key]forkeyinkey_list]self.image_ids=np.unique(self.image_ids)num_iou_thresh=len(self.iou_thres)num_recall_thresh=len(self.recall_thres)num_categories=len(self.categories)cat_list_zeroidx=[nforn,catinenumerate(self.categories)]num_imgs=len(self.image_ids)maxdets=self.max_detection# initialize these at -1precision_matrix=-np.ones((num_iou_thresh,num_recall_thresh,num_categories))recall_matrix=-np.ones((num_iou_thresh,num_categories))scores_matrix=-np.ones((num_iou_thresh,num_recall_thresh,num_categories))# loop through categoryforcat_indexincat_list_zeroidx:Nk=cat_index*num_imgs# each element is one image inside this categoryeval_by_category=[eval_list[Nk+img_idx]forimg_idxinrange(num_imgs)]# drop Noneeval_by_category=[eforeineval_by_categoryifnoteisNone]# no image inside this categoryiflen(eval_by_category)==0:continuedet_scores=np.concatenate([e['dtScores'][0:maxdets]foreineval_by_category])# sort from high score to low score, is this necessary?sorted_score_inds=np.argsort(-det_scores,kind='mergesort')det_scores_sorted=det_scores[sorted_score_inds]det_match=np.concatenate([e['dtMatches'][:,0:maxdets]foreineval_by_category],axis=1)[:,sorted_score_inds]# shape (num_iou_thresh, num_det_all_images)# number of all image gts in one categorynum_all_gt=np.sum([e['num_gt']foreineval_by_category])# for all images no gt inside this categoryifnum_all_gt==0:continuetps=det_match>0fps=det_match==0tp_sum=np.cumsum(tps,axis=1).astype(dtype=np.float)fp_sum=np.cumsum(fps,axis=1).astype(dtype=np.float)forindex,(true_positives,false_positives)inenumerate(zip(tp_sum,fp_sum)):true_positives=np.array(true_positives)false_positives=np.array(false_positives)nd=len(true_positives)recall=true_positives/num_all_gtprecision=true_positives/(false_positives+true_positives+np.spacing(1))precision_at_recall=np.zeros((num_recall_thresh,))score=np.zeros((num_recall_thresh,))ifnd:recall_matrix[index,cat_index]=recall[-1]else:recall_matrix[index,cat_index]=0precision=precision.tolist()precision_at_recall=precision_at_recall.tolist()# smooth precision along the curve, remove zigzagforiinrange(nd-1,0,-1):ifprecision[i]>precision[i-1]:precision[i-1]=precision[i]inds=np.searchsorted(recall,self.recall_thres,side='left')try:forrecall_index,precision_indexinenumerate(inds):precision_at_recall[recall_index]=precision[precision_index]score[recall_index]=det_scores_sorted[precision_index]except:passprecision_matrix[index,:,cat_index]=np.array(precision_at_recall)scores_matrix[index,:,cat_index]=np.array(score)self.eval={'counts':[num_iou_thresh,num_recall_thresh,num_categories],'precision':precision_matrix,'recall':recall_matrix,'scores':scores_matrix,}defsummarize(self,iou:float=None)->float:"""Compute average precision given one intersection union threshold. Args: iou: Intersection over union threshold. If this value is `None`, then average all iou thresholds. The result is the mean average precision. Returns: Average precision. """precision_at_iou=self.eval['precision']# shape (num_iou_thresh, num_recall_thresh, num_categories)ifiouisnotNone:iou_thresh_index=np.where(iou==self.iou_thres)[0]precision_at_iou=precision_at_iou[iou_thresh_index]precision_at_iou=precision_at_iou[:,:,:]iflen(precision_at_iou[precision_at_iou>-1])==0:mean_ap=-1else:mean_ap=np.mean(precision_at_iou[precision_at_iou>-1])returnmean_apdefcompute_iou(self,det:np.ndarray,gt:np.ndarray)->np.ndarray:"""Compute intersection over union. We leverage `maskUtils.iou`. Args: det: Detection array. gt: Ground truth array. Returns: Intersection of union array. """num_dt=len(det)num_gt=len(gt)ifnum_gt==0andnum_dt==0:return[]boxes_a=np.zeros(shape=(0,4),dtype=float)boxes_b=np.zeros(shape=(0,4),dtype=float)inds=np.argsort([-d['score']fordindet],kind='mergesort')det=[det[i]foriininds]iflen(det)>self.max_detection:det=det[0:self.max_detection]boxes_a=[[dt_elem['x1'],dt_elem['y1'],dt_elem['w'],dt_elem['h']]fordt_elemindet]boxes_b=[[gt_elem['x1'],gt_elem['y1'],gt_elem['w'],gt_elem['h']]forgt_elemingt]iscrowd=[0]*num_gt# to leverage maskUtils.iouiou_dt_gt=maskUtils.iou(boxes_a,boxes_b,iscrowd)returniou_dt_gt
defaccumulate(self)->None:"""Generate precision-recall curve."""key_list=sorted(self.evalimgs)# key format (cat_id, img_id)eval_list=[self.evalimgs[key]forkeyinkey_list]self.image_ids=np.unique(self.image_ids)num_iou_thresh=len(self.iou_thres)num_recall_thresh=len(self.recall_thres)num_categories=len(self.categories)cat_list_zeroidx=[nforn,catinenumerate(self.categories)]num_imgs=len(self.image_ids)maxdets=self.max_detection# initialize these at -1precision_matrix=-np.ones((num_iou_thresh,num_recall_thresh,num_categories))recall_matrix=-np.ones((num_iou_thresh,num_categories))scores_matrix=-np.ones((num_iou_thresh,num_recall_thresh,num_categories))# loop through categoryforcat_indexincat_list_zeroidx:Nk=cat_index*num_imgs# each element is one image inside this categoryeval_by_category=[eval_list[Nk+img_idx]forimg_idxinrange(num_imgs)]# drop Noneeval_by_category=[eforeineval_by_categoryifnoteisNone]# no image inside this categoryiflen(eval_by_category)==0:continuedet_scores=np.concatenate([e['dtScores'][0:maxdets]foreineval_by_category])# sort from high score to low score, is this necessary?sorted_score_inds=np.argsort(-det_scores,kind='mergesort')det_scores_sorted=det_scores[sorted_score_inds]det_match=np.concatenate([e['dtMatches'][:,0:maxdets]foreineval_by_category],axis=1)[:,sorted_score_inds]# shape (num_iou_thresh, num_det_all_images)# number of all image gts in one categorynum_all_gt=np.sum([e['num_gt']foreineval_by_category])# for all images no gt inside this categoryifnum_all_gt==0:continuetps=det_match>0fps=det_match==0tp_sum=np.cumsum(tps,axis=1).astype(dtype=np.float)fp_sum=np.cumsum(fps,axis=1).astype(dtype=np.float)forindex,(true_positives,false_positives)inenumerate(zip(tp_sum,fp_sum)):true_positives=np.array(true_positives)false_positives=np.array(false_positives)nd=len(true_positives)recall=true_positives/num_all_gtprecision=true_positives/(false_positives+true_positives+np.spacing(1))precision_at_recall=np.zeros((num_recall_thresh,))score=np.zeros((num_recall_thresh,))ifnd:recall_matrix[index,cat_index]=recall[-1]else:recall_matrix[index,cat_index]=0precision=precision.tolist()precision_at_recall=precision_at_recall.tolist()# smooth precision along the curve, remove zigzagforiinrange(nd-1,0,-1):ifprecision[i]>precision[i-1]:precision[i-1]=precision[i]inds=np.searchsorted(recall,self.recall_thres,side='left')try:forrecall_index,precision_indexinenumerate(inds):precision_at_recall[recall_index]=precision[precision_index]score[recall_index]=det_scores_sorted[precision_index]except:passprecision_matrix[index,:,cat_index]=np.array(precision_at_recall)scores_matrix[index,:,cat_index]=np.array(score)self.eval={'counts':[num_iou_thresh,num_recall_thresh,num_categories],'precision':precision_matrix,'recall':recall_matrix,'scores':scores_matrix,}
defcompute_iou(self,det:np.ndarray,gt:np.ndarray)->np.ndarray:"""Compute intersection over union. We leverage `maskUtils.iou`. Args: det: Detection array. gt: Ground truth array. Returns: Intersection of union array. """num_dt=len(det)num_gt=len(gt)ifnum_gt==0andnum_dt==0:return[]boxes_a=np.zeros(shape=(0,4),dtype=float)boxes_b=np.zeros(shape=(0,4),dtype=float)inds=np.argsort([-d['score']fordindet],kind='mergesort')det=[det[i]foriininds]iflen(det)>self.max_detection:det=det[0:self.max_detection]boxes_a=[[dt_elem['x1'],dt_elem['y1'],dt_elem['w'],dt_elem['h']]fordt_elemindet]boxes_b=[[gt_elem['x1'],gt_elem['y1'],gt_elem['w'],gt_elem['h']]forgt_elemingt]iscrowd=[0]*num_gt# to leverage maskUtils.iouiou_dt_gt=maskUtils.iou(boxes_a,boxes_b,iscrowd)returniou_dt_gt
defevaluate_img(self,cat_id:int,img_id:int)->Dict:"""Find gt matches for det given one image and one category. Args: cat_id: img_id: Returns: """det=self.det[img_id,cat_id]gt=self.gt[img_id,cat_id]num_det=len(det)num_gt=len(gt)ifnum_gt==0andnum_det==0:returnNone# sort detections, is ths necessary?det_index=np.argsort([-d['score']fordindet],kind='mergesort')# cap to max_detectiondet=[det[i]foriindet_index[0:self.max_detection]]# get iou matrix for given (img_id, cat_id), the output has shape (num_det, num_gt)iou_mat=self.ious[img_id,cat_id]num_iou_thresh=len(self.iou_thres)det_match=np.zeros((num_iou_thresh,num_det))gt_match=np.zeros((num_iou_thresh,num_gt))iflen(iou_mat)!=0:# loop through each iou threshforthres_idx,thres_valueinenumerate(self.iou_thres):# loop through each detection, for each detection, match only one gtfordet_idx,_inenumerate(det):m=-1iou_threshold=min([thres_value,1-1e-10])# loop through each gt, find the gt gives max iouforgt_idx,_inenumerate(gt):ifgt_match[thres_idx,gt_idx]>0:continueifiou_mat[det_idx,gt_idx]>=iou_threshold:iou_threshold=iou_mat[det_idx,gt_idx]m=gt_idxifm!=-1:det_match[thres_idx,det_idx]=gt[m]['idx']gt_match[thres_idx,m]=1return{'image_id':img_id,'category_id':cat_id,'gtIds':[g['idx']forgingt],'dtMatches':det_match,# shape (num_iou_thresh, num_det), value is zero or GT index'gtMatches':gt_match,# shape (num_iou_thresh, num_gt), value 1 or zero'dtScores':[d['score']fordindet],'num_gt':num_gt,}
defon_batch_begin(self,data:Data):"""Reset instance variables."""self.gt=defaultdict(list)# gt for evaluationself.det=defaultdict(list)# det for evaluationself.batch_image_ids=[]# img_ids per batchself.ious=defaultdict(list)self.ids_unique=[]self.ids_batch_to_epoch={}
defon_epoch_begin(self,data:Data):"""Reset instance variables."""self.image_ids=[]# append all the image ids coming from each iterationself.evalimgs={}self.eval={}self.ids_in_epoch=0
defsummarize(self,iou:float=None)->float:"""Compute average precision given one intersection union threshold. Args: iou: Intersection over union threshold. If this value is `None`, then average all iou thresholds. The result is the mean average precision. Returns: Average precision. """precision_at_iou=self.eval['precision']# shape (num_iou_thresh, num_recall_thresh, num_categories)ifiouisnotNone:iou_thresh_index=np.where(iou==self.iou_thres)[0]precision_at_iou=precision_at_iou[iou_thresh_index]precision_at_iou=precision_at_iou[:,:,:]iflen(precision_at_iou[precision_at_iou>-1])==0:mean_ap=-1else:mean_ap=np.mean(precision_at_iou[precision_at_iou>-1])returnmean_ap