一、简介
这部分主要围绕如何评估一个人脸检测器性能
展开记录。
代码地址:Retinaface代码地址
主要记录脚本:evaluation.py
,位于./widerface_evaluate/
下。
二、主要内容
一般来评价人脸检测器性能,常用的评价指标就是PR-精确度precision、召回率Recall
和AP
,此次围绕widerface测试集
和人脸检测器Retinaface
进行记录。
1、
在进行AP计算之前,需要按照./widerface_evaluate/
的Readme进行一些操作。首先需要利用test_widerface.py
脚本生成我们的预测集,如下图所示,包含61个文件夹,每个文件夹里有以图片命名的txt,txt里面的内容是首行为图片name,第二行是预测人脸框的个数,下面便是人脸框坐标和置信度(x,y,w,h,score)。然后运行python3 setup.py build_ext --inplace
,这个好像是为了把evaluation.py
里的一个求iou的函数转化为了用C实现,提高速度。然后运行evaluation.py
脚本就计算出了AP了,包括有Easy、Medium、Hard三个的。当然,也可以绘制出三种的PR图,如下图。下面详细来讲解下evaluation.py
脚本和如何绘制PR图。
2、
下面我先插入下evaluation.py
的代码,方便讲解和记录。
展开线路是围绕evaluation()进行。
首先,pred = get_preds(pred) ,这是把之前通过test_widerface.py
得到的信息进行获取,存储到pred中,其具体格式如下面附录
中的Ex1,获取方式主要利用了get_preds()、read_pred_file()等函数
,主要是对一些文件txt的操作。
norm_score(pred)函数
,利用norm_score()函数对pred中各个文件夹下图片的各个boxes的score进行归一化。
get_gt_boxes(gt_path),是获取官方所给的每个图片的boxes,便于我们进行对比,评估,下面就每个获取到的内容是什么,代表什么含义,进行详细解释,因为网上可搜到的这类资料很少,对于一些刚入门的可能不太友好。具体见附录
中的Ex2。
然后 for setting_id in range(3):是针对easy,medium,hard三个测试集分别进行测试,for j in range(len(img_list))针对每个文件夹下每张图片进行计算PR和AP。
首先是,image_eval()
,在了解这个函数之前,我们说下Precision和Recall的含义,Precision即预测正确的人脸个数占预测为人脸个数的比例,通俗点就是你找的人脸里有几个对的;Recall预测为正确的人脸个数占真实为人脸个数的比例,通俗点就是在对的人脸里,你找回了几个。更多介绍可自行谷歌。然后我们看此函数,在开头创建了三个array:pred_recall(全0)、recall_list(全0)、proposal_list(全1),shape分别为预测人脸框个数、真实人脸框个数、预测人脸框个数,然后通过这个iou进行筛选,大于设定iou_thresh的有以下两种情况:第一种,就是通过筛选但预测不是标注的‘合格人脸’,这时,proposal_list被标注为-1,recall_list被标注为-1,;第二种,就是通过筛选且预测为‘合格人脸’的,recall_list被标注为1。然后将recall_list为1的个数记录与pred_recall中,并返回pred_recall, proposal_list。
img_pr_info()函数
,将上述得到的pred_recall, proposal_list,用于Precision和Recall的计算。对置信度进行了1000分划分,分别统各个划分点的P和R,pr_info[:,0]存放的是预测的人脸的总个数,是用的proposal_list中为1的进行统计计算的,那么在image_eval()函数
中,置proposal_list为-1的意思就是从预测总人脸剔除了,既不算预测对,也不算预测错,相对于减小了计算precision的分母,即(TP+FP)预测人脸总数。而pr_info[t, 1]便是用recall_list中为1进行统计计算的,最后在dataset_pr_info()函数
中,Precision通过pr_curve[i, 1] / pr_curve[i, 0]计算得到,即TP/(TP+FP);Recall通过pr_curve[i, 1] / count_face计算得到,即TP/真实人脸总数,即TP/(TP+FN)。有了这俩参数,把我代码里注释的关于plt
(见下面代码)的代码取消注释,便可绘制P-R图,而AP的值,即P-R曲线下的面积。具体计算在voc_ap()函数中。
voc_ap()函数
,类似于计算曲线积分线下面积的原始方法,即小矩形方法,底×高。底是Recall,高是Precision,然后进行简单的排序和去重操作,先把一些底即Recall值相同的找出来,避免两者相减为0,减少计算量,然后np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) 即底相减,然后乘以较大的底对应的高Precision,得到最终的AP。这部分更多的参考可见目标检测算法的评估指标:mAP定义及计算方式和目标检测番外篇(2)_mAP。
import os
...
from bbox import bbox_overlaps
...
def get_gt_boxes(gt_dir):
""" gt dir: (wider_face_val.mat, wider_easy_val.mat, wider_medium_val.mat, wider_hard_val.mat)"""
gt_mat = loadmat(os.path.join(gt_dir, 'wider_face_val.mat'))
hard_mat = loadmat(os.path.join(gt_dir, 'wider_hard_val.mat'))
medium_mat = loadmat(os.path.join(gt_dir, 'wider_medium_val.mat'))
easy_mat = loadmat(os.path.join(gt_dir, 'wider_easy_val.mat'))
facebox_list = gt_mat['face_bbx_list']
event_list = gt_mat['event_list'] #文件夹
file_list = gt_mat['file_list'] #图片名称
hard_gt_list = hard_mat['gt_list']
medium_gt_list = medium_mat['gt_list']
easy_gt_list = easy_mat['gt_list']
return facebox_list, event_list, file_list, hard_gt_list, medium_gt_list, easy_gt_list
def get_gt_boxes_from_txt(gt_path, cache_dir):...
def read_pred_file(filepath):...
def get_preds(pred_dir):...
def norm_score(pred):...
def image_eval(pred, gt, ignore, iou_thresh):
""" single image evaluation
pred: Nx5
gt: Nx4
ignore:
"""
# pred_info, gt_boxes, ignore, iou_thresh
_pred = pred.copy()
_gt = gt.copy()
pred_recall = np.zeros(_pred.shape[0])
recall_list = np.zeros(_gt.shape[0])
proposal_list = np.ones(_pred.shape[0])
_pred[:, 2] = _pred[:, 2] + _pred[:, 0]
_pred[:, 3] = _pred[:, 3] + _pred[:, 1]
_gt[:, 2] = _gt[:, 2] + _gt[:, 0]
_gt[:, 3] = _gt[:, 3] + _gt[:, 1]
overlaps = bbox_overlaps(_pred[:, :4], _gt)
for h in range(_pred.shape[0]):
gt_overlap = overlaps[h]
max_overlap, max_idx = gt_overlap.max(), gt_overlap.argmax()
if max_overlap >= iou_thresh:
if ignore[max_idx] == 0:
recall_list[max_idx] = -1
proposal_list[h] = -1
elif recall_list[max_idx] == 0:
recall_list[max_idx] = 1
r_keep_index = np.where(recall_list == 1)[0]
pred_recall[h] = len(r_keep_index)
return pred_recall, proposal_list
def img_pr_info(thresh_num, pred_info, proposal_list, pred_recall):
pr_info = np.zeros((thresh_num, 2)).astype('float')
for t in range(thresh_num):
thresh = 1 - (t+1)/thresh_num
r_index = np.where(pred_info[:, 4] >= thresh)[0]
if len(r_index) == 0:
pr_info[t, 0] = 0
pr_info[t, 1] = 0
else:
r_index = r_index[-1]
p_index = np.where(proposal_list[:r_index+1] == 1)[0]
pr_info[t, 0] = len(p_index) #TP+FP
pr_info[t, 1] = pred_recall[r_index] #TP
return pr_info
def dataset_pr_info(thresh_num, pr_curve, count_face):
_pr_curve = np.zeros((thresh_num, 2))
for i in range(thresh_num):
_pr_curve[i, 0] = pr_curve[i, 1] / pr_curve[i, 0] #TP/(TP+FP)
_pr_curve[i, 1] = pr_curve[i, 1] / count_face #TP/(TP+FN)
return _pr_curve
def voc_ap(rec, prec):
# correct AP calculation
# first append sentinel values at the end
mrec = np.concatenate(([0.], rec, [1.]))
mpre = np.concatenate(([0.], prec, [0.]))
# compute the precision envelope
for i in range(mpre.size - 1, 0, -1):
mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i]) ###
# to calculate area under PR curve, look for points
# where X axis (recall) changes value
i = np.where(mrec[1:] != mrec[:-1])[0]
# and sum (\Delta recall) * prec
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
return ap
def evaluation(pred, gt_path, iou_thresh=0.5):
pred = get_preds(pred)
norm_score(pred)
facebox_list, event_list, file_list, hard_gt_list, medium_gt_list, easy_gt_list = get_gt_boxes(gt_path)
event_num = len(event_list)
thresh_num = 1000
settings = ['easy', 'medium', 'hard']
setting_gts = [easy_gt_list, medium_gt_list, hard_gt_list]
aps = []
data_=list()
for setting_id in range(3):
# different setting
gt_list = setting_gts[setting_id]
count_face = 0
pr_curve = np.zeros((thresh_num, 2)).astype('float')
# [hard, medium, easy]
pbar = tqdm.tqdm(range(event_num)) # 61
for i in pbar:
pbar.set_description('Processing {}'.format(settings[setting_id]))
event_name = str(event_list[i][0][0]) # 每个文件夹
img_list = file_list[i][0]
pred_list = pred[event_name]
sub_gt_list = gt_list[i][0] # ???
# img_pr_info_list = np.zeros((len(img_list), thresh_num, 2))
gt_bbx_list = facebox_list[i][0]
for j in range(len(img_list)):
pred_info = pred_list[str(img_list[j][0][0])] #file_list
gt_boxes = gt_bbx_list[j][0].astype('float')
keep_index = sub_gt_list[j][0]
count_face += len(keep_index)
if len(gt_boxes) == 0 or len(pred_info) == 0:
continue
ignore = np.zeros(gt_boxes.shape[0])
if len(keep_index) != 0:
ignore[keep_index-1] = 1
pred_recall, proposal_list = image_eval(pred_info, gt_boxes, ignore, iou_thresh)
_img_pr_info = img_pr_info(thresh_num, pred_info, proposal_list, pred_recall)
pr_curve += _img_pr_info
pr_curve = dataset_pr_info(thresh_num, pr_curve, count_face)
propose = pr_curve[:, 0]
recall = pr_curve[:, 1]
data_.append(recall)
data_.append(propose)
ap = voc_ap(recall, propose)
aps.append(ap)
# import matplotlib.pyplot as plt
# # y = recall*count_face
# # x = y/propose-y
# plt.figure(figsize=(6,6))
# plt.title("Precision Recall Curve")
# plt.xlabel('Recall')
# plt.ylabel('Precision')
# plt.style.use('ggplot')
# #plt.plot_date(data_[0],data_[1],color='r')
# plt.xticks(rotation=45)
# plt.plot(data_[0],data_[1],color='r',label = 'Easy')
# plt.plot(data_[2],data_[3],color='g',label = 'Medium')
# plt.plot(data_[4],data_[5],color='b',label = 'Hard')
# plt.legend(loc=1)
# plt.grid()
# plt.show()
print("==================== Results ====================")
print("Easy Val AP: {}".format(aps[0]))
print("Medium Val AP: {}".format(aps[1]))
print("Hard Val AP: {}".format(aps[2]))
print("=================================================")
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--pred', default="./widerface_evaluate/widerface_txt/")
parser.add_argument('-g', '--gt', default='./widerface_evaluate/ground_truth/')
args = parser.parse_args()
evaluation(args.pred, args.gt)
附录:
Ex1:
Ex2:
facebox_list记录了每个文件夹下,每个图片所包含人脸的人脸框(x,y,w,h),是一个np.ndarray。
event_list记录了61个文件夹的名称,也是一个np.ndarray。
file_list记录了61个文件夹下每张图片的名称,如图。也是一个np.ndarray.
至于hard_gt_list, medium_gt_list, easy_gt_list三个,他们的作用就是标注出一张图片中真正称得上‘合格人脸’的人脸,根据easy,medium,hard意思可知,easy标注的是一些比较好识别的人脸,而hard除了标注出easy和medium的,还要标注出一些相对较难识别的,比如侧脸的,模糊的,遮挡的等等。具体是给出facebox_list中标注人脸框的下标,这样可以对应且方便找出。
上述是列举的easy的数据格式。
三、结束语
到这里差不多就结束了。上面的记录可能存在手写体、书写格式不合理翻上翻下的情况、解释一大段文字等等缺点,但仍希望每位读者可以静心阅读,从中获取你想要的东西。有哪里不太清楚的或者错误的,可以直接私信或者留言!