赞
踩
代码github地址:https://github.com/eriklindernoren/PyTorch-YOLOv3
- # reference: https://github.com/eriklindernoren/PyTorch-YOLOv3/blob/f917503ffe4a21d2b1148d8cb13b89b834517d76/utils/utils.py
-
- def ap_per_class(tp, conf, pred_cls, target_cls):
- """ 通过召回率与精确度曲线计算mAP
- Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
- # 参数说明
- tp: True positives (list).
- conf: 置信度[0,1] (list).
- pred_cls: 预测的目标类别 (list).
- target_cls: 真正的目标类别 (list).
- # 返回
- [precision,recall,average precision,f1, classes_num]
- """
-
- # 按照预测的置信度做降序排列, 得到排序的索引
- i = np.argsort(-conf)
- tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
-
- # 去除重复项
- unique_classes = np.unique(target_cls)
-
- # 为每个类别创建精度-召回曲线, 计算AP
- ap, p, r = [], [], []
- for c in tqdm.tqdm(unique_classes, desc="Computing AP"):
- # 找出等于c的位置
- i = pred_cls == c
- # 类别c的人工标注目标的数量
- n_gt = (target_cls == c).sum()
- # 类别c的预测目标数量
- n_p = i.sum()
-
- if n_p == 0 and n_gt == 0:
- continue
- elif n_p == 0 or n_gt == 0:
- ap.append(0)
- r.append(0)
- p.append(0)
- else:
- # 累加计算FPs与TPs
- fpc = (1 - tp[i]).cumsum() #
- tpc = (tp[i]).cumsum()
-
- # Recall
- recall_curve = tpc / (n_gt + 1e-16) # TP/(TP + FN) (TP+FN)为当前类人工标注目标数量
- r.append(recall_curve[-1]) # 这个类别的召回率
-
- # Precision
- precision_curve = tpc / (tpc + fpc) # TP/(TP + FP) (TP+FP)为预测框的数量
- p.append(precision_curve[-1]) # 这个类别的精确率
-
- # 从召回率-精确率曲线计算AP
- ap.append(compute_ap(recall_curve, precision_curve))
-
- # Compute F1 score (harmonic mean of precision and recall)
- p, r, ap = np.array(p), np.array(r), np.array(ap)
- f1 = 2 * p * r / (p + r + 1e-16)
-
- return p, r, ap, f1, unique_classes.astype("int32")

(1) 数据加载。通过统计非极大值抑制后得到的outputs与人工标注框的targets条目,得到TP(目标预测正确)。统计的数据为一个batch的,保存的数据为statistics_data.pt,输出数据的shape。
- # 非极大值抑制后的outputs,与targets
- batch_statistics = torch.load('statistics_data.pt',map_location='cpu')
- outputs = batch_statistics['outputs']
- targets = batch_statistics['targets']
- print('outputs_shape: ',[x.shape for x in outputs])
- print('targets_size: ', targets.shape,end='\n\n')
输出结果:
数据statistics_data.pt保存的是8幅图像预测框与人工标注框数据,其中outputs的数据由非极大值抑制获取。outputs数据格式[x1,y1,x2,y2,conf,class_score,class_idx],targets数据格式[batch_idx,class_idx,x1,y1,x2,y2]。数据中的坐标是相对于网络中的输入尺寸。
(2) 统计TP。当某幅图像的某个预测框与targets中的真实框的IoU大于某个阈值,则表示该预测框能够作为targets中真实框的预测值。
- # output为某幅图像的预测框,初始化tp
- true_positives = np.zeros(output.shape[0])
- # 找出targets中batch_idx=i的class_idx与标注框坐标
- annotations = targets[targets[:, 0] == i][:, 1:]
-
- if len(annotations):
- # 真实的目标类别代号,标注框
- target_labels = annotations[:, 0]
- target_boxes = annotations[:, 1:]
- print('target_boxes: ',target_boxes,',target_labels: ',target_labels)
- detected_boxes = []
- # 遍历预测框索引,预测框坐标,预测类别代号
- for pred_i,(pred_box, pred_cls) in enumerate(zip(output[:,:4],output[:,-1])):
- if len(detected_boxes) == len(target_boxes): break
- if pred_cls not in target_labels: continue
- # 预测框去拟合目标框(通过最大的IoU加阈值判断)
- iou, box_index = bbox_iou(pred_box.unsqueeze(0), target_boxes).max(0)
- print('pred_{}'.format(pred_i),iou, ',box_index: ',box_index)
- if iou >= iou_threshold and box_index not in detected_boxes:
- # 预测真实值则赋值相应位置为1
- true_positives[pred_i] = 1
- detected_boxes += [box_index]

