glenn-jocher
commited on
Commit
•
354a97e
1
Parent(s):
d681799
Create sotabench.py
Browse files- sotabench.py +310 -0
sotabench.py
ADDED
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import glob
|
3 |
+
import json
|
4 |
+
import os
|
5 |
+
import shutil
|
6 |
+
from pathlib import Path
|
7 |
+
|
8 |
+
import numpy as np
|
9 |
+
import torch
|
10 |
+
import yaml
|
11 |
+
from tqdm import tqdm
|
12 |
+
|
13 |
+
from models.experimental import attempt_load
|
14 |
+
from utils.datasets import create_dataloader
|
15 |
+
from utils.general import (
|
16 |
+
coco80_to_coco91_class, check_dataset, check_file, check_img_size, compute_loss, non_max_suppression, scale_coords,
|
17 |
+
xyxy2xywh, clip_coords, plot_images, xywh2xyxy, box_iou, output_to_target, ap_per_class, set_logging)
|
18 |
+
from utils.torch_utils import select_device, time_synchronized
|
19 |
+
|
20 |
+
|
21 |
+
from sotabencheval.object_detection import COCOEvaluator
|
22 |
+
from sotabencheval.utils import is_server
|
23 |
+
|
24 |
+
DATA_ROOT = './.data/vision/coco' if is_server() else '../coco' # sotabench data dir
|
25 |
+
|
26 |
+
|
27 |
+
def test(data,
|
28 |
+
weights=None,
|
29 |
+
batch_size=16,
|
30 |
+
imgsz=640,
|
31 |
+
conf_thres=0.001,
|
32 |
+
iou_thres=0.6, # for NMS
|
33 |
+
save_json=False,
|
34 |
+
single_cls=False,
|
35 |
+
augment=False,
|
36 |
+
verbose=False,
|
37 |
+
model=None,
|
38 |
+
dataloader=None,
|
39 |
+
save_dir='',
|
40 |
+
merge=False,
|
41 |
+
save_txt=False):
|
42 |
+
# Initialize/load model and set device
|
43 |
+
training = model is not None
|
44 |
+
if training: # called by train.py
|
45 |
+
device = next(model.parameters()).device # get model device
|
46 |
+
|
47 |
+
else: # called directly
|
48 |
+
set_logging()
|
49 |
+
device = select_device(opt.device, batch_size=batch_size)
|
50 |
+
merge, save_txt = opt.merge, opt.save_txt # use Merge NMS, save *.txt labels
|
51 |
+
if save_txt:
|
52 |
+
out = Path('inference/output')
|
53 |
+
if os.path.exists(out):
|
54 |
+
shutil.rmtree(out) # delete output folder
|
55 |
+
os.makedirs(out) # make new output folder
|
56 |
+
|
57 |
+
# Remove previous
|
58 |
+
for f in glob.glob(str(Path(save_dir) / 'test_batch*.jpg')):
|
59 |
+
os.remove(f)
|
60 |
+
|
61 |
+
# Load model
|
62 |
+
model = attempt_load(weights, map_location=device) # load FP32 model
|
63 |
+
imgsz = check_img_size(imgsz, s=model.stride.max()) # check img_size
|
64 |
+
|
65 |
+
# Multi-GPU disabled, incompatible with .half() https://github.com/ultralytics/yolov5/issues/99
|
66 |
+
# if device.type != 'cpu' and torch.cuda.device_count() > 1:
|
67 |
+
# model = nn.DataParallel(model)
|
68 |
+
|
69 |
+
# Half
|
70 |
+
half = device.type != 'cpu' # half precision only supported on CUDA
|
71 |
+
if half:
|
72 |
+
model.half()
|
73 |
+
|
74 |
+
# Configure
|
75 |
+
model.eval()
|
76 |
+
with open(data) as f:
|
77 |
+
data = yaml.load(f, Loader=yaml.FullLoader) # model dict
|
78 |
+
check_dataset(data) # check
|
79 |
+
nc = 1 if single_cls else int(data['nc']) # number of classes
|
80 |
+
iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95
|
81 |
+
niou = iouv.numel()
|
82 |
+
|
83 |
+
# Dataloader
|
84 |
+
if not training:
|
85 |
+
img = torch.zeros((1, 3, imgsz, imgsz), device=device) # init img
|
86 |
+
_ = model(img.half() if half else img) if device.type != 'cpu' else None # run once
|
87 |
+
path = data['test'] if opt.task == 'test' else data['val'] # path to val/test images
|
88 |
+
dataloader = create_dataloader(path, imgsz, batch_size, model.stride.max(), opt,
|
89 |
+
hyp=None, augment=False, cache=True, pad=0.5, rect=True)[0]
|
90 |
+
|
91 |
+
seen = 0
|
92 |
+
names = model.names if hasattr(model, 'names') else model.module.names
|
93 |
+
coco91class = coco80_to_coco91_class()
|
94 |
+
s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Targets', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
|
95 |
+
p, r, f1, mp, mr, map50, map, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0.
|
96 |
+
loss = torch.zeros(3, device=device)
|
97 |
+
jdict, stats, ap, ap_class = [], [], [], []
|
98 |
+
evaluator = COCOEvaluator(root=DATA_ROOT, model_name=opt.weights.replace('.pt', ''))
|
99 |
+
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
|
100 |
+
img = img.to(device, non_blocking=True)
|
101 |
+
img = img.half() if half else img.float() # uint8 to fp16/32
|
102 |
+
img /= 255.0 # 0 - 255 to 0.0 - 1.0
|
103 |
+
targets = targets.to(device)
|
104 |
+
nb, _, height, width = img.shape # batch size, channels, height, width
|
105 |
+
whwh = torch.Tensor([width, height, width, height]).to(device)
|
106 |
+
|
107 |
+
# Disable gradients
|
108 |
+
with torch.no_grad():
|
109 |
+
# Run model
|
110 |
+
t = time_synchronized()
|
111 |
+
inf_out, train_out = model(img, augment=augment) # inference and training outputs
|
112 |
+
t0 += time_synchronized() - t
|
113 |
+
|
114 |
+
# Compute loss
|
115 |
+
if training: # if model has loss hyperparameters
|
116 |
+
loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # GIoU, obj, cls
|
117 |
+
|
118 |
+
# Run NMS
|
119 |
+
t = time_synchronized()
|
120 |
+
output = non_max_suppression(inf_out, conf_thres=conf_thres, iou_thres=iou_thres, merge=merge)
|
121 |
+
t1 += time_synchronized() - t
|
122 |
+
|
123 |
+
# Statistics per image
|
124 |
+
for si, pred in enumerate(output):
|
125 |
+
labels = targets[targets[:, 0] == si, 1:]
|
126 |
+
nl = len(labels)
|
127 |
+
tcls = labels[:, 0].tolist() if nl else [] # target class
|
128 |
+
seen += 1
|
129 |
+
|
130 |
+
if pred is None:
|
131 |
+
if nl:
|
132 |
+
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
|
133 |
+
continue
|
134 |
+
|
135 |
+
# Append to text file
|
136 |
+
if save_txt:
|
137 |
+
gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]] # normalization gain whwh
|
138 |
+
x = pred.clone()
|
139 |
+
x[:, :4] = scale_coords(img[si].shape[1:], x[:, :4], shapes[si][0], shapes[si][1]) # to original
|
140 |
+
for *xyxy, conf, cls in x:
|
141 |
+
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
|
142 |
+
with open(str(out / Path(paths[si]).stem) + '.txt', 'a') as f:
|
143 |
+
f.write(('%g ' * 5 + '\n') % (cls, *xywh)) # label format
|
144 |
+
|
145 |
+
# Clip boxes to image bounds
|
146 |
+
clip_coords(pred, (height, width))
|
147 |
+
|
148 |
+
# Append to pycocotools JSON dictionary
|
149 |
+
if save_json:
|
150 |
+
# [{"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}, ...
|
151 |
+
image_id = Path(paths[si]).stem
|
152 |
+
box = pred[:, :4].clone() # xyxy
|
153 |
+
scale_coords(img[si].shape[1:], box, shapes[si][0], shapes[si][1]) # to original shape
|
154 |
+
box = xyxy2xywh(box) # xywh
|
155 |
+
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
156 |
+
for p, b in zip(pred.tolist(), box.tolist()):
|
157 |
+
result = {'image_id': int(image_id) if image_id.isnumeric() else image_id,
|
158 |
+
'category_id': coco91class[int(p[5])],
|
159 |
+
'bbox': [round(x, 3) for x in b],
|
160 |
+
'score': round(p[4], 5)}
|
161 |
+
jdict.append(result)
|
162 |
+
|
163 |
+
#evaluator.add([result])
|
164 |
+
#if evaluator.cache_exists:
|
165 |
+
# break
|
166 |
+
|
167 |
+
# # Assign all predictions as incorrect
|
168 |
+
# correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device)
|
169 |
+
# if nl:
|
170 |
+
# detected = [] # target indices
|
171 |
+
# tcls_tensor = labels[:, 0]
|
172 |
+
#
|
173 |
+
# # target boxes
|
174 |
+
# tbox = xywh2xyxy(labels[:, 1:5]) * whwh
|
175 |
+
#
|
176 |
+
# # Per target class
|
177 |
+
# for cls in torch.unique(tcls_tensor):
|
178 |
+
# ti = (cls == tcls_tensor).nonzero(as_tuple=False).view(-1) # prediction indices
|
179 |
+
# pi = (cls == pred[:, 5]).nonzero(as_tuple=False).view(-1) # target indices
|
180 |
+
#
|
181 |
+
# # Search for detections
|
182 |
+
# if pi.shape[0]:
|
183 |
+
# # Prediction to target ious
|
184 |
+
# ious, i = box_iou(pred[pi, :4], tbox[ti]).max(1) # best ious, indices
|
185 |
+
#
|
186 |
+
# # Append detections
|
187 |
+
# detected_set = set()
|
188 |
+
# for j in (ious > iouv[0]).nonzero(as_tuple=False):
|
189 |
+
# d = ti[i[j]] # detected target
|
190 |
+
# if d.item() not in detected_set:
|
191 |
+
# detected_set.add(d.item())
|
192 |
+
# detected.append(d)
|
193 |
+
# correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn
|
194 |
+
# if len(detected) == nl: # all targets already located in image
|
195 |
+
# break
|
196 |
+
#
|
197 |
+
# # Append statistics (correct, conf, pcls, tcls)
|
198 |
+
# stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
|
199 |
+
|
200 |
+
# # Plot images
|
201 |
+
# if batch_i < 1:
|
202 |
+
# f = Path(save_dir) / ('test_batch%g_gt.jpg' % batch_i) # filename
|
203 |
+
# plot_images(img, targets, paths, str(f), names) # ground truth
|
204 |
+
# f = Path(save_dir) / ('test_batch%g_pred.jpg' % batch_i)
|
205 |
+
# plot_images(img, output_to_target(output, width, height), paths, str(f), names) # predictions
|
206 |
+
|
207 |
+
evaluator.add(jdict)
|
208 |
+
evaluator.save()
|
209 |
+
|
210 |
+
# # Compute statistics
|
211 |
+
# stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy
|
212 |
+
# if len(stats) and stats[0].any():
|
213 |
+
# p, r, ap, f1, ap_class = ap_per_class(*stats)
|
214 |
+
# p, r, ap50, ap = p[:, 0], r[:, 0], ap[:, 0], ap.mean(1) # [P, R, AP@0.5, AP@0.5:0.95]
|
215 |
+
# mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
|
216 |
+
# nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
|
217 |
+
# else:
|
218 |
+
# nt = torch.zeros(1)
|
219 |
+
#
|
220 |
+
# # Print results
|
221 |
+
# pf = '%20s' + '%12.3g' * 6 # print format
|
222 |
+
# print(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
|
223 |
+
#
|
224 |
+
# # Print results per class
|
225 |
+
# if verbose and nc > 1 and len(stats):
|
226 |
+
# for i, c in enumerate(ap_class):
|
227 |
+
# print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
|
228 |
+
#
|
229 |
+
# # Print speeds
|
230 |
+
# t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + (imgsz, imgsz, batch_size) # tuple
|
231 |
+
# if not training:
|
232 |
+
# print('Speed: %.1f/%.1f/%.1f ms inference/NMS/total per %gx%g image at batch-size %g' % t)
|
233 |
+
#
|
234 |
+
# # Save JSON
|
235 |
+
# if save_json and len(jdict):
|
236 |
+
# f = 'detections_val2017_%s_results.json' % \
|
237 |
+
# (weights.split(os.sep)[-1].replace('.pt', '') if isinstance(weights, str) else '') # filename
|
238 |
+
# print('\nCOCO mAP with pycocotools... saving %s...' % f)
|
239 |
+
# with open(f, 'w') as file:
|
240 |
+
# json.dump(jdict, file)
|
241 |
+
#
|
242 |
+
# try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
|
243 |
+
# from pycocotools.coco import COCO
|
244 |
+
# from pycocotools.cocoeval import COCOeval
|
245 |
+
#
|
246 |
+
# imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files]
|
247 |
+
# cocoGt = COCO(glob.glob('../coco/annotations/instances_val*.json')[0]) # initialize COCO ground truth api
|
248 |
+
# cocoDt = cocoGt.loadRes(f) # initialize COCO pred api
|
249 |
+
# cocoEval = COCOeval(cocoGt, cocoDt, 'bbox')
|
250 |
+
# cocoEval.params.imgIds = imgIds # image IDs to evaluate
|
251 |
+
# cocoEval.evaluate()
|
252 |
+
# cocoEval.accumulate()
|
253 |
+
# cocoEval.summarize()
|
254 |
+
# map, map50 = cocoEval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
|
255 |
+
# except Exception as e:
|
256 |
+
# print('ERROR: pycocotools unable to run: %s' % e)
|
257 |
+
#
|
258 |
+
# # Return results
|
259 |
+
# model.float() # for training
|
260 |
+
# maps = np.zeros(nc) + map
|
261 |
+
# for i, c in enumerate(ap_class):
|
262 |
+
# maps[c] = ap[i]
|
263 |
+
# return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
|
264 |
+
|
265 |
+
|
266 |
+
if __name__ == '__main__':
|
267 |
+
parser = argparse.ArgumentParser(prog='test.py')
|
268 |
+
parser.add_argument('--weights', nargs='+', type=str, default='yolov5s.pt', help='model.pt path(s)')
|
269 |
+
parser.add_argument('--data', type=str, default='data/coco.yaml', help='*.data path')
|
270 |
+
parser.add_argument('--batch-size', type=int, default=32, help='size of each image batch')
|
271 |
+
parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)')
|
272 |
+
parser.add_argument('--conf-thres', type=float, default=0.001, help='object confidence threshold')
|
273 |
+
parser.add_argument('--iou-thres', type=float, default=0.65, help='IOU threshold for NMS')
|
274 |
+
parser.add_argument('--save-json', action='store_true', help='save a cocoapi-compatible JSON results file')
|
275 |
+
parser.add_argument('--task', default='val', help="'val', 'test', 'study'")
|
276 |
+
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
|
277 |
+
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
|
278 |
+
parser.add_argument('--augment', action='store_true', help='augmented inference')
|
279 |
+
parser.add_argument('--merge', action='store_true', help='use Merge NMS')
|
280 |
+
parser.add_argument('--verbose', action='store_true', help='report mAP by class')
|
281 |
+
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
|
282 |
+
opt = parser.parse_args()
|
283 |
+
opt.save_json |= opt.data.endswith('coco.yaml')
|
284 |
+
opt.data = check_file(opt.data) # check file
|
285 |
+
print(opt)
|
286 |
+
|
287 |
+
if opt.task in ['val', 'test']: # run normally
|
288 |
+
test(opt.data,
|
289 |
+
opt.weights,
|
290 |
+
opt.batch_size,
|
291 |
+
opt.img_size,
|
292 |
+
opt.conf_thres,
|
293 |
+
opt.iou_thres,
|
294 |
+
opt.save_json,
|
295 |
+
opt.single_cls,
|
296 |
+
opt.augment,
|
297 |
+
opt.verbose)
|
298 |
+
|
299 |
+
elif opt.task == 'study': # run over a range of settings and save/plot
|
300 |
+
for weights in ['yolov5s.pt', 'yolov5m.pt', 'yolov5l.pt', 'yolov5x.pt']:
|
301 |
+
f = 'study_%s_%s.txt' % (Path(opt.data).stem, Path(weights).stem) # filename to save to
|
302 |
+
x = list(range(320, 800, 64)) # x axis
|
303 |
+
y = [] # y axis
|
304 |
+
for i in x: # img-size
|
305 |
+
print('\nRunning %s point %s...' % (f, i))
|
306 |
+
r, _, t = test(opt.data, weights, opt.batch_size, i, opt.conf_thres, opt.iou_thres, opt.save_json)
|
307 |
+
y.append(r + t) # results and times
|
308 |
+
np.savetxt(f, y, fmt='%10.4g') # save
|
309 |
+
os.system('zip -r study.zip study_*.txt')
|
310 |
+
# utils.general.plot_study_txt(f, x) # plot
|