Diff of /yolov5/val.py [000000] .. [f26a44]

Switch to unified view

a b/yolov5/val.py
1
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
"""
3
Validate a trained YOLOv5 model accuracy on a custom dataset
4
5
Usage:
6
    $ python path/to/val.py --data coco128.yaml --weights yolov5s.pt --img 640
7
"""
8
9
import argparse
10
import json
11
import os
12
import sys
13
from pathlib import Path
14
from threading import Thread
15
16
import numpy as np
17
import torch
18
from tqdm import tqdm
19
20
FILE = Path(__file__).resolve()
21
ROOT = FILE.parents[0]  # YOLOv5 root directory
22
if str(ROOT) not in sys.path:
23
    sys.path.append(str(ROOT))  # add ROOT to PATH
24
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative
25
26
from models.common import DetectMultiBackend
27
from utils.callbacks import Callbacks
28
from utils.datasets import create_dataloader
29
from utils.general import (LOGGER, box_iou, check_dataset, check_img_size, check_requirements, check_yaml,
30
                           coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
31
                           scale_coords, xywh2xyxy, xyxy2xywh)
32
from utils.metrics import ConfusionMatrix, ap_per_class
33
from utils.plots import output_to_target, plot_images, plot_val_study
34
from utils.torch_utils import select_device, time_sync
35
36
37
def save_one_txt(predn, save_conf, shape, file):
38
    # Save one txt result
39
    gn = torch.tensor(shape)[[1, 0, 1, 0]]  # normalization gain whwh
40
    for *xyxy, conf, cls in predn.tolist():
41
        xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # normalized xywh
42
        line = (cls, *xywh, conf) if save_conf else (cls, *xywh)  # label format
43
        with open(file, 'a') as f:
44
            f.write(('%g ' * len(line)).rstrip() % line + '\n')
45
46
47
def save_one_json(predn, jdict, path, class_map):
48
    # Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
49
    image_id = int(path.stem) if path.stem.isnumeric() else path.stem
50
    box = xyxy2xywh(predn[:, :4])  # xywh
51
    box[:, :2] -= box[:, 2:] / 2  # xy center to top-left corner
52
    for p, b in zip(predn.tolist(), box.tolist()):
53
        jdict.append({'image_id': image_id,
54
                      'category_id': class_map[int(p[5])],
55
                      'bbox': [round(x, 3) for x in b],
56
                      'score': round(p[4], 5)})
57
58
59
def process_batch(detections, labels, iouv):
60
    """
61
    Return correct predictions matrix. Both sets of boxes are in (x1, y1, x2, y2) format.
62
    Arguments:
63
        detections (Array[N, 6]), x1, y1, x2, y2, conf, class
64
        labels (Array[M, 5]), class, x1, y1, x2, y2
65
    Returns:
66
        correct (Array[N, 10]), for 10 IoU levels
67
    """
68
    correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device)
69
    iou = box_iou(labels[:, 1:], detections[:, :4])
70
    x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5]))  # IoU above threshold and classes match
71
    if x[0].shape[0]:
72
        matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy()  # [label, detection, iou]
73
        if x[0].shape[0] > 1:
74
            matches = matches[matches[:, 2].argsort()[::-1]]
75
            matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
76
            # matches = matches[matches[:, 2].argsort()[::-1]]
77
            matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
78
        matches = torch.Tensor(matches).to(iouv.device)
79
        correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv
80
    return correct
81
82
83
@torch.no_grad()
84
def run(data,
85
        weights=None,  # model.pt path(s)
86
        batch_size=32,  # batch size
87
        imgsz=640,  # inference size (pixels)
88
        conf_thres=0.001,  # confidence threshold
89
        iou_thres=0.6,  # NMS IoU threshold
90
        task='val',  # train, val, test, speed or study
91
        device='',  # cuda device, i.e. 0 or 0,1,2,3 or cpu
92
        single_cls=False,  # treat as single-class dataset
93
        augment=False,  # augmented inference
94
        verbose=False,  # verbose output
95
        save_txt=False,  # save results to *.txt
96
        save_hybrid=False,  # save label+prediction hybrid results to *.txt
97
        save_conf=False,  # save confidences in --save-txt labels
98
        save_json=False,  # save a COCO-JSON results file
99
        project=ROOT / 'runs/val',  # save to project/name
100
        name='exp',  # save to project/name
101
        exist_ok=False,  # existing project/name ok, do not increment
102
        half=True,  # use FP16 half-precision inference
103
        dnn=False,  # use OpenCV DNN for ONNX inference
104
        model=None,
105
        dataloader=None,
106
        save_dir=Path(''),
107
        plots=True,
108
        callbacks=Callbacks(),
109
        compute_loss=None,
110
        ):