输出结果:
[true_positives, pred_confs, pred_cls]:
多幅图像的统计:
- # batch统计
- batch_metrics = []
- for i,output in enumerate(outputs):
- true_positives = np.zeros(output.shape[0])
- annotations = targets[targets[:, 0] == i][:, 1:]
-
- if len(annotations):
- target_boxes = annotations[:, 1:]
- target_labels = annotations[:, 0]
- detected_boxes = []
-
- for pred_i,(pred_box, pred_cls) in enumerate(zip(output[:,:4],output[:,-1])):
- if len(detected_boxes) == len(target_boxes): break
- if pred_cls not in target_labels: continue
-
- iou, box_index = bbox_iou(pred_box.unsqueeze(0), target_boxes).max(0)
- if iou >= iou_threshold and box_index not in detected_boxes:
- true_positives[pred_i] = 1
- detected_boxes += [box_index]
- # 每幅图像的TP,预测的目标置信度,预测类别代号
- batch_metrics.append([true_positives,output[:,4],output[:,-1]])

(1) 获取batch统计的结果与排序
batch_metrics的数据结构(list):
[[true_positives1, pred_confs1, pred_cls1],
[true_positives2, pred_confs2, pred_cls2],
[true_positives3, pred_confs3, pred_cls3],
......]
通过解包重组: list(zip(*batch_metrics)),然后得到如下:
[(true_positives1, true_positives2, ......),
(pred_confs1, pred_confs2, ......),
(pred_cls1, pred_cls2, ......) ]
- # 解包一个batch的数据
- true_positives, pred_confs, pred_cls = [np.concatenate(x, 0) for x in list(zip(*batch_metrics))]
按照置信度降序排列统计的数据
- # 按照置信度降序排序,排序tp,conf,cls
- idx = np.argsort(-pred_confs)
- tp, pre_confs, pred_cls = true_positives[idx],pred_confs[idx],pred_cls[idx]
(2) 评估一个batch的预测结果。通常情况是通过第2步的统计,统计出整个验证图片库的[true_positives, pred_confs, pred_cls]数据,然后再计算相应的评估值。
a. 召回率、精确度曲线
- # 人工标注的类别
- unique_cls = np.unique(target_cls)
-
- # 每个类别创建精度-召回曲线, 计算AP
- ap, p, r = [], [], []
- for num, c in enumerate(unique_cls):
- idx = pred_cls==c
- # 类别c的人工标注目标的数量
- n_gt = (target_cls == c).sum().numpy()
- n_p = idx.sum()
- print('n_gt: ',n_gt,',n_p: ',n_p)
-
- if n_p == 0 and n_gt == 0:
- continue
- elif n_p == 0 or n_gt == 0:
- ap.append(0)
- r.append(0)
- p.append(0)
- else:
- # 累加计算FPs与TPs
- fpc = (1 - tp[idx]).cumsum()
- tpc = tp[idx].cumsum()
-
- # 召回率
- recall_curve = tpc/(n_gt + 1e-16) # TP/(TP+FN)
- r.append(recall_curve[-1])
-
- # 精确率
- precision_curve = tpc/(tpc+fpc) # TP/(TP+FP)
- p.append(precision_curve[-1])
- print('recall_curve: ',recall_curve,'\nprecision_curve: ',precision_curve)

