目标检测评估计算(utils.py)
代码github地址:https://github.com/eriklindernoren/PyTorch-YOLOv3
1. 检测的评估函数
# 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")
2. batch预测数据的统计
(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]])
3. Recall、Precision、F1、AP、mAP计算
(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))
输出结果: