cledouxluma commited on
Commit
c0f802b
·
verified ·
1 Parent(s): a8aabd2

Upload evaluation/widerface_eval.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. evaluation/widerface_eval.py +242 -0
evaluation/widerface_eval.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WiderFace Evaluation Protocol.
3
+
4
+ Implements the official WiderFace evaluation methodology:
5
+ 1. Run detection on all validation images
6
+ 2. Save predictions in WiderFace submission format
7
+ 3. Compute AP on Easy/Medium/Hard subsets
8
+ 4. Generate precision-recall curves
9
+
10
+ WiderFace difficulty levels:
11
+ - Easy: Large, unoccluded, frontal faces
12
+ - Medium: Medium-sized, partially occluded or non-frontal
13
+ - Hard: Tiny (<16px), heavily occluded, extreme blur/pose
14
+
15
+ The official evaluation uses:
16
+ - IoU threshold = 0.5
17
+ - Prediction format: one file per event, sorted by confidence
18
+ - AP computed via interpolated precision-recall curve
19
+ """
20
+
21
+ import os
22
+ import json
23
+ import numpy as np
24
+ from typing import Dict, List, Optional, Tuple
25
+ from pathlib import Path
26
+
27
+ from .metrics import compute_iou_matrix, compute_ap
28
+
29
+
30
+ class WiderFaceEvaluator:
31
+ """
32
+ WiderFace evaluation with Easy/Medium/Hard AP computation.
33
+
34
+ Usage:
35
+ evaluator = WiderFaceEvaluator(gt_dir='wider_face/wider_face_split')
36
+ evaluator.add_prediction(filename, boxes, scores)
37
+ results = evaluator.evaluate()
38
+ print(f"Easy={results['easy_ap']:.4f}, Med={results['medium_ap']:.4f}, Hard={results['hard_ap']:.4f}")
39
+ """
40
+
41
+ # WiderFace event names (61 event categories)
42
+ EVENTS = [
43
+ '0--Parade', '1--Handshaking', '2--Demonstration',
44
+ '3--Riot', '4--Dancing', '5--Car_Accident',
45
+ '6--Funeral', '7--Cheering', '8--Election_Campain',
46
+ '9--Press_Conference', '10--People_Marching',
47
+ '11--Meeting', '12--Group', '13--Interview',
48
+ '14--Traffic', '15--Stock_Market', '16--Award_Ceremony',
49
+ '17--Ceremony', '18--Concerts', '19--Couple',
50
+ '20--Family_Group', '21--Festival', '22--Picnic',
51
+ '23--Shoppers', '24--Soldier_Firing', '25--Soldier_Patrol',
52
+ '26--Soldier_Drilling', '27--Spa', '28--Sports_Fan',
53
+ '29--Students_Schoolkids', '30--Surgeons',
54
+ '31--Waiter_Waitress', '32--Workers_Laborers',
55
+ '33--Running', '34--Baseball', '35--Basketball',
56
+ '36--Football', '37--Soccer', '38--Tennis',
57
+ '39--Ice_Skating', '40--Gymnastics', '41--Swimming',
58
+ '42--Car_Racing', '43--Row_Boat', '44--Aerobics',
59
+ '45--Balloonist', '46--Jockey', '47--Matador_Bullfighter',
60
+ '48--Parachutist_Paraglider', '49--Greeting',
61
+ '50--Celebration_Or_Party', '51--Dresses',
62
+ '52--Photographers', '53--Raid', '54--Rescue',
63
+ '55--Sports_Coach_Trainer', '56--Voter',
64
+ '57--Angler', '58--Hockey', '59--people--driving--car',
65
+ '60--Tableau', '61--Street_Battle',
66
+ ]
67
+
68
+ def __init__(self, gt_dir: Optional[str] = None, iou_threshold: float = 0.5):
69
+ """
70
+ Args:
71
+ gt_dir: Directory containing WiderFace ground truth annotation files
72
+ iou_threshold: IoU threshold for matching (default: 0.5, WiderFace standard)
73
+ """
74
+ self.gt_dir = gt_dir
75
+ self.iou_threshold = iou_threshold
76
+ self.predictions = {} # filename → (boxes, scores)
77
+ self.ground_truth = {} # filename → boxes
78
+
79
+ if gt_dir:
80
+ self._load_ground_truth()
81
+
82
+ def _load_ground_truth(self):
83
+ """Load WiderFace validation ground truth."""
84
+ ann_file = os.path.join(self.gt_dir, 'wider_face_val_bbx_gt.txt')
85
+ if not os.path.exists(ann_file):
86
+ print(f"Warning: GT file not found: {ann_file}")
87
+ return
88
+
89
+ with open(ann_file, 'r') as f:
90
+ while True:
91
+ filename = f.readline().strip()
92
+ if not filename:
93
+ break
94
+ num_faces = int(f.readline().strip())
95
+ boxes = []
96
+ for _ in range(max(num_faces, 1)):
97
+ line = f.readline().strip()
98
+ if num_faces == 0:
99
+ continue
100
+ parts = list(map(float, line.split()))
101
+ x, y, w, h = parts[0], parts[1], parts[2], parts[3]
102
+ if w > 0 and h > 0:
103
+ boxes.append([x, y, x+w, y+h])
104
+
105
+ self.ground_truth[filename] = np.array(boxes, dtype=np.float32) \
106
+ if boxes else np.empty((0, 4), dtype=np.float32)
107
+
108
+ def add_prediction(self, filename: str, boxes: np.ndarray, scores: np.ndarray):
109
+ """Add prediction for a single image."""
110
+ self.predictions[filename] = (boxes.copy(), scores.copy())
111
+
112
+ def evaluate(self, difficulty: str = 'all') -> Dict:
113
+ """
114
+ Run WiderFace evaluation.
115
+
116
+ Args:
117
+ difficulty: 'easy', 'medium', 'hard', or 'all'
118
+
119
+ Returns:
120
+ dict with AP values per difficulty level
121
+ """
122
+ results = {}
123
+
124
+ for diff in (['easy', 'medium', 'hard'] if difficulty == 'all' else [difficulty]):
125
+ ap = self._evaluate_difficulty(diff)
126
+ results[f'{diff}_ap'] = ap
127
+
128
+ return results
129
+
130
+ def _evaluate_difficulty(self, difficulty: str) -> float:
131
+ """Evaluate AP for a single difficulty level."""
132
+ # For full evaluation, we'd need the official difficulty masks
133
+ # Here we implement a simplified version based on face size
134
+ size_thresholds = {
135
+ 'easy': 50, # faces > 50px
136
+ 'medium': 20, # faces > 20px
137
+ 'hard': 0, # all faces
138
+ }
139
+ min_size = size_thresholds.get(difficulty, 0)
140
+
141
+ all_tp = []
142
+ all_fp = []
143
+ all_scores = []
144
+ total_gt = 0
145
+
146
+ for filename in self.ground_truth:
147
+ gt_boxes = self.ground_truth[filename]
148
+
149
+ # Filter GT by size for difficulty level
150
+ if min_size > 0 and len(gt_boxes) > 0:
151
+ sizes = np.sqrt((gt_boxes[:, 2] - gt_boxes[:, 0]) *
152
+ (gt_boxes[:, 3] - gt_boxes[:, 1]))
153
+ gt_mask = sizes >= min_size
154
+ gt_boxes = gt_boxes[gt_mask]
155
+
156
+ total_gt += len(gt_boxes)
157
+
158
+ if filename not in self.predictions:
159
+ continue
160
+
161
+ pred_boxes, pred_scores = self.predictions[filename]
162
+
163
+ if len(pred_boxes) == 0 or len(gt_boxes) == 0:
164
+ all_fp.extend([1] * len(pred_boxes))
165
+ all_tp.extend([0] * len(pred_boxes))
166
+ all_scores.extend(pred_scores.tolist())
167
+ continue
168
+
169
+ # Match predictions to GT
170
+ iou_matrix = compute_iou_matrix(pred_boxes, gt_boxes)
171
+ gt_matched = np.zeros(len(gt_boxes), dtype=bool)
172
+
173
+ # Sort predictions by score (descending)
174
+ order = np.argsort(-pred_scores)
175
+ for i in order:
176
+ if iou_matrix.shape[1] > 0:
177
+ best_gt = iou_matrix[i].argmax()
178
+ if iou_matrix[i, best_gt] >= self.iou_threshold and not gt_matched[best_gt]:
179
+ all_tp.append(1)
180
+ all_fp.append(0)
181
+ gt_matched[best_gt] = True
182
+ else:
183
+ all_tp.append(0)
184
+ all_fp.append(1)
185
+ else:
186
+ all_tp.append(0)
187
+ all_fp.append(1)
188
+ all_scores.append(pred_scores[i])
189
+
190
+ if total_gt == 0:
191
+ return 0.0
192
+
193
+ # Sort by score
194
+ order = np.argsort(-np.array(all_scores))
195
+ tp = np.array(all_tp)[order]
196
+ fp = np.array(all_fp)[order]
197
+
198
+ tp_cumsum = np.cumsum(tp)
199
+ fp_cumsum = np.cumsum(fp)
200
+
201
+ recall = tp_cumsum / total_gt
202
+ precision = tp_cumsum / (tp_cumsum + fp_cumsum)
203
+
204
+ return compute_ap(recall, precision, use_11_point=True)
205
+
206
+ def save_predictions(self, output_dir: str):
207
+ """Save predictions in WiderFace submission format."""
208
+ os.makedirs(output_dir, exist_ok=True)
209
+
210
+ for filename, (boxes, scores) in self.predictions.items():
211
+ event = os.path.dirname(filename)
212
+ event_dir = os.path.join(output_dir, event)
213
+ os.makedirs(event_dir, exist_ok=True)
214
+
215
+ base = os.path.splitext(os.path.basename(filename))[0]
216
+ pred_file = os.path.join(event_dir, f'{base}.txt')
217
+
218
+ with open(pred_file, 'w') as f:
219
+ f.write(f'{base}\n')
220
+ f.write(f'{len(boxes)}\n')
221
+ for i in range(len(boxes)):
222
+ x1, y1, x2, y2 = boxes[i]
223
+ w, h = x2 - x1, y2 - y1
224
+ f.write(f'{x1:.1f} {y1:.1f} {w:.1f} {h:.1f} {scores[i]:.4f}\n')
225
+
226
+ def generate_report(self) -> str:
227
+ """Generate a text report of evaluation results."""
228
+ results = self.evaluate()
229
+ report = [
230
+ "=" * 60,
231
+ "WiderFace Evaluation Results",
232
+ "=" * 60,
233
+ f" Easy AP: {results.get('easy_ap', 0):.4f}",
234
+ f" Medium AP: {results.get('medium_ap', 0):.4f}",
235
+ f" Hard AP: {results.get('hard_ap', 0):.4f}",
236
+ f"",
237
+ f" Total images with GT: {len(self.ground_truth)}",
238
+ f" Total images with predictions: {len(self.predictions)}",
239
+ f" IoU threshold: {self.iou_threshold}",
240
+ "=" * 60,
241
+ ]
242
+ return '\n'.join(report)