一个类别召回率、精确度曲线输出:
b. AP计算
AP是召回率-精确度曲线的面积,采用的方法为积分求面积。通过plot绘制的曲线如下图所示:
积分的方法需要找出图中recall存在梯度的位置,即上图红色线与红色线之间在recall方向存在梯度(也就是相邻的两个点的recall值不同)。把整个面积区域划分成坐标点-1个条形图(图中8个点,则有7个条形图),然后求和所有条形图的面积得到AP的面积。实现代码如下:
- # 计算AP,检测评估函数compute_ap
- mrec = np.concatenate(([0.0], recall_curve, [1.0]))
- mpre = np.concatenate(([1.0], precision_curve, [0.0]))
-
- for i in range(mpre.size - 1, 0, -1):
- mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
-
- # 找出召回率有梯度的位置
- idx = np.where(mrec[1:] != mrec[:-1])[0]
- # 计算面积 sum(delta_recall*precision)
- ap_c = np.sum((mrec[idx+1] - mrec[idx])*mpre[idx + 1])
c. 计算F1
f1 = 2 * p * r / (p + r + 1e-16)
d. 总体代码
- # 评估一个batch的预测结果
- true_positives, pred_confs, pred_cls = [np.concatenate(x, 0) for x in list(zip(*batch_metrics))]
- target_cls = targets[:,1]
-
- # 按照置信度降序排序,排序tp,conf,cls
- idx = np.argsort(-pred_confs)
- tp, pre_confs, pred_cls = true_positives[idx],pred_confs[idx],pred_cls[idx]
-
- # 得到人工标注的类别
- unique_cls = np.unique(target_cls)
-
- # 为每个类别创建精度-召回曲线, 计算AP
- ap, p, r = [], [], []
- for num, c in enumerate(unique_cls):
- idx = pred_cls==c
- # 类别c的人工标注目标的数量
- n_gt = (target_cls == c).sum().numpy()
- n_p = idx.sum()
-
- if n_p == 0 and n_gt == 0:
- continue
- elif n_p == 0 or n_gt == 0:
- ap.append(0)
- r.append(0)
- p.append(0)
- else:
- # 累加计算FPs与TPs
- fpc = (1 - tp[idx]).cumsum()
- tpc = tp[idx].cumsum()
-
- # 召回率
- recall_curve = tpc/(n_gt + 1e-16) # TP/(TP+FN)
- r.append(recall_curve[-1])
-
- # 精确率
- precision_curve = tpc/(tpc+fpc) # TP/(TP+FP)
- p.append(precision_curve[-1])
-
- # 计算AP
- mrec = np.concatenate(([0.0], recall_curve, [1.0]))
- mpre = np.concatenate(([1.0], precision_curve, [0.0]))
-
- for i in range(mpre.size - 1, 0, -1):
- mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
-
- # 找出召回率有梯度的位置
- idx = np.where(mrec[1:] != mrec[:-1])[0]
-
- # 计算面积
- ap_c = np.sum((mrec[idx+1] - mrec[idx])*mpre[idx + 1])
- ap.append(ap_c)
-
- # batch中所有类别
- p, r, ap = np.array(p), np.array(r), np.array(ap)
- f1 = 2 * p * r / (p + r + 1e-16)
-
- #[类别,召回率,精确率,F1, AP]
- str_list = ['%d \t %.3f \t %.3f \t %.3f \t %.3f'%(x[0],x[1],x[2],x[3],x[4]) for x in np.array([unique_cls,r,p,f1,ap]).T]
- print('cls_num | recall | precision | F1 | AP')
- for x in str_list:
- print(x)
-
- print('mAP:', np.mean(ap))

输出结果:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。