111
    # Initialize/load model and set device
112
    training = model is not None
113
    if training:  # called by train.py
114
        device, pt, engine = next(model.parameters()).device, True, False  # get model device, PyTorch model
115
116
        half &= device.type != 'cpu'  # half precision only supported on CUDA
117
        model.half() if half else model.float()
118
    else:  # called directly
119
        device = select_device(device, batch_size=batch_size)
120
121
        # Directories
122
        save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)  # increment run
123
        (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)  # make dir
124
125
        # Load model
126
        model = DetectMultiBackend(weights, device=device, dnn=dnn)
127
        stride, pt, engine = model.stride, model.pt, model.engine
128
        imgsz = check_img_size(imgsz, s=stride)  # check image size
129
        half &= (pt or engine) and device.type != 'cpu'  # half precision only supported by PyTorch on CUDA
130
        if pt:
131
            model.model.half() if half else model.model.float()
132
        elif engine:
133
            batch_size = model.batch_size
134
        else:
135
            half = False
136
            batch_size = 1  # export.py models default to batch-size 1
137
            device = torch.device('cpu')
138
            LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')
139
140
        # Data
141
        data = check_dataset(data)  # check
142
143
    # Configure
144
    model.eval()
145
    is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt')  # COCO dataset
146
    nc = 1 if single_cls else int(data['nc'])  # number of classes
147
    iouv = torch.linspace(0.5, 0.95, 10).to(device)  # iou vector for mAP@0.5:0.95
148
    niou = iouv.numel()
149
150
    # Dataloader
151
    if not training:
152
        model.warmup(imgsz=(1, 3, imgsz, imgsz), half=half)  # warmup
153
        pad = 0.0 if task == 'speed' else 0.5
154
        task = task if task in ('train', 'val', 'test') else 'val'  # path to train/val/test images
155
        dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
156
                                       prefix=colorstr(f'{task}: '))[0]
157
158
    seen = 0
159
    confusion_matrix = ConfusionMatrix(nc=nc)
160
    names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
161
    class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
162
    s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
163
    dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
164
    loss = torch.zeros(3, device=device)
165
    jdict, stats, ap, ap_class = [], [], [], []
166
    pbar = tqdm(dataloader, desc=s, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # progress bar
167
    for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
168
        t1 = time_sync()
169
        if pt or engine:
170
            im = im.to(device, non_blocking=True)
171
            targets = targets.to(device)
172
        im = im.half() if half else im.float()  # uint8 to fp16/32
173
        im /= 255  # 0 - 255 to 0.0 - 1.0
174
        nb, _, height, width = im.shape  # batch size, channels, height, width
175
        t2 = time_sync()
176
        dt[0] += t2 - t1
177
178
        # Inference
179
        out, train_out = model(im) if training else model(im, augment=augment, val=True)  # inference, loss outputs
180
        dt[1] += time_sync() - t2
181
182
        # Loss
183
        if compute_loss:
184
            loss += compute_loss([x.float() for x in train_out], targets)[1]  # box, obj, cls
185
186
        # NMS
187
        targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device)  # to pixels
188
        lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []  # for autolabelling
189
        t3 = time_sync()
190
        out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
191
        dt[2] += time_sync() - t3
192
193
        # Metrics
194
        for si, pred in enumerate(out):
195
            labels = targets[targets[:, 0] == si, 1:]
196
            nl = len(labels)
197
            tcls = labels[:, 0].tolist() if nl else []  # target class
198
            path, shape = Path(paths[si]), shapes[si][0]
199
            seen += 1
200
201
            if len(pred) == 0:
202
                if nl:
203
                    stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
204
                continue
205
206
            # Predictions
207
            if single_cls:
208
                pred[:, 5] = 0
209
            predn = pred.clone()
210
            scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1])  # native-space pred
211
212
            # Evaluate
213
            if nl:
214
                tbox = xywh2xyxy(labels[:, 1:5])  # target boxes
215
                scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1])  # native-space labels
216
                labelsn = torch.cat((labels[:, 0:1], tbox), 1)  # native-space labels
217
                correct = process_batch(predn, labelsn, iouv)
218
                if plots:
219
                    confusion_matrix.process_batch(predn, labelsn)
220
            else:
221
                correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
222
            stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))  # (correct, conf, pcls, tcls)
223
224
            # Save/log
225
            if save_txt:
226
                save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
227
            if save_json:
228
                save_one_json(predn, jdict, path, class_map)  # append to COCO-JSON dictionary
229
            callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
230
231
        # Plot images
232
        if plots and batch_i < 3:
233
            f = save_dir / f'val_batch{batch_i}_labels.jpg'  # labels
234
            Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
235
            f = save_dir / f'val_batch{batch_i}_pred.jpg'  # predictions
236
            Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
237
238
    # Compute metrics
239
    stats = [np.concatenate(x, 0) for x in zip(*stats)]  # to numpy
240
    if len(stats) and stats[0].any():
241
        tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
242
        ap50, ap = ap[:, 0], ap.mean(1)  # AP@0.5, AP@0.5:0.95
243
        mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
244
        nt = np.bincount(stats[3].astype(np.int64), minlength=nc)  # number of targets per class
245
    else:
246
        nt = torch.zeros(1)
247
248
    # Print results
249
    pf = '%20s' + '%11i' * 2 + '%11.3g' * 4  # print format
250
    LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
251
252
    # Print results per class
253
    if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
254
        for i, c in enumerate(ap_class):
255
            LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
256
257
    # Print speeds
258
    t = tuple(x / seen * 1E3 for x in dt)  # speeds per image
259
    if not training:
260
        shape = (batch_size, 3, imgsz, imgsz)
261
        LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)
262
263
    # Plots
264
    if plots:
265
        confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
266
        callbacks.run('on_val_end')
267
268
    # Save JSON
269
    if save_json and len(jdict):
270
        w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else ''  # weights
271
        anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json')  # annotations json
272
        pred_json = str(save_dir / f"{w}_predictions.json")  # predictions json
273
        LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
274
        with open(pred_json, 'w') as f:
275
            json.dump(jdict, f)
276
277
        try:  # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
278
            check_requirements(['pycocotools'])
279
            from pycocotools.coco import COCO
280
            from pycocotools.cocoeval import COCOeval
281
282
            anno = COCO(anno_json)  # init annotations api
283
            pred = anno.loadRes(pred_json)  # init predictions api
284
            eval = COCOeval(anno, pred, 'bbox')
285
            if is_coco:
286
                eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files]  # image IDs to evaluate
287
            eval.evaluate()
288
            eval.accumulate()
289
            eval.summarize()
290
            map, map50 = eval.stats[:2]  # update results (mAP@0.5:0.95, mAP@0.5)
291
        except Exception as e:
292
            LOGGER.info(f'pycocotools unable to run: {e}')
293
294
    # Return results
295
    model.float()  # for training
296
    if not training:
297
        s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
298
        LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
299
    maps = np.zeros(nc) + map
300
    for i, c in enumerate(ap_class):
301
        maps[c] = ap[i]
302
    return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
303
304
305
def parse_opt():
306
    parser = argparse.ArgumentParser()
307
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
308
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
309
    parser.add_argument('--batch-size', type=int, default=32, help='batch size')
310
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
311
    parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
312
    parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
313
    parser.add_argument('--task', default='val', help='train, val, test, speed or study')
314
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
315
    parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
316
    parser.add_argument('--augment', action='store_true', help='augmented inference')
317
    parser.add_argument('--verbose', action='store_true', help='report mAP by class')
318
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
319
    parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
320
    parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
321
    parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
322
    parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
323
    parser.add_argument('--name', default='exp', help='save to project/name')
324
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
325
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
326
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
327
    opt = parser.parse_args()
328
    opt.data = check_yaml(opt.data)  # check YAML
329
    opt.save_json |= opt.data.endswith('coco.yaml')
330
    opt.save_txt |= opt.save_hybrid
331
    print_args(FILE.stem, opt)
332
    return opt
333
334
335
def main(opt):
336
    check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
337
338
    if opt.task in ('train', 'val', 'test'):  # run normally
339
        if opt.conf_thres > 0.001:  # https://github.com/ultralytics/yolov5/issues/1466
340
            LOGGER.info(f'WARNING: confidence threshold {opt.conf_thres} >> 0.001 will produce invalid mAP values.')
341
        run(**vars(opt))
342
343
    else:
344
        weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
345
        opt.half = True  # FP16 for fastest results
346
        if opt.task == 'speed':  # speed benchmarks
347
            # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
348
            opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
349
            for opt.weights in weights:
350
                run(**vars(opt), plots=False)
351
352
        elif opt.task == 'study':  # speed vs mAP benchmarks
353
            # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
354
            for opt.weights in weights:
355
                f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt'  # filename to save to
356
                x, y = list(range(256, 1536 + 128, 128)), []  # x axis (image sizes), y axis
357
                for opt.imgsz in x:  # img-size
358
                    LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
359
                    r, _, t = run(**vars(opt), plots=False)
360
                    y.append(r + t)  # results and times
361
                np.savetxt(f, y, fmt='%10.4g')  # save
362
            os.system('zip -r study.zip study_*.txt')
363
            plot_val_study(x=x)  # plot
364
365
366
if __name__ == "__main__":
367
    opt = parse_opt()
368
    main(opt)