Wanli commited on
Commit
60ba673
·
1 Parent(s): 28adb60

Add script to evaluate face detection by WIDERFace (#70)

Browse files

* Add script to evaluate face detection by WIDERFace

* add the result of YuNet

models/face_detection_yunet/README.md CHANGED
@@ -7,6 +7,15 @@ Notes:
7
  - For details on training this model, please visit https://github.com/ShiqiYu/libfacedetection.train.
8
  - This ONNX model has fixed input shape, but OpenCV DNN infers on the exact shape of input image. See https://github.com/opencv/opencv_zoo/issues/44 for more information.
9
 
 
 
 
 
 
 
 
 
 
10
  ## Demo
11
 
12
  Run the following command to try the demo:
 
7
  - For details on training this model, please visit https://github.com/ShiqiYu/libfacedetection.train.
8
  - This ONNX model has fixed input shape, but OpenCV DNN infers on the exact shape of input image. See https://github.com/opencv/opencv_zoo/issues/44 for more information.
9
 
10
+ Results of accuracy evaluation with [tools/eval](../../tools/eval).
11
+
12
+ | Models | Easy AP | Medium AP | Hard AP |
13
+ |-------------|---------|-----------|---------|
14
+ | YuNet | 0.8498 | 0.8384 | 0.7357 |
15
+ | YuNet quant | 0.7751 | 0.8145 | 0.7312 |
16
+
17
+ \*: 'quant' stands for 'quantized'.
18
+
19
  ## Demo
20
 
21
  Run the following command to try the demo:
tools/eval/README.md CHANGED
@@ -4,6 +4,7 @@ Make sure you have the following packages installed:
4
 
5
  ```shell
6
  pip install tqdm
 
7
  ```
8
 
9
  Generally speaking, evaluation can be done with the following command:
@@ -13,7 +14,8 @@ python eval.py -m model_name -d dataset_name -dr dataset_root_dir
13
  ```
14
 
15
  Supported datasets:
16
- - [ImageNet](./datasets/imagenet.py)
 
17
 
18
  ## ImageNet
19
 
@@ -53,3 +55,42 @@ Run evaluation with the following command:
53
  python eval.py -m mobilenet -d imagenet -dr /path/to/imagenet
54
  ```
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  ```shell
6
  pip install tqdm
7
+ pip install scipy
8
  ```
9
 
10
  Generally speaking, evaluation can be done with the following command:
 
14
  ```
15
 
16
  Supported datasets:
17
+ - [ImageNet](#imagenet)
18
+ - [WIDERFace](#widerface)
19
 
20
  ## ImageNet
21
 
 
55
  python eval.py -m mobilenet -d imagenet -dr /path/to/imagenet
56
  ```
57
 
58
+ ## WIDERFace
59
+
60
+ The script is modified based on [WiderFace-Evaluation](https://github.com/wondervictor/WiderFace-Evaluation).
61
+
62
+ ### Prepare data
63
+
64
+ Please visit http://shuoyang1213.me/WIDERFACE to download the WIDERFace dataset [Validation Images](https://huggingface.co/datasets/wider_face/resolve/main/data/WIDER_val.zip), [Face annotations](http://shuoyang1213.me/WIDERFACE/support/bbx_annotation/wider_face_split.zip) and [eval_tools](http://shuoyang1213.me/WIDERFACE/support/eval_script/eval_tools.zip). Organize files as follow:
65
+
66
+ ```shell
67
+ $ tree -L 2 /path/to/widerface
68
+ .
69
+ ├── eval_tools
70
+ │   ├── boxoverlap.m
71
+ │   ├── evaluation.m
72
+ │   ├── ground_truth
73
+ │   ├── nms.m
74
+ │   ├── norm_score.m
75
+ │   ├── plot
76
+ │   ├── read_pred.m
77
+ │   └── wider_eval.m
78
+ ├── wider_face_split
79
+ │   ├── readme.txt
80
+ │   ├── wider_face_test_filelist.txt
81
+ │   ├── wider_face_test.mat
82
+ │   ├── wider_face_train_bbx_gt.txt
83
+ │   ├── wider_face_train.mat
84
+ │   ├── wider_face_val_bbx_gt.txt
85
+ │   └── wider_face_val.mat
86
+ └── WIDER_val
87
+ └── images
88
+ ```
89
+
90
+ ### Evaluation
91
+
92
+ Run evaluation with the following command:
93
+
94
+ ```shell
95
+ python eval.py -m yunet -d widerface -dr /path/to/widerface
96
+ ```
tools/eval/datasets/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
  from .imagenet import ImageNet
 
2
 
3
  class Registery:
4
  def __init__(self, name):
@@ -13,3 +14,4 @@ class Registery:
13
 
14
  DATASETS = Registery("Datasets")
15
  DATASETS.register(ImageNet)
 
 
1
  from .imagenet import ImageNet
2
+ from .widerface import WIDERFace
3
 
4
  class Registery:
5
  def __init__(self, name):
 
14
 
15
  DATASETS = Registery("Datasets")
16
  DATASETS.register(ImageNet)
17
+ DATASETS.register(WIDERFace)
tools/eval/datasets/widerface.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tqdm
3
+ import pickle
4
+ import numpy as np
5
+ from scipy.io import loadmat
6
+ import cv2 as cv
7
+
8
+
9
+ def get_gt_boxes(gt_dir):
10
+ """ gt dir: (wider_face_val.mat, wider_easy_val.mat, wider_medium_val.mat, wider_hard_val.mat)"""
11
+
12
+ gt_mat = loadmat(os.path.join(gt_dir, 'wider_face_val.mat'))
13
+ hard_mat = loadmat(os.path.join(gt_dir, 'wider_hard_val.mat'))
14
+ medium_mat = loadmat(os.path.join(gt_dir, 'wider_medium_val.mat'))
15
+ easy_mat = loadmat(os.path.join(gt_dir, 'wider_easy_val.mat'))
16
+
17
+ facebox_list = gt_mat['face_bbx_list']
18
+ event_list = gt_mat['event_list']
19
+ file_list = gt_mat['file_list']
20
+
21
+ hard_gt_list = hard_mat['gt_list']
22
+ medium_gt_list = medium_mat['gt_list']
23
+ easy_gt_list = easy_mat['gt_list']
24
+
25
+ return facebox_list, event_list, file_list, hard_gt_list, medium_gt_list, easy_gt_list
26
+
27
+
28
+ def get_gt_boxes_from_txt(gt_path, cache_dir):
29
+ cache_file = os.path.join(cache_dir, 'gt_cache.pkl')
30
+ if os.path.exists(cache_file):
31
+ f = open(cache_file, 'rb')
32
+ boxes = pickle.load(f)
33
+ f.close()
34
+ return boxes
35
+
36
+ f = open(gt_path, 'r')
37
+ state = 0
38
+ lines = f.readlines()
39
+ lines = list(map(lambda x: x.rstrip('\r\n'), lines))
40
+ boxes = {}
41
+ print(len(lines))
42
+ f.close()
43
+ current_boxes = []
44
+ current_name = None
45
+ for line in lines:
46
+ if state == 0 and '--' in line:
47
+ state = 1
48
+ current_name = line
49
+ continue
50
+ if state == 1:
51
+ state = 2
52
+ continue
53
+
54
+ if state == 2 and '--' in line:
55
+ state = 1
56
+ boxes[current_name] = np.array(current_boxes).astype('float32')
57
+ current_name = line
58
+ current_boxes = []
59
+ continue
60
+
61
+ if state == 2:
62
+ box = [float(x) for x in line.split(' ')[:4]]
63
+ current_boxes.append(box)
64
+ continue
65
+
66
+ f = open(cache_file, 'wb')
67
+ pickle.dump(boxes, f)
68
+ f.close()
69
+ return boxes
70
+
71
+
72
+ def norm_score(pred):
73
+ """ norm score
74
+ pred {key: [[x1,y1,x2,y2,s]]}
75
+ """
76
+
77
+ max_score = 0
78
+ min_score = 1
79
+
80
+ for _, k in pred.items():
81
+ for _, v in k.items():
82
+ if len(v) == 0:
83
+ continue
84
+ _min = np.min(v[:, -1])
85
+ _max = np.max(v[:, -1])
86
+ max_score = max(_max, max_score)
87
+ min_score = min(_min, min_score)
88
+
89
+ diff = max_score - min_score
90
+ for _, k in pred.items():
91
+ for _, v in k.items():
92
+ if len(v) == 0:
93
+ continue
94
+ v[:, -1] = (v[:, -1] - min_score) / diff
95
+
96
+
97
+ def bbox_overlaps(a, b):
98
+ """
99
+ return iou of a and b, numpy version for data augenmentation
100
+ """
101
+ lt = np.maximum(a[:, np.newaxis, 0:2], b[:, 0:2])
102
+ rb = np.minimum(a[:, np.newaxis, 2:4], b[:, 2:4])
103
+
104
+ area_i = np.prod(rb - lt + 1, axis=2) * (lt < rb).all(axis=2)
105
+ area_a = np.prod(a[:, 2:4] - a[:, 0:2] + 1, axis=1)
106
+ area_b = np.prod(b[:, 2:4] - b[:, 0:2] + 1, axis=1)
107
+ return area_i / (area_a[:, np.newaxis] + area_b - area_i)
108
+
109
+
110
+ def image_eval(pred, gt, ignore, iou_thresh):
111
+ """ single image evaluation
112
+ pred: Nx5
113
+ gt: Nx4
114
+ ignore:
115
+ """
116
+
117
+ _pred = pred.copy()
118
+ _gt = gt.copy()
119
+ pred_recall = np.zeros(_pred.shape[0])
120
+ recall_list = np.zeros(_gt.shape[0])
121
+ proposal_list = np.ones(_pred.shape[0])
122
+
123
+ _pred[:, 2] = _pred[:, 2] + _pred[:, 0]
124
+ _pred[:, 3] = _pred[:, 3] + _pred[:, 1]
125
+ _gt[:, 2] = _gt[:, 2] + _gt[:, 0]
126
+ _gt[:, 3] = _gt[:, 3] + _gt[:, 1]
127
+
128
+ overlaps = bbox_overlaps(_pred[:, :4], _gt)
129
+
130
+ for h in range(_pred.shape[0]):
131
+
132
+ gt_overlap = overlaps[h]
133
+ max_overlap, max_idx = gt_overlap.max(), gt_overlap.argmax()
134
+ if max_overlap >= iou_thresh:
135
+ if ignore[max_idx] == 0:
136
+ recall_list[max_idx] = -1
137
+ proposal_list[h] = -1
138
+ elif recall_list[max_idx] == 0:
139
+ recall_list[max_idx] = 1
140
+
141
+ r_keep_index = np.where(recall_list == 1)[0]
142
+ pred_recall[h] = len(r_keep_index)
143
+ return pred_recall, proposal_list
144
+
145
+
146
+ def img_pr_info(thresh_num, pred_info, proposal_list, pred_recall):
147
+ pr_info = np.zeros((thresh_num, 2)).astype('float')
148
+ for t in range(thresh_num):
149
+
150
+ thresh = 1 - (t + 1) / thresh_num
151
+ r_index = np.where(pred_info[:, 4] >= thresh)[0]
152
+ if len(r_index) == 0:
153
+ pr_info[t, 0] = 0
154
+ pr_info[t, 1] = 0
155
+ else:
156
+ r_index = r_index[-1]
157
+ p_index = np.where(proposal_list[:r_index + 1] == 1)[0]
158
+ pr_info[t, 0] = len(p_index)
159
+ pr_info[t, 1] = pred_recall[r_index]
160
+ return pr_info
161
+
162
+
163
+ def dataset_pr_info(thresh_num, pr_curve, count_face):
164
+ _pr_curve = np.zeros((thresh_num, 2))
165
+ for i in range(thresh_num):
166
+ _pr_curve[i, 0] = pr_curve[i, 1] / pr_curve[i, 0]
167
+ _pr_curve[i, 1] = pr_curve[i, 1] / count_face
168
+ return _pr_curve
169
+
170
+
171
+ def voc_ap(rec, prec):
172
+ # correct AP calculation
173
+ # first append sentinel values at the end
174
+ mrec = np.concatenate(([0.], rec, [1.]))
175
+ mpre = np.concatenate(([0.], prec, [0.]))
176
+
177
+ # compute the precision envelope
178
+ for i in range(mpre.size - 1, 0, -1):
179
+ mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
180
+
181
+ # to calculate area under PR curve, look for points
182
+ # where X axis (recall) changes value
183
+ i = np.where(mrec[1:] != mrec[:-1])[0]
184
+
185
+ # and sum (\Delta recall) * prec
186
+ ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
187
+ return ap
188
+
189
+
190
+ def evaluation(pred, gt_path, iou_thresh=0.5):
191
+ norm_score(pred)
192
+ facebox_list, event_list, file_list, hard_gt_list, medium_gt_list, easy_gt_list = get_gt_boxes(gt_path)
193
+ event_num = len(event_list)
194
+ thresh_num = 1000
195
+ settings = ['easy', 'medium', 'hard']
196
+ setting_gts = [easy_gt_list, medium_gt_list, hard_gt_list]
197
+ aps = []
198
+ for setting_id in range(3):
199
+ # different setting
200
+ gt_list = setting_gts[setting_id]
201
+ count_face = 0
202
+ pr_curve = np.zeros((thresh_num, 2)).astype('float')
203
+ # [hard, medium, easy]
204
+ pbar = tqdm.tqdm(range(event_num))
205
+ for i in pbar:
206
+ pbar.set_description('Processing {}'.format(settings[setting_id]))
207
+ event_name = str(event_list[i][0][0])
208
+ img_list = file_list[i][0]
209
+ pred_list = pred[event_name]
210
+ sub_gt_list = gt_list[i][0]
211
+ # img_pr_info_list = np.zeros((len(img_list), thresh_num, 2))
212
+ gt_bbx_list = facebox_list[i][0]
213
+
214
+ for j in range(len(img_list)):
215
+ pred_info = pred_list[str(img_list[j][0][0])]
216
+
217
+ gt_boxes = gt_bbx_list[j][0].astype('float')
218
+ keep_index = sub_gt_list[j][0]
219
+ count_face += len(keep_index)
220
+
221
+ if len(gt_boxes) == 0 or len(pred_info) == 0:
222
+ continue
223
+ ignore = np.zeros(gt_boxes.shape[0])
224
+ if len(keep_index) != 0:
225
+ ignore[keep_index - 1] = 1
226
+ pred_recall, proposal_list = image_eval(pred_info, gt_boxes, ignore, iou_thresh)
227
+
228
+ _img_pr_info = img_pr_info(thresh_num, pred_info, proposal_list, pred_recall)
229
+
230
+ pr_curve += _img_pr_info
231
+ pr_curve = dataset_pr_info(thresh_num, pr_curve, count_face)
232
+
233
+ propose = pr_curve[:, 0]
234
+ recall = pr_curve[:, 1]
235
+
236
+ ap = voc_ap(recall, propose)
237
+ aps.append(ap)
238
+ return aps
239
+
240
+
241
+ class WIDERFace:
242
+ def __init__(self, root, split='val'):
243
+ self.aps = []
244
+ self.widerface_root = root
245
+ self._split = split
246
+
247
+ self.widerface_img_paths = {
248
+ 'val': os.path.join(self.widerface_root, 'WIDER_val', 'images'),
249
+ 'test': os.path.join(self.widerface_root, 'WIDER_test', 'images')
250
+ }
251
+
252
+ self.widerface_split_fpaths = {
253
+ 'val': os.path.join(self.widerface_root, 'wider_face_split', 'wider_face_val.mat'),
254
+ 'test': os.path.join(self.widerface_root, 'wider_face_split', 'wider_face_test.mat')
255
+ }
256
+ self.img_list, self.num_img = self.load_list()
257
+
258
+ @property
259
+ def name(self):
260
+ return self.__class__.__name__
261
+
262
+ def load_list(self):
263
+ n_imgs = 0
264
+ flist = []
265
+
266
+ split_fpath = self.widerface_split_fpaths[self._split]
267
+ img_path = self.widerface_img_paths[self._split]
268
+
269
+ anno_data = loadmat(split_fpath)
270
+ event_list = anno_data.get('event_list')
271
+ file_list = anno_data.get('file_list')
272
+
273
+ for event_idx, event in enumerate(event_list):
274
+ event_name = event[0][0]
275
+ for f_idx, f in enumerate(file_list[event_idx][0]):
276
+ f_name = f[0][0]
277
+ f_path = os.path.join(img_path, event_name, f_name + '.jpg')
278
+ flist.append(f_path)
279
+ n_imgs += 1
280
+
281
+ return flist, n_imgs
282
+
283
+ def __getitem__(self, index):
284
+ img = cv.imread(self.img_list[index])
285
+ event, name = self.img_list[index].split(os.sep)[-2:]
286
+ return event, name, img
287
+
288
+ def eval(self, model):
289
+ results_list = dict()
290
+ pbar = tqdm.tqdm(self)
291
+ pbar.set_description_str("Evaluating {} with {} val set".format(model.name, self.name))
292
+ # forward
293
+ for event_name, img_name, img in pbar:
294
+ img_shape = [img.shape[1], img.shape[0]]
295
+ model.setInputSize(img_shape)
296
+ det = model.infer(img)
297
+
298
+ if not results_list.get(event_name):
299
+ results_list[event_name] = dict()
300
+
301
+ if det is None:
302
+ det = np.array([[10, 10, 20, 20, 0.002]])
303
+ else:
304
+ det = np.append(np.around(det[:, :4], 1), np.around(det[:, -1], 3).reshape(-1, 1), axis=1)
305
+
306
+ results_list[event_name][img_name.rstrip('.jpg')] = det
307
+
308
+ self.aps = evaluation(results_list, os.path.join(self.widerface_root, 'eval_tools', 'ground_truth'))
309
+
310
+ def print_result(self):
311
+ print("==================== Results ====================")
312
+ print("Easy Val AP: {}".format(self.aps[0]))
313
+ print("Medium Val AP: {}".format(self.aps[1]))
314
+ print("Hard Val AP: {}".format(self.aps[2]))
315
+ print("=================================================")
tools/eval/eval.py CHANGED
@@ -51,6 +51,20 @@ models = dict(
51
  topic="image_classification",
52
  modelPath=os.path.join(root_dir, "models/image_classification_ppresnet/image_classification_ppresnet50_2022jan-act_int8-wt_int8-quantized.onnx"),
53
  topK=5),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  )
55
 
56
  datasets = dict(
@@ -58,6 +72,9 @@ datasets = dict(
58
  name="ImageNet",
59
  topic="image_classification",
60
  size=224),
 
 
 
61
  )
62
 
63
  def main(args):
 
51
  topic="image_classification",
52
  modelPath=os.path.join(root_dir, "models/image_classification_ppresnet/image_classification_ppresnet50_2022jan-act_int8-wt_int8-quantized.onnx"),
53
  topK=5),
54
+ yunet=dict(
55
+ name="YuNet",
56
+ topic="face_detection",
57
+ modelPath=os.path.join(root_dir, "models/face_detection_yunet/face_detection_yunet_2022mar.onnx"),
58
+ topK=5000,
59
+ confThreshold=0.3,
60
+ nmsThreshold=0.45),
61
+ yunet_q=dict(
62
+ name="YuNet",
63
+ topic="face_detection",
64
+ modelPath=os.path.join(root_dir, "models/face_detection_yunet/face_detection_yunet_2022mar-act_int8-wt_int8-quantized.onnx"),
65
+ topK=5000,
66
+ confThreshold=0.3,
67
+ nmsThreshold=0.45)
68
  )
69
 
70
  datasets = dict(
 
72
  name="ImageNet",
73
  topic="image_classification",
74
  size=224),
75
+ widerface=dict(
76
+ name="WIDERFace",
77
+ topic="face_detection")
78
  )
79
 
80
  def main(args):