glenn-jocher
commited on
Commit
•
fe341fa
1
Parent(s):
379396e
Utils reorganization (#1392)
Browse files* Utils reorganization
* Add new utils files
* cleanup
* simplify
* reduce datasets.py
* remove evolve.sh
* loadWebcam cleanup
- detect.py +6 -6
- models/yolo.py +8 -6
- test.py +5 -3
- train.py +10 -9
- utils/activations.py +2 -0
- utils/autoanchor.py +152 -0
- utils/datasets.py +12 -63
- utils/evolve.sh +0 -15
- utils/general.py +11 -877
- utils/google_utils.py +1 -3
- utils/loss.py +179 -0
- utils/metrics.py +110 -0
- utils/plots.py +377 -0
- utils/torch_utils.py +17 -6
detect.py
CHANGED
@@ -10,14 +10,15 @@ from numpy import random
|
|
10 |
from models.experimental import attempt_load
|
11 |
from utils.datasets import LoadStreams, LoadImages
|
12 |
from utils.general import check_img_size, non_max_suppression, apply_classifier, scale_coords, xyxy2xywh, \
|
13 |
-
|
|
|
14 |
from utils.torch_utils import select_device, load_classifier, time_synchronized
|
15 |
|
16 |
|
17 |
def detect(save_img=False):
|
18 |
source, weights, view_img, save_txt, imgsz = opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size
|
19 |
-
webcam = source.isnumeric() or source.endswith('.txt') or
|
20 |
-
|
21 |
|
22 |
# Directories
|
23 |
save_dir = Path(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # increment run
|
@@ -38,8 +39,7 @@ def detect(save_img=False):
|
|
38 |
classify = False
|
39 |
if classify:
|
40 |
modelc = load_classifier(name='resnet101', n=2) # initialize
|
41 |
-
modelc.load_state_dict(torch.load('weights/resnet101.pt', map_location=device)['model'])
|
42 |
-
modelc.to(device).eval()
|
43 |
|
44 |
# Set Dataloader
|
45 |
vid_path, vid_writer = None, None
|
@@ -53,7 +53,7 @@ def detect(save_img=False):
|
|
53 |
|
54 |
# Get names and colors
|
55 |
names = model.module.names if hasattr(model, 'module') else model.names
|
56 |
-
colors = [[random.randint(0, 255) for _ in range(3)] for _ in
|
57 |
|
58 |
# Run inference
|
59 |
t0 = time.time()
|
|
|
10 |
from models.experimental import attempt_load
|
11 |
from utils.datasets import LoadStreams, LoadImages
|
12 |
from utils.general import check_img_size, non_max_suppression, apply_classifier, scale_coords, xyxy2xywh, \
|
13 |
+
strip_optimizer, set_logging, increment_path
|
14 |
+
from utils.plots import plot_one_box
|
15 |
from utils.torch_utils import select_device, load_classifier, time_synchronized
|
16 |
|
17 |
|
18 |
def detect(save_img=False):
|
19 |
source, weights, view_img, save_txt, imgsz = opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size
|
20 |
+
webcam = source.isnumeric() or source.endswith('.txt') or source.lower().startswith(
|
21 |
+
('rtsp://', 'rtmp://', 'http://'))
|
22 |
|
23 |
# Directories
|
24 |
save_dir = Path(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # increment run
|
|
|
39 |
classify = False
|
40 |
if classify:
|
41 |
modelc = load_classifier(name='resnet101', n=2) # initialize
|
42 |
+
modelc.load_state_dict(torch.load('weights/resnet101.pt', map_location=device)['model']).to(device).eval()
|
|
|
43 |
|
44 |
# Set Dataloader
|
45 |
vid_path, vid_writer = None, None
|
|
|
53 |
|
54 |
# Get names and colors
|
55 |
names = model.module.names if hasattr(model, 'module') else model.names
|
56 |
+
colors = [[random.randint(0, 255) for _ in range(3)] for _ in names]
|
57 |
|
58 |
# Run inference
|
59 |
t0 = time.time()
|
models/yolo.py
CHANGED
@@ -13,10 +13,16 @@ import torch.nn as nn
|
|
13 |
|
14 |
from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS, autoShape
|
15 |
from models.experimental import MixConv2d, CrossConv, C3
|
16 |
-
from utils.
|
|
|
17 |
from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
|
18 |
select_device, copy_attr
|
19 |
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
class Detect(nn.Module):
|
22 |
stride = None # strides computed during build
|
@@ -121,11 +127,7 @@ class Model(nn.Module):
|
|
121 |
x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
|
122 |
|
123 |
if profile:
|
124 |
-
|
125 |
-
import thop
|
126 |
-
o = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # FLOPS
|
127 |
-
except:
|
128 |
-
o = 0
|
129 |
t = time_synchronized()
|
130 |
for _ in range(10):
|
131 |
_ = m(x)
|
|
|
13 |
|
14 |
from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS, autoShape
|
15 |
from models.experimental import MixConv2d, CrossConv, C3
|
16 |
+
from utils.autoanchor import check_anchor_order
|
17 |
+
from utils.general import make_divisible, check_file, set_logging
|
18 |
from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
|
19 |
select_device, copy_attr
|
20 |
|
21 |
+
try:
|
22 |
+
import thop # for FLOPS computation
|
23 |
+
except ImportError:
|
24 |
+
thop = None
|
25 |
+
|
26 |
|
27 |
class Detect(nn.Module):
|
28 |
stride = None # strides computed during build
|
|
|
127 |
x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
|
128 |
|
129 |
if profile:
|
130 |
+
o = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPS
|
|
|
|
|
|
|
|
|
131 |
t = time_synchronized()
|
132 |
for _ in range(10):
|
133 |
_ = m(x)
|
test.py
CHANGED
@@ -11,9 +11,11 @@ from tqdm import tqdm
|
|
11 |
|
12 |
from models.experimental import attempt_load
|
13 |
from utils.datasets import create_dataloader
|
14 |
-
from utils.general import coco80_to_coco91_class, check_dataset, check_file, check_img_size,
|
15 |
-
non_max_suppression, scale_coords, xyxy2xywh,
|
16 |
-
|
|
|
|
|
17 |
from utils.torch_utils import select_device, time_synchronized
|
18 |
|
19 |
|
|
|
11 |
|
12 |
from models.experimental import attempt_load
|
13 |
from utils.datasets import create_dataloader
|
14 |
+
from utils.general import coco80_to_coco91_class, check_dataset, check_file, check_img_size, box_iou, \
|
15 |
+
non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, clip_coords, set_logging, increment_path
|
16 |
+
from utils.loss import compute_loss
|
17 |
+
from utils.metrics import ap_per_class
|
18 |
+
from utils.plots import plot_images, output_to_target
|
19 |
from utils.torch_utils import select_device, time_synchronized
|
20 |
|
21 |
|
train.py
CHANGED
@@ -3,7 +3,6 @@ import logging
|
|
3 |
import math
|
4 |
import os
|
5 |
import random
|
6 |
-
import shutil
|
7 |
import time
|
8 |
from pathlib import Path
|
9 |
from warnings import warn
|
@@ -23,13 +22,15 @@ from tqdm import tqdm
|
|
23 |
|
24 |
import test # import test.py to get mAP after each epoch
|
25 |
from models.yolo import Model
|
|
|
26 |
from utils.datasets import create_dataloader
|
27 |
-
from utils.general import
|
28 |
-
|
29 |
-
|
30 |
-
check_git_status, check_img_size, increment_path, print_mutation, plot_evolution, set_logging, init_seeds)
|
31 |
from utils.google_utils import attempt_download
|
32 |
-
from utils.
|
|
|
|
|
33 |
|
34 |
logger = logging.getLogger(__name__)
|
35 |
|
@@ -209,7 +210,7 @@ def train(hyp, opt, device, tb_writer=None, wandb=None):
|
|
209 |
|
210 |
# Start training
|
211 |
t0 = time.time()
|
212 |
-
nw = max(round(hyp['warmup_epochs'] * nb),
|
213 |
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
|
214 |
maps = np.zeros(nc) # mAP per class
|
215 |
results = (0, 0, 0, 0, 0, 0, 0) # P, R, [email protected], [email protected], val_loss(box, obj, cls)
|
@@ -334,9 +335,9 @@ def train(hyp, opt, device, tb_writer=None, wandb=None):
|
|
334 |
os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name))
|
335 |
|
336 |
# Log
|
337 |
-
tags = ['train/
|
338 |
'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95',
|
339 |
-
'val/
|
340 |
'x/lr0', 'x/lr1', 'x/lr2'] # params
|
341 |
for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags):
|
342 |
if tb_writer:
|
|
|
3 |
import math
|
4 |
import os
|
5 |
import random
|
|
|
6 |
import time
|
7 |
from pathlib import Path
|
8 |
from warnings import warn
|
|
|
22 |
|
23 |
import test # import test.py to get mAP after each epoch
|
24 |
from models.yolo import Model
|
25 |
+
from utils.autoanchor import check_anchors
|
26 |
from utils.datasets import create_dataloader
|
27 |
+
from utils.general import labels_to_class_weights, increment_path, labels_to_image_weights, init_seeds, \
|
28 |
+
fitness, strip_optimizer, get_latest_run, check_dataset, check_file, check_git_status, check_img_size, \
|
29 |
+
print_mutation, set_logging
|
|
|
30 |
from utils.google_utils import attempt_download
|
31 |
+
from utils.loss import compute_loss
|
32 |
+
from utils.plots import plot_images, plot_labels, plot_results, plot_evolution
|
33 |
+
from utils.torch_utils import ModelEMA, select_device, intersect_dicts, torch_distributed_zero_first
|
34 |
|
35 |
logger = logging.getLogger(__name__)
|
36 |
|
|
|
210 |
|
211 |
# Start training
|
212 |
t0 = time.time()
|
213 |
+
nw = max(round(hyp['warmup_epochs'] * nb), 1000) # number of warmup iterations, max(3 epochs, 1k iterations)
|
214 |
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
|
215 |
maps = np.zeros(nc) # mAP per class
|
216 |
results = (0, 0, 0, 0, 0, 0, 0) # P, R, [email protected], [email protected], val_loss(box, obj, cls)
|
|
|
335 |
os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name))
|
336 |
|
337 |
# Log
|
338 |
+
tags = ['train/box_loss', 'train/obj_loss', 'train/cls_loss', # train loss
|
339 |
'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95',
|
340 |
+
'val/box_loss', 'val/obj_loss', 'val/cls_loss', # val loss
|
341 |
'x/lr0', 'x/lr1', 'x/lr2'] # params
|
342 |
for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags):
|
343 |
if tb_writer:
|
utils/activations.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import torch
|
2 |
import torch.nn as nn
|
3 |
import torch.nn.functional as F
|
|
|
1 |
+
# Activation functions
|
2 |
+
|
3 |
import torch
|
4 |
import torch.nn as nn
|
5 |
import torch.nn.functional as F
|
utils/autoanchor.py
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Auto-anchor utils
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
import torch
|
5 |
+
import yaml
|
6 |
+
from scipy.cluster.vq import kmeans
|
7 |
+
from tqdm import tqdm
|
8 |
+
|
9 |
+
|
10 |
+
def check_anchor_order(m):
|
11 |
+
# Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary
|
12 |
+
a = m.anchor_grid.prod(-1).view(-1) # anchor area
|
13 |
+
da = a[-1] - a[0] # delta a
|
14 |
+
ds = m.stride[-1] - m.stride[0] # delta s
|
15 |
+
if da.sign() != ds.sign(): # same order
|
16 |
+
print('Reversing anchor order')
|
17 |
+
m.anchors[:] = m.anchors.flip(0)
|
18 |
+
m.anchor_grid[:] = m.anchor_grid.flip(0)
|
19 |
+
|
20 |
+
|
21 |
+
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
22 |
+
# Check anchor fit to data, recompute if necessary
|
23 |
+
print('\nAnalyzing anchors... ', end='')
|
24 |
+
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
25 |
+
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
26 |
+
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
27 |
+
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
|
28 |
+
|
29 |
+
def metric(k): # compute metric
|
30 |
+
r = wh[:, None] / k[None]
|
31 |
+
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
32 |
+
best = x.max(1)[0] # best_x
|
33 |
+
aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
|
34 |
+
bpr = (best > 1. / thr).float().mean() # best possible recall
|
35 |
+
return bpr, aat
|
36 |
+
|
37 |
+
bpr, aat = metric(m.anchor_grid.clone().cpu().view(-1, 2))
|
38 |
+
print('anchors/target = %.2f, Best Possible Recall (BPR) = %.4f' % (aat, bpr), end='')
|
39 |
+
if bpr < 0.98: # threshold to recompute
|
40 |
+
print('. Attempting to improve anchors, please wait...')
|
41 |
+
na = m.anchor_grid.numel() // 2 # number of anchors
|
42 |
+
new_anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
43 |
+
new_bpr = metric(new_anchors.reshape(-1, 2))[0]
|
44 |
+
if new_bpr > bpr: # replace anchors
|
45 |
+
new_anchors = torch.tensor(new_anchors, device=m.anchors.device).type_as(m.anchors)
|
46 |
+
m.anchor_grid[:] = new_anchors.clone().view_as(m.anchor_grid) # for inference
|
47 |
+
m.anchors[:] = new_anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
48 |
+
check_anchor_order(m)
|
49 |
+
print('New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
50 |
+
else:
|
51 |
+
print('Original anchors better than new anchors. Proceeding with original anchors.')
|
52 |
+
print('') # newline
|
53 |
+
|
54 |
+
|
55 |
+
def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
56 |
+
""" Creates kmeans-evolved anchors from training dataset
|
57 |
+
|
58 |
+
Arguments:
|
59 |
+
path: path to dataset *.yaml, or a loaded dataset
|
60 |
+
n: number of anchors
|
61 |
+
img_size: image size used for training
|
62 |
+
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
|
63 |
+
gen: generations to evolve anchors using genetic algorithm
|
64 |
+
verbose: print all results
|
65 |
+
|
66 |
+
Return:
|
67 |
+
k: kmeans evolved anchors
|
68 |
+
|
69 |
+
Usage:
|
70 |
+
from utils.general import *; _ = kmean_anchors()
|
71 |
+
"""
|
72 |
+
thr = 1. / thr
|
73 |
+
|
74 |
+
def metric(k, wh): # compute metrics
|
75 |
+
r = wh[:, None] / k[None]
|
76 |
+
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
77 |
+
# x = wh_iou(wh, torch.tensor(k)) # iou metric
|
78 |
+
return x, x.max(1)[0] # x, best_x
|
79 |
+
|
80 |
+
def anchor_fitness(k): # mutation fitness
|
81 |
+
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
82 |
+
return (best * (best > thr).float()).mean() # fitness
|
83 |
+
|
84 |
+
def print_results(k):
|
85 |
+
k = k[np.argsort(k.prod(1))] # sort small to large
|
86 |
+
x, best = metric(k, wh0)
|
87 |
+
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
88 |
+
print('thr=%.2f: %.4f best possible recall, %.2f anchors past thr' % (thr, bpr, aat))
|
89 |
+
print('n=%g, img_size=%s, metric_all=%.3f/%.3f-mean/best, past_thr=%.3f-mean: ' %
|
90 |
+
(n, img_size, x.mean(), best.mean(), x[x > thr].mean()), end='')
|
91 |
+
for i, x in enumerate(k):
|
92 |
+
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
93 |
+
return k
|
94 |
+
|
95 |
+
if isinstance(path, str): # *.yaml file
|
96 |
+
with open(path) as f:
|
97 |
+
data_dict = yaml.load(f, Loader=yaml.FullLoader) # model dict
|
98 |
+
from utils.datasets import LoadImagesAndLabels
|
99 |
+
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
100 |
+
else:
|
101 |
+
dataset = path # dataset
|
102 |
+
|
103 |
+
# Get label wh
|
104 |
+
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
105 |
+
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
|
106 |
+
|
107 |
+
# Filter
|
108 |
+
i = (wh0 < 3.0).any(1).sum()
|
109 |
+
if i:
|
110 |
+
print('WARNING: Extremely small objects found. '
|
111 |
+
'%g of %g labels are < 3 pixels in width or height.' % (i, len(wh0)))
|
112 |
+
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
113 |
+
|
114 |
+
# Kmeans calculation
|
115 |
+
print('Running kmeans for %g anchors on %g points...' % (n, len(wh)))
|
116 |
+
s = wh.std(0) # sigmas for whitening
|
117 |
+
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
118 |
+
k *= s
|
119 |
+
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
120 |
+
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
121 |
+
k = print_results(k)
|
122 |
+
|
123 |
+
# Plot
|
124 |
+
# k, d = [None] * 20, [None] * 20
|
125 |
+
# for i in tqdm(range(1, 21)):
|
126 |
+
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
|
127 |
+
# fig, ax = plt.subplots(1, 2, figsize=(14, 7))
|
128 |
+
# ax = ax.ravel()
|
129 |
+
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
|
130 |
+
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
|
131 |
+
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
|
132 |
+
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
|
133 |
+
# fig.tight_layout()
|
134 |
+
# fig.savefig('wh.png', dpi=200)
|
135 |
+
|
136 |
+
# Evolve
|
137 |
+
npr = np.random
|
138 |
+
f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
139 |
+
pbar = tqdm(range(gen), desc='Evolving anchors with Genetic Algorithm') # progress bar
|
140 |
+
for _ in pbar:
|
141 |
+
v = np.ones(sh)
|
142 |
+
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
143 |
+
v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
|
144 |
+
kg = (k.copy() * v).clip(min=2.0)
|
145 |
+
fg = anchor_fitness(kg)
|
146 |
+
if fg > f:
|
147 |
+
f, k = fg, kg.copy()
|
148 |
+
pbar.desc = 'Evolving anchors with Genetic Algorithm: fitness = %.4f' % f
|
149 |
+
if verbose:
|
150 |
+
print_results(k)
|
151 |
+
|
152 |
+
return print_results(k)
|
utils/datasets.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import glob
|
2 |
import math
|
3 |
import os
|
@@ -16,8 +18,10 @@ from PIL import Image, ExifTags
|
|
16 |
from torch.utils.data import Dataset
|
17 |
from tqdm import tqdm
|
18 |
|
19 |
-
from utils.general import xyxy2xywh, xywh2xyxy
|
|
|
20 |
|
|
|
21 |
help_url = 'https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data'
|
22 |
img_formats = ['.bmp', '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.dng']
|
23 |
vid_formats = ['.mov', '.avi', '.mp4', '.mpg', '.mpeg', '.m4v', '.wmv', '.mkv']
|
@@ -50,7 +54,7 @@ def exif_size(img):
|
|
50 |
|
51 |
def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False,
|
52 |
rank=-1, world_size=1, workers=8):
|
53 |
-
# Make sure only the first process in DDP process the dataset first, and the following others can use the cache
|
54 |
with torch_distributed_zero_first(rank):
|
55 |
dataset = LoadImagesAndLabels(path, imgsz, batch_size,
|
56 |
augment=augment, # augment images
|
@@ -75,9 +79,9 @@ def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=Fa
|
|
75 |
|
76 |
|
77 |
class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader):
|
78 |
-
""" Dataloader that reuses workers
|
79 |
|
80 |
-
Uses same syntax as vanilla DataLoader
|
81 |
"""
|
82 |
|
83 |
def __init__(self, *args, **kwargs):
|
@@ -94,7 +98,7 @@ class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader):
|
|
94 |
|
95 |
|
96 |
class _RepeatSampler(object):
|
97 |
-
""" Sampler that repeats forever
|
98 |
|
99 |
Args:
|
100 |
sampler (Sampler)
|
@@ -177,7 +181,6 @@ class LoadImages: # for inference
|
|
177 |
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
178 |
img = np.ascontiguousarray(img)
|
179 |
|
180 |
-
# cv2.imwrite(path + '.letterbox.jpg', 255 * img.transpose((1, 2, 0))[:, :, ::-1]) # save letterbox image
|
181 |
return path, img, img0, self.cap
|
182 |
|
183 |
def new_video(self, path):
|
@@ -190,23 +193,15 @@ class LoadImages: # for inference
|
|
190 |
|
191 |
|
192 |
class LoadWebcam: # for inference
|
193 |
-
def __init__(self, pipe=0, img_size=640):
|
194 |
self.img_size = img_size
|
195 |
|
196 |
-
if pipe
|
197 |
-
pipe =
|
198 |
# pipe = 'rtsp://192.168.1.64/1' # IP camera
|
199 |
# pipe = 'rtsp://username:[email protected]/1' # IP camera with login
|
200 |
-
# pipe = 'rtsp://170.93.143.139/rtplive/470011e600ef003a004ee33696235daa' # IP traffic camera
|
201 |
# pipe = 'http://wmccpinetop.axiscam.net/mjpg/video.mjpg' # IP golf camera
|
202 |
|
203 |
-
# https://answers.opencv.org/question/215996/changing-gstreamer-pipeline-to-opencv-in-pythonsolved/
|
204 |
-
# pipe = '"rtspsrc location="rtsp://username:[email protected]/1" latency=10 ! appsink' # GStreamer
|
205 |
-
|
206 |
-
# https://answers.opencv.org/question/200787/video-acceleration-gstremer-pipeline-in-videocapture/
|
207 |
-
# https://stackoverflow.com/questions/54095699/install-gstreamer-support-for-opencv-python-package # install help
|
208 |
-
# pipe = "rtspsrc location=rtsp://root:[email protected]:554/axis-media/media.amp?videocodec=h264&resolution=3840x2160 protocols=GST_RTSP_LOWER_TRANS_TCP ! rtph264depay ! queue ! vaapih264dec ! videoconvert ! appsink" # GStreamer
|
209 |
-
|
210 |
self.pipe = pipe
|
211 |
self.cap = cv2.VideoCapture(pipe) # video capture object
|
212 |
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) # set buffer size
|
@@ -895,52 +890,6 @@ def cutout(image, labels):
|
|
895 |
return labels
|
896 |
|
897 |
|
898 |
-
def reduce_img_size(path='path/images', img_size=1024): # from utils.datasets import *; reduce_img_size()
|
899 |
-
# creates a new ./images_reduced folder with reduced size images of maximum size img_size
|
900 |
-
path_new = path + '_reduced' # reduced images path
|
901 |
-
create_folder(path_new)
|
902 |
-
for f in tqdm(glob.glob('%s/*.*' % path)):
|
903 |
-
try:
|
904 |
-
img = cv2.imread(f)
|
905 |
-
h, w = img.shape[:2]
|
906 |
-
r = img_size / max(h, w) # size ratio
|
907 |
-
if r < 1.0:
|
908 |
-
img = cv2.resize(img, (int(w * r), int(h * r)), interpolation=cv2.INTER_AREA) # _LINEAR fastest
|
909 |
-
fnew = f.replace(path, path_new) # .replace(Path(f).suffix, '.jpg')
|
910 |
-
cv2.imwrite(fnew, img)
|
911 |
-
except:
|
912 |
-
print('WARNING: image failure %s' % f)
|
913 |
-
|
914 |
-
|
915 |
-
def recursive_dataset2bmp(dataset='path/dataset_bmp'): # from utils.datasets import *; recursive_dataset2bmp()
|
916 |
-
# Converts dataset to bmp (for faster training)
|
917 |
-
formats = [x.lower() for x in img_formats] + [x.upper() for x in img_formats]
|
918 |
-
for a, b, files in os.walk(dataset):
|
919 |
-
for file in tqdm(files, desc=a):
|
920 |
-
p = a + '/' + file
|
921 |
-
s = Path(file).suffix
|
922 |
-
if s == '.txt': # replace text
|
923 |
-
with open(p, 'r') as f:
|
924 |
-
lines = f.read()
|
925 |
-
for f in formats:
|
926 |
-
lines = lines.replace(f, '.bmp')
|
927 |
-
with open(p, 'w') as f:
|
928 |
-
f.write(lines)
|
929 |
-
elif s in formats: # replace image
|
930 |
-
cv2.imwrite(p.replace(s, '.bmp'), cv2.imread(p))
|
931 |
-
if s != '.bmp':
|
932 |
-
os.system("rm '%s'" % p)
|
933 |
-
|
934 |
-
|
935 |
-
def imagelist2folder(path='path/images.txt'): # from utils.datasets import *; imagelist2folder()
|
936 |
-
# Copies all the images in a text file (list of images) into a folder
|
937 |
-
create_folder(path[:-4])
|
938 |
-
with open(path, 'r') as f:
|
939 |
-
for line in f.read().splitlines():
|
940 |
-
os.system('cp "%s" %s' % (line, path[:-4]))
|
941 |
-
print(line)
|
942 |
-
|
943 |
-
|
944 |
def create_folder(path='./new'):
|
945 |
# Create folder
|
946 |
if os.path.exists(path):
|
|
|
1 |
+
# Dataset utils and dataloaders
|
2 |
+
|
3 |
import glob
|
4 |
import math
|
5 |
import os
|
|
|
18 |
from torch.utils.data import Dataset
|
19 |
from tqdm import tqdm
|
20 |
|
21 |
+
from utils.general import xyxy2xywh, xywh2xyxy
|
22 |
+
from utils.torch_utils import torch_distributed_zero_first
|
23 |
|
24 |
+
# Parameters
|
25 |
help_url = 'https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data'
|
26 |
img_formats = ['.bmp', '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.dng']
|
27 |
vid_formats = ['.mov', '.avi', '.mp4', '.mpg', '.mpeg', '.m4v', '.wmv', '.mkv']
|
|
|
54 |
|
55 |
def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False,
|
56 |
rank=-1, world_size=1, workers=8):
|
57 |
+
# Make sure only the first process in DDP process the dataset first, and the following others can use the cache
|
58 |
with torch_distributed_zero_first(rank):
|
59 |
dataset = LoadImagesAndLabels(path, imgsz, batch_size,
|
60 |
augment=augment, # augment images
|
|
|
79 |
|
80 |
|
81 |
class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader):
|
82 |
+
""" Dataloader that reuses workers
|
83 |
|
84 |
+
Uses same syntax as vanilla DataLoader
|
85 |
"""
|
86 |
|
87 |
def __init__(self, *args, **kwargs):
|
|
|
98 |
|
99 |
|
100 |
class _RepeatSampler(object):
|
101 |
+
""" Sampler that repeats forever
|
102 |
|
103 |
Args:
|
104 |
sampler (Sampler)
|
|
|
181 |
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
|
182 |
img = np.ascontiguousarray(img)
|
183 |
|
|
|
184 |
return path, img, img0, self.cap
|
185 |
|
186 |
def new_video(self, path):
|
|
|
193 |
|
194 |
|
195 |
class LoadWebcam: # for inference
|
196 |
+
def __init__(self, pipe='0', img_size=640):
|
197 |
self.img_size = img_size
|
198 |
|
199 |
+
if pipe.isnumeric():
|
200 |
+
pipe = eval(pipe) # local camera
|
201 |
# pipe = 'rtsp://192.168.1.64/1' # IP camera
|
202 |
# pipe = 'rtsp://username:[email protected]/1' # IP camera with login
|
|
|
203 |
# pipe = 'http://wmccpinetop.axiscam.net/mjpg/video.mjpg' # IP golf camera
|
204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
self.pipe = pipe
|
206 |
self.cap = cv2.VideoCapture(pipe) # video capture object
|
207 |
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) # set buffer size
|
|
|
890 |
return labels
|
891 |
|
892 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
893 |
def create_folder(path='./new'):
|
894 |
# Create folder
|
895 |
if os.path.exists(path):
|
utils/evolve.sh
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
#!/bin/bash
|
2 |
-
# Hyperparameter evolution commands (avoids CUDA memory leakage issues)
|
3 |
-
# Replaces train.py python generations 'for' loop with a bash 'for' loop
|
4 |
-
|
5 |
-
# Start on 4-GPU machine
|
6 |
-
#for i in 0 1 2 3; do
|
7 |
-
# t=ultralytics/yolov5:evolve && sudo docker pull $t && sudo docker run -d --ipc=host --gpus all -v "$(pwd)"/VOC:/usr/src/VOC $t bash utils/evolve.sh $i
|
8 |
-
# sleep 60 # avoid simultaneous evolve.txt read/write
|
9 |
-
#done
|
10 |
-
|
11 |
-
# Hyperparameter evolution commands
|
12 |
-
while true; do
|
13 |
-
# python train.py --batch 64 --weights yolov5m.pt --data voc.yaml --img 512 --epochs 50 --evolve --bucket ult/evolve/voc --device $1
|
14 |
-
python train.py --batch 40 --weights yolov5m.pt --data coco.yaml --img 640 --epochs 30 --evolve --bucket ult/evolve/coco --device $1
|
15 |
-
done
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/general.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import glob
|
2 |
import logging
|
3 |
import math
|
@@ -5,27 +7,19 @@ import os
|
|
5 |
import platform
|
6 |
import random
|
7 |
import re
|
8 |
-
import shutil
|
9 |
import subprocess
|
10 |
import time
|
11 |
-
from contextlib import contextmanager
|
12 |
-
from copy import copy
|
13 |
from pathlib import Path
|
14 |
|
15 |
import cv2
|
16 |
import matplotlib
|
17 |
-
import matplotlib.pyplot as plt
|
18 |
import numpy as np
|
19 |
import torch
|
20 |
-
import torch.nn as nn
|
21 |
import yaml
|
22 |
-
from PIL import Image
|
23 |
-
from scipy.cluster.vq import kmeans
|
24 |
-
from scipy.signal import butter, filtfilt
|
25 |
-
from tqdm import tqdm
|
26 |
|
27 |
from utils.google_utils import gsutil_getsize
|
28 |
-
from utils.
|
|
|
29 |
|
30 |
# Set printoptions
|
31 |
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
@@ -36,18 +30,6 @@ matplotlib.rc('font', **{'size': 11})
|
|
36 |
cv2.setNumThreads(0)
|
37 |
|
38 |
|
39 |
-
@contextmanager
|
40 |
-
def torch_distributed_zero_first(local_rank: int):
|
41 |
-
"""
|
42 |
-
Decorator to make all processes in distributed training wait for each local_master to do something.
|
43 |
-
"""
|
44 |
-
if local_rank not in [-1, 0]:
|
45 |
-
torch.distributed.barrier()
|
46 |
-
yield
|
47 |
-
if local_rank == 0:
|
48 |
-
torch.distributed.barrier()
|
49 |
-
|
50 |
-
|
51 |
def set_logging(rank=-1):
|
52 |
logging.basicConfig(
|
53 |
format="%(message)s",
|
@@ -82,51 +64,6 @@ def check_img_size(img_size, s=32):
|
|
82 |
return new_size
|
83 |
|
84 |
|
85 |
-
def check_anchors(dataset, model, thr=4.0, imgsz=640):
|
86 |
-
# Check anchor fit to data, recompute if necessary
|
87 |
-
print('\nAnalyzing anchors... ', end='')
|
88 |
-
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect()
|
89 |
-
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
90 |
-
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
|
91 |
-
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
|
92 |
-
|
93 |
-
def metric(k): # compute metric
|
94 |
-
r = wh[:, None] / k[None]
|
95 |
-
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
96 |
-
best = x.max(1)[0] # best_x
|
97 |
-
aat = (x > 1. / thr).float().sum(1).mean() # anchors above threshold
|
98 |
-
bpr = (best > 1. / thr).float().mean() # best possible recall
|
99 |
-
return bpr, aat
|
100 |
-
|
101 |
-
bpr, aat = metric(m.anchor_grid.clone().cpu().view(-1, 2))
|
102 |
-
print('anchors/target = %.2f, Best Possible Recall (BPR) = %.4f' % (aat, bpr), end='')
|
103 |
-
if bpr < 0.98: # threshold to recompute
|
104 |
-
print('. Attempting to generate improved anchors, please wait...' % bpr)
|
105 |
-
na = m.anchor_grid.numel() // 2 # number of anchors
|
106 |
-
new_anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
|
107 |
-
new_bpr = metric(new_anchors.reshape(-1, 2))[0]
|
108 |
-
if new_bpr > bpr: # replace anchors
|
109 |
-
new_anchors = torch.tensor(new_anchors, device=m.anchors.device).type_as(m.anchors)
|
110 |
-
m.anchor_grid[:] = new_anchors.clone().view_as(m.anchor_grid) # for inference
|
111 |
-
m.anchors[:] = new_anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1) # loss
|
112 |
-
check_anchor_order(m)
|
113 |
-
print('New anchors saved to model. Update model *.yaml to use these anchors in the future.')
|
114 |
-
else:
|
115 |
-
print('Original anchors better than new anchors. Proceeding with original anchors.')
|
116 |
-
print('') # newline
|
117 |
-
|
118 |
-
|
119 |
-
def check_anchor_order(m):
|
120 |
-
# Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary
|
121 |
-
a = m.anchor_grid.prod(-1).view(-1) # anchor area
|
122 |
-
da = a[-1] - a[0] # delta a
|
123 |
-
ds = m.stride[-1] - m.stride[0] # delta s
|
124 |
-
if da.sign() != ds.sign(): # same order
|
125 |
-
print('Reversing anchor order')
|
126 |
-
m.anchors[:] = m.anchors.flip(0)
|
127 |
-
m.anchor_grid[:] = m.anchor_grid.flip(0)
|
128 |
-
|
129 |
-
|
130 |
def check_file(file):
|
131 |
# Search for file if not found
|
132 |
if os.path.isfile(file) or file == '':
|
@@ -139,7 +76,7 @@ def check_file(file):
|
|
139 |
|
140 |
|
141 |
def check_dataset(dict):
|
142 |
-
# Download dataset if not found
|
143 |
val, s = dict.get('val'), dict.get('download')
|
144 |
if val and len(val):
|
145 |
val = [os.path.abspath(x) for x in (val if isinstance(val, list) else [val])] # val path
|
@@ -247,106 +184,6 @@ def clip_coords(boxes, img_shape):
|
|
247 |
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
248 |
|
249 |
|
250 |
-
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, fname='precision-recall_curve.png'):
|
251 |
-
""" Compute the average precision, given the recall and precision curves.
|
252 |
-
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
253 |
-
# Arguments
|
254 |
-
tp: True positives (nparray, nx1 or nx10).
|
255 |
-
conf: Objectness value from 0-1 (nparray).
|
256 |
-
pred_cls: Predicted object classes (nparray).
|
257 |
-
target_cls: True object classes (nparray).
|
258 |
-
plot: Plot precision-recall curve at [email protected]
|
259 |
-
fname: Plot filename
|
260 |
-
# Returns
|
261 |
-
The average precision as computed in py-faster-rcnn.
|
262 |
-
"""
|
263 |
-
|
264 |
-
# Sort by objectness
|
265 |
-
i = np.argsort(-conf)
|
266 |
-
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
267 |
-
|
268 |
-
# Find unique classes
|
269 |
-
unique_classes = np.unique(target_cls)
|
270 |
-
|
271 |
-
# Create Precision-Recall curve and compute AP for each class
|
272 |
-
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
273 |
-
pr_score = 0.1 # score to evaluate P and R https://github.com/ultralytics/yolov3/issues/898
|
274 |
-
s = [unique_classes.shape[0], tp.shape[1]] # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95)
|
275 |
-
ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s)
|
276 |
-
for ci, c in enumerate(unique_classes):
|
277 |
-
i = pred_cls == c
|
278 |
-
n_l = (target_cls == c).sum() # number of labels
|
279 |
-
n_p = i.sum() # number of predictions
|
280 |
-
|
281 |
-
if n_p == 0 or n_l == 0:
|
282 |
-
continue
|
283 |
-
else:
|
284 |
-
# Accumulate FPs and TPs
|
285 |
-
fpc = (1 - tp[i]).cumsum(0)
|
286 |
-
tpc = tp[i].cumsum(0)
|
287 |
-
|
288 |
-
# Recall
|
289 |
-
recall = tpc / (n_l + 1e-16) # recall curve
|
290 |
-
r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) # r at pr_score, negative x, xp because xp decreases
|
291 |
-
|
292 |
-
# Precision
|
293 |
-
precision = tpc / (tpc + fpc) # precision curve
|
294 |
-
p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0]) # p at pr_score
|
295 |
-
|
296 |
-
# AP from recall-precision curve
|
297 |
-
for j in range(tp.shape[1]):
|
298 |
-
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])
|
299 |
-
if j == 0:
|
300 |
-
py.append(np.interp(px, mrec, mpre)) # precision at [email protected]
|
301 |
-
|
302 |
-
# Compute F1 score (harmonic mean of precision and recall)
|
303 |
-
f1 = 2 * p * r / (p + r + 1e-16)
|
304 |
-
|
305 |
-
if plot:
|
306 |
-
py = np.stack(py, axis=1)
|
307 |
-
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
|
308 |
-
ax.plot(px, py, linewidth=0.5, color='grey') # plot(recall, precision)
|
309 |
-
ax.plot(px, py.mean(1), linewidth=2, color='blue', label='all classes %.3f [email protected]' % ap[:, 0].mean())
|
310 |
-
ax.set_xlabel('Recall')
|
311 |
-
ax.set_ylabel('Precision')
|
312 |
-
ax.set_xlim(0, 1)
|
313 |
-
ax.set_ylim(0, 1)
|
314 |
-
plt.legend()
|
315 |
-
fig.tight_layout()
|
316 |
-
fig.savefig(fname, dpi=200)
|
317 |
-
|
318 |
-
return p, r, ap, f1, unique_classes.astype('int32')
|
319 |
-
|
320 |
-
|
321 |
-
def compute_ap(recall, precision):
|
322 |
-
""" Compute the average precision, given the recall and precision curves.
|
323 |
-
Source: https://github.com/rbgirshick/py-faster-rcnn.
|
324 |
-
# Arguments
|
325 |
-
recall: The recall curve (list).
|
326 |
-
precision: The precision curve (list).
|
327 |
-
# Returns
|
328 |
-
The average precision as computed in py-faster-rcnn.
|
329 |
-
"""
|
330 |
-
|
331 |
-
# Append sentinel values to beginning and end
|
332 |
-
mrec = recall # np.concatenate(([0.], recall, [recall[-1] + 1E-3]))
|
333 |
-
mpre = precision # np.concatenate(([0.], precision, [0.]))
|
334 |
-
|
335 |
-
# Compute the precision envelope
|
336 |
-
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
337 |
-
|
338 |
-
# Integrate area under curve
|
339 |
-
method = 'interp' # methods: 'continuous', 'interp'
|
340 |
-
if method == 'interp':
|
341 |
-
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
342 |
-
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
343 |
-
else: # 'continuous'
|
344 |
-
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
|
345 |
-
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
346 |
-
|
347 |
-
return ap, mpre, mrec
|
348 |
-
|
349 |
-
|
350 |
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
|
351 |
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
352 |
box2 = box2.T
|
@@ -425,178 +262,6 @@ def wh_iou(wh1, wh2):
|
|
425 |
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
426 |
|
427 |
|
428 |
-
class FocalLoss(nn.Module):
|
429 |
-
# Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
|
430 |
-
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
|
431 |
-
super(FocalLoss, self).__init__()
|
432 |
-
self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
|
433 |
-
self.gamma = gamma
|
434 |
-
self.alpha = alpha
|
435 |
-
self.reduction = loss_fcn.reduction
|
436 |
-
self.loss_fcn.reduction = 'none' # required to apply FL to each element
|
437 |
-
|
438 |
-
def forward(self, pred, true):
|
439 |
-
loss = self.loss_fcn(pred, true)
|
440 |
-
# p_t = torch.exp(-loss)
|
441 |
-
# loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
|
442 |
-
|
443 |
-
# TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
|
444 |
-
pred_prob = torch.sigmoid(pred) # prob from logits
|
445 |
-
p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
|
446 |
-
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
|
447 |
-
modulating_factor = (1.0 - p_t) ** self.gamma
|
448 |
-
loss *= alpha_factor * modulating_factor
|
449 |
-
|
450 |
-
if self.reduction == 'mean':
|
451 |
-
return loss.mean()
|
452 |
-
elif self.reduction == 'sum':
|
453 |
-
return loss.sum()
|
454 |
-
else: # 'none'
|
455 |
-
return loss
|
456 |
-
|
457 |
-
|
458 |
-
def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
|
459 |
-
# return positive, negative label smoothing BCE targets
|
460 |
-
return 1.0 - 0.5 * eps, 0.5 * eps
|
461 |
-
|
462 |
-
|
463 |
-
class BCEBlurWithLogitsLoss(nn.Module):
|
464 |
-
# BCEwithLogitLoss() with reduced missing label effects.
|
465 |
-
def __init__(self, alpha=0.05):
|
466 |
-
super(BCEBlurWithLogitsLoss, self).__init__()
|
467 |
-
self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss()
|
468 |
-
self.alpha = alpha
|
469 |
-
|
470 |
-
def forward(self, pred, true):
|
471 |
-
loss = self.loss_fcn(pred, true)
|
472 |
-
pred = torch.sigmoid(pred) # prob from logits
|
473 |
-
dx = pred - true # reduce only missing label effects
|
474 |
-
# dx = (pred - true).abs() # reduce missing label and false label effects
|
475 |
-
alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
|
476 |
-
loss *= alpha_factor
|
477 |
-
return loss.mean()
|
478 |
-
|
479 |
-
|
480 |
-
def compute_loss(p, targets, model): # predictions, targets, model
|
481 |
-
device = targets.device
|
482 |
-
lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
|
483 |
-
tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets
|
484 |
-
h = model.hyp # hyperparameters
|
485 |
-
|
486 |
-
# Define criteria
|
487 |
-
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device)
|
488 |
-
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device)
|
489 |
-
|
490 |
-
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
|
491 |
-
cp, cn = smooth_BCE(eps=0.0)
|
492 |
-
|
493 |
-
# Focal loss
|
494 |
-
g = h['fl_gamma'] # focal loss gamma
|
495 |
-
if g > 0:
|
496 |
-
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
|
497 |
-
|
498 |
-
# Losses
|
499 |
-
nt = 0 # number of targets
|
500 |
-
np = len(p) # number of outputs
|
501 |
-
balance = [4.0, 1.0, 0.4] if np == 3 else [4.0, 1.0, 0.4, 0.1] # P3-5 or P3-6
|
502 |
-
for i, pi in enumerate(p): # layer index, layer predictions
|
503 |
-
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
|
504 |
-
tobj = torch.zeros_like(pi[..., 0], device=device) # target obj
|
505 |
-
|
506 |
-
n = b.shape[0] # number of targets
|
507 |
-
if n:
|
508 |
-
nt += n # cumulative targets
|
509 |
-
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
|
510 |
-
|
511 |
-
# Regression
|
512 |
-
pxy = ps[:, :2].sigmoid() * 2. - 0.5
|
513 |
-
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
|
514 |
-
pbox = torch.cat((pxy, pwh), 1).to(device) # predicted box
|
515 |
-
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target)
|
516 |
-
lbox += (1.0 - iou).mean() # iou loss
|
517 |
-
|
518 |
-
# Objectness
|
519 |
-
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio
|
520 |
-
|
521 |
-
# Classification
|
522 |
-
if model.nc > 1: # cls loss (only if multiple classes)
|
523 |
-
t = torch.full_like(ps[:, 5:], cn, device=device) # targets
|
524 |
-
t[range(n), tcls[i]] = cp
|
525 |
-
lcls += BCEcls(ps[:, 5:], t) # BCE
|
526 |
-
|
527 |
-
# Append targets to text file
|
528 |
-
# with open('targets.txt', 'a') as file:
|
529 |
-
# [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
|
530 |
-
|
531 |
-
lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss
|
532 |
-
|
533 |
-
s = 3 / np # output count scaling
|
534 |
-
lbox *= h['box'] * s
|
535 |
-
lobj *= h['obj'] * s * (1.4 if np == 4 else 1.)
|
536 |
-
lcls *= h['cls'] * s
|
537 |
-
bs = tobj.shape[0] # batch size
|
538 |
-
|
539 |
-
loss = lbox + lobj + lcls
|
540 |
-
return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
|
541 |
-
|
542 |
-
|
543 |
-
def build_targets(p, targets, model):
|
544 |
-
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
|
545 |
-
det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module
|
546 |
-
na, nt = det.na, targets.shape[0] # number of anchors, targets
|
547 |
-
tcls, tbox, indices, anch = [], [], [], []
|
548 |
-
gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
|
549 |
-
ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
|
550 |
-
targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices
|
551 |
-
|
552 |
-
g = 0.5 # bias
|
553 |
-
off = torch.tensor([[0, 0],
|
554 |
-
[1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
|
555 |
-
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
|
556 |
-
], device=targets.device).float() * g # offsets
|
557 |
-
|
558 |
-
for i in range(det.nl):
|
559 |
-
anchors = det.anchors[i]
|
560 |
-
gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
|
561 |
-
|
562 |
-
# Match targets to anchors
|
563 |
-
t = targets * gain
|
564 |
-
if nt:
|
565 |
-
# Matches
|
566 |
-
r = t[:, :, 4:6] / anchors[:, None] # wh ratio
|
567 |
-
j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare
|
568 |
-
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
|
569 |
-
t = t[j] # filter
|
570 |
-
|
571 |
-
# Offsets
|
572 |
-
gxy = t[:, 2:4] # grid xy
|
573 |
-
gxi = gain[[2, 3]] - gxy # inverse
|
574 |
-
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
|
575 |
-
l, m = ((gxi % 1. < g) & (gxi > 1.)).T
|
576 |
-
j = torch.stack((torch.ones_like(j), j, k, l, m))
|
577 |
-
t = t.repeat((5, 1, 1))[j]
|
578 |
-
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
|
579 |
-
else:
|
580 |
-
t = targets[0]
|
581 |
-
offsets = 0
|
582 |
-
|
583 |
-
# Define
|
584 |
-
b, c = t[:, :2].long().T # image, class
|
585 |
-
gxy = t[:, 2:4] # grid xy
|
586 |
-
gwh = t[:, 4:6] # grid wh
|
587 |
-
gij = (gxy - offsets).long()
|
588 |
-
gi, gj = gij.T # grid xy indices
|
589 |
-
|
590 |
-
# Append
|
591 |
-
a = t[:, 6].long() # anchor indices
|
592 |
-
indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices
|
593 |
-
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
|
594 |
-
anch.append(anchors[a]) # anchors
|
595 |
-
tcls.append(c) # class
|
596 |
-
|
597 |
-
return tcls, tbox, indices, anch
|
598 |
-
|
599 |
-
|
600 |
def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False, classes=None, agnostic=False):
|
601 |
"""Performs Non-Maximum Suppression (NMS) on inference results
|
602 |
|
@@ -662,15 +327,12 @@ def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False,
|
|
662 |
if i.shape[0] > max_det: # limit detections
|
663 |
i = i[:max_det]
|
664 |
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
except: # possible CUDA error https://github.com/ultralytics/yolov3/issues/1139
|
672 |
-
print(x, i, x.shape, i.shape)
|
673 |
-
pass
|
674 |
|
675 |
output[xi] = x[i]
|
676 |
if (time.time() - t) > time_limit:
|
@@ -693,170 +355,6 @@ def strip_optimizer(f='weights/best.pt', s=''): # from utils.general import *;
|
|
693 |
print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
|
694 |
|
695 |
|
696 |
-
def coco_class_count(path='../coco/labels/train2014/'):
|
697 |
-
# Histogram of occurrences per class
|
698 |
-
nc = 80 # number classes
|
699 |
-
x = np.zeros(nc, dtype='int32')
|
700 |
-
files = sorted(glob.glob('%s/*.*' % path))
|
701 |
-
for i, file in enumerate(files):
|
702 |
-
labels = np.loadtxt(file, dtype=np.float32).reshape(-1, 5)
|
703 |
-
x += np.bincount(labels[:, 0].astype('int32'), minlength=nc)
|
704 |
-
print(i, len(files))
|
705 |
-
|
706 |
-
|
707 |
-
def coco_only_people(path='../coco/labels/train2017/'): # from utils.general import *; coco_only_people()
|
708 |
-
# Find images with only people
|
709 |
-
files = sorted(glob.glob('%s/*.*' % path))
|
710 |
-
for i, file in enumerate(files):
|
711 |
-
labels = np.loadtxt(file, dtype=np.float32).reshape(-1, 5)
|
712 |
-
if all(labels[:, 0] == 0):
|
713 |
-
print(labels.shape[0], file)
|
714 |
-
|
715 |
-
|
716 |
-
def crop_images_random(path='../images/', scale=0.50): # from utils.general import *; crop_images_random()
|
717 |
-
# crops images into random squares up to scale fraction
|
718 |
-
# WARNING: overwrites images!
|
719 |
-
for file in tqdm(sorted(glob.glob('%s/*.*' % path))):
|
720 |
-
img = cv2.imread(file) # BGR
|
721 |
-
if img is not None:
|
722 |
-
h, w = img.shape[:2]
|
723 |
-
|
724 |
-
# create random mask
|
725 |
-
a = 30 # minimum size (pixels)
|
726 |
-
mask_h = random.randint(a, int(max(a, h * scale))) # mask height
|
727 |
-
mask_w = mask_h # mask width
|
728 |
-
|
729 |
-
# box
|
730 |
-
xmin = max(0, random.randint(0, w) - mask_w // 2)
|
731 |
-
ymin = max(0, random.randint(0, h) - mask_h // 2)
|
732 |
-
xmax = min(w, xmin + mask_w)
|
733 |
-
ymax = min(h, ymin + mask_h)
|
734 |
-
|
735 |
-
# apply random color mask
|
736 |
-
cv2.imwrite(file, img[ymin:ymax, xmin:xmax])
|
737 |
-
|
738 |
-
|
739 |
-
def coco_single_class_labels(path='../coco/labels/train2014/', label_class=43):
|
740 |
-
# Makes single-class coco datasets. from utils.general import *; coco_single_class_labels()
|
741 |
-
if os.path.exists('new/'):
|
742 |
-
shutil.rmtree('new/') # delete output folder
|
743 |
-
os.makedirs('new/') # make new output folder
|
744 |
-
os.makedirs('new/labels/')
|
745 |
-
os.makedirs('new/images/')
|
746 |
-
for file in tqdm(sorted(glob.glob('%s/*.*' % path))):
|
747 |
-
with open(file, 'r') as f:
|
748 |
-
labels = np.array([x.split() for x in f.read().splitlines()], dtype=np.float32)
|
749 |
-
i = labels[:, 0] == label_class
|
750 |
-
if any(i):
|
751 |
-
img_file = file.replace('labels', 'images').replace('txt', 'jpg')
|
752 |
-
labels[:, 0] = 0 # reset class to 0
|
753 |
-
with open('new/images.txt', 'a') as f: # add image to dataset list
|
754 |
-
f.write(img_file + '\n')
|
755 |
-
with open('new/labels/' + Path(file).name, 'a') as f: # write label
|
756 |
-
for l in labels[i]:
|
757 |
-
f.write('%g %.6f %.6f %.6f %.6f\n' % tuple(l))
|
758 |
-
shutil.copyfile(src=img_file, dst='new/images/' + Path(file).name.replace('txt', 'jpg')) # copy images
|
759 |
-
|
760 |
-
|
761 |
-
def kmean_anchors(path='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
|
762 |
-
""" Creates kmeans-evolved anchors from training dataset
|
763 |
-
|
764 |
-
Arguments:
|
765 |
-
path: path to dataset *.yaml, or a loaded dataset
|
766 |
-
n: number of anchors
|
767 |
-
img_size: image size used for training
|
768 |
-
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
|
769 |
-
gen: generations to evolve anchors using genetic algorithm
|
770 |
-
|
771 |
-
Return:
|
772 |
-
k: kmeans evolved anchors
|
773 |
-
|
774 |
-
Usage:
|
775 |
-
from utils.general import *; _ = kmean_anchors()
|
776 |
-
"""
|
777 |
-
thr = 1. / thr
|
778 |
-
|
779 |
-
def metric(k, wh): # compute metrics
|
780 |
-
r = wh[:, None] / k[None]
|
781 |
-
x = torch.min(r, 1. / r).min(2)[0] # ratio metric
|
782 |
-
# x = wh_iou(wh, torch.tensor(k)) # iou metric
|
783 |
-
return x, x.max(1)[0] # x, best_x
|
784 |
-
|
785 |
-
def fitness(k): # mutation fitness
|
786 |
-
_, best = metric(torch.tensor(k, dtype=torch.float32), wh)
|
787 |
-
return (best * (best > thr).float()).mean() # fitness
|
788 |
-
|
789 |
-
def print_results(k):
|
790 |
-
k = k[np.argsort(k.prod(1))] # sort small to large
|
791 |
-
x, best = metric(k, wh0)
|
792 |
-
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
|
793 |
-
print('thr=%.2f: %.4f best possible recall, %.2f anchors past thr' % (thr, bpr, aat))
|
794 |
-
print('n=%g, img_size=%s, metric_all=%.3f/%.3f-mean/best, past_thr=%.3f-mean: ' %
|
795 |
-
(n, img_size, x.mean(), best.mean(), x[x > thr].mean()), end='')
|
796 |
-
for i, x in enumerate(k):
|
797 |
-
print('%i,%i' % (round(x[0]), round(x[1])), end=', ' if i < len(k) - 1 else '\n') # use in *.cfg
|
798 |
-
return k
|
799 |
-
|
800 |
-
if isinstance(path, str): # *.yaml file
|
801 |
-
with open(path) as f:
|
802 |
-
data_dict = yaml.load(f, Loader=yaml.FullLoader) # model dict
|
803 |
-
from utils.datasets import LoadImagesAndLabels
|
804 |
-
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
|
805 |
-
else:
|
806 |
-
dataset = path # dataset
|
807 |
-
|
808 |
-
# Get label wh
|
809 |
-
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
|
810 |
-
wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)]) # wh
|
811 |
-
|
812 |
-
# Filter
|
813 |
-
i = (wh0 < 3.0).any(1).sum()
|
814 |
-
if i:
|
815 |
-
print('WARNING: Extremely small objects found. '
|
816 |
-
'%g of %g labels are < 3 pixels in width or height.' % (i, len(wh0)))
|
817 |
-
wh = wh0[(wh0 >= 2.0).any(1)] # filter > 2 pixels
|
818 |
-
|
819 |
-
# Kmeans calculation
|
820 |
-
print('Running kmeans for %g anchors on %g points...' % (n, len(wh)))
|
821 |
-
s = wh.std(0) # sigmas for whitening
|
822 |
-
k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
|
823 |
-
k *= s
|
824 |
-
wh = torch.tensor(wh, dtype=torch.float32) # filtered
|
825 |
-
wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered
|
826 |
-
k = print_results(k)
|
827 |
-
|
828 |
-
# Plot
|
829 |
-
# k, d = [None] * 20, [None] * 20
|
830 |
-
# for i in tqdm(range(1, 21)):
|
831 |
-
# k[i-1], d[i-1] = kmeans(wh / s, i) # points, mean distance
|
832 |
-
# fig, ax = plt.subplots(1, 2, figsize=(14, 7))
|
833 |
-
# ax = ax.ravel()
|
834 |
-
# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
|
835 |
-
# fig, ax = plt.subplots(1, 2, figsize=(14, 7)) # plot wh
|
836 |
-
# ax[0].hist(wh[wh[:, 0]<100, 0],400)
|
837 |
-
# ax[1].hist(wh[wh[:, 1]<100, 1],400)
|
838 |
-
# fig.tight_layout()
|
839 |
-
# fig.savefig('wh.png', dpi=200)
|
840 |
-
|
841 |
-
# Evolve
|
842 |
-
npr = np.random
|
843 |
-
f, sh, mp, s = fitness(k), k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
|
844 |
-
pbar = tqdm(range(gen), desc='Evolving anchors with Genetic Algorithm') # progress bar
|
845 |
-
for _ in pbar:
|
846 |
-
v = np.ones(sh)
|
847 |
-
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
|
848 |
-
v = ((npr.random(sh) < mp) * npr.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
|
849 |
-
kg = (k.copy() * v).clip(min=2.0)
|
850 |
-
fg = fitness(kg)
|
851 |
-
if fg > f:
|
852 |
-
f, k = fg, kg.copy()
|
853 |
-
pbar.desc = 'Evolving anchors with Genetic Algorithm: fitness = %.4f' % f
|
854 |
-
if verbose:
|
855 |
-
print_results(k)
|
856 |
-
|
857 |
-
return print_results(k)
|
858 |
-
|
859 |
-
|
860 |
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
861 |
# Print mutation results to evolve.txt (for use with train.py --evolve)
|
862 |
a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
|
@@ -923,34 +421,6 @@ def apply_classifier(x, model, img, im0):
|
|
923 |
return x
|
924 |
|
925 |
|
926 |
-
def fitness(x):
|
927 |
-
# Returns fitness (for use with results.txt or evolve.txt)
|
928 |
-
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, [email protected], [email protected]:0.95]
|
929 |
-
return (x[:, :4] * w).sum(1)
|
930 |
-
|
931 |
-
|
932 |
-
def output_to_target(output, width, height):
|
933 |
-
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
934 |
-
if isinstance(output, torch.Tensor):
|
935 |
-
output = output.cpu().numpy()
|
936 |
-
|
937 |
-
targets = []
|
938 |
-
for i, o in enumerate(output):
|
939 |
-
if o is not None:
|
940 |
-
for pred in o:
|
941 |
-
box = pred[:4]
|
942 |
-
w = (box[2] - box[0]) / width
|
943 |
-
h = (box[3] - box[1]) / height
|
944 |
-
x = box[0] / width + w / 2
|
945 |
-
y = box[1] / height + h / 2
|
946 |
-
conf = pred[4]
|
947 |
-
cls = int(pred[5])
|
948 |
-
|
949 |
-
targets.append([i, cls, x, y, w, h, conf])
|
950 |
-
|
951 |
-
return np.array(targets)
|
952 |
-
|
953 |
-
|
954 |
def increment_path(path, exist_ok=True, sep=''):
|
955 |
# Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
|
956 |
path = Path(path) # os-agnostic
|
@@ -962,339 +432,3 @@ def increment_path(path, exist_ok=True, sep=''):
|
|
962 |
i = [int(m.groups()[0]) for m in matches if m] # indices
|
963 |
n = max(i) + 1 if i else 2 # increment number
|
964 |
return f"{path}{sep}{n}" # update path
|
965 |
-
|
966 |
-
|
967 |
-
# Plotting functions ---------------------------------------------------------------------------------------------------
|
968 |
-
def hist2d(x, y, n=100):
|
969 |
-
# 2d histogram used in labels.png and evolve.png
|
970 |
-
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
971 |
-
hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
|
972 |
-
xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
|
973 |
-
yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
|
974 |
-
return np.log(hist[xidx, yidx])
|
975 |
-
|
976 |
-
|
977 |
-
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
978 |
-
# https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
|
979 |
-
def butter_lowpass(cutoff, fs, order):
|
980 |
-
nyq = 0.5 * fs
|
981 |
-
normal_cutoff = cutoff / nyq
|
982 |
-
b, a = butter(order, normal_cutoff, btype='low', analog=False)
|
983 |
-
return b, a
|
984 |
-
|
985 |
-
b, a = butter_lowpass(cutoff, fs, order=order)
|
986 |
-
return filtfilt(b, a, data) # forward-backward filter
|
987 |
-
|
988 |
-
|
989 |
-
def plot_one_box(x, img, color=None, label=None, line_thickness=None):
|
990 |
-
# Plots one bounding box on image img
|
991 |
-
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
992 |
-
color = color or [random.randint(0, 255) for _ in range(3)]
|
993 |
-
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
994 |
-
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
995 |
-
if label:
|
996 |
-
tf = max(tl - 1, 1) # font thickness
|
997 |
-
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
998 |
-
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
999 |
-
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
1000 |
-
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
1001 |
-
|
1002 |
-
|
1003 |
-
def plot_wh_methods(): # from utils.general import *; plot_wh_methods()
|
1004 |
-
# Compares the two methods for width-height anchor multiplication
|
1005 |
-
# https://github.com/ultralytics/yolov3/issues/168
|
1006 |
-
x = np.arange(-4.0, 4.0, .1)
|
1007 |
-
ya = np.exp(x)
|
1008 |
-
yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
|
1009 |
-
|
1010 |
-
fig = plt.figure(figsize=(6, 3), dpi=150)
|
1011 |
-
plt.plot(x, ya, '.-', label='YOLOv3')
|
1012 |
-
plt.plot(x, yb ** 2, '.-', label='YOLOv5 ^2')
|
1013 |
-
plt.plot(x, yb ** 1.6, '.-', label='YOLOv5 ^1.6')
|
1014 |
-
plt.xlim(left=-4, right=4)
|
1015 |
-
plt.ylim(bottom=0, top=6)
|
1016 |
-
plt.xlabel('input')
|
1017 |
-
plt.ylabel('output')
|
1018 |
-
plt.grid()
|
1019 |
-
plt.legend()
|
1020 |
-
fig.tight_layout()
|
1021 |
-
fig.savefig('comparison.png', dpi=200)
|
1022 |
-
|
1023 |
-
|
1024 |
-
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
|
1025 |
-
tl = 3 # line thickness
|
1026 |
-
tf = max(tl - 1, 1) # font thickness
|
1027 |
-
|
1028 |
-
if isinstance(images, torch.Tensor):
|
1029 |
-
images = images.cpu().float().numpy()
|
1030 |
-
|
1031 |
-
if isinstance(targets, torch.Tensor):
|
1032 |
-
targets = targets.cpu().numpy()
|
1033 |
-
|
1034 |
-
# un-normalise
|
1035 |
-
if np.max(images[0]) <= 1:
|
1036 |
-
images *= 255
|
1037 |
-
|
1038 |
-
bs, _, h, w = images.shape # batch size, _, height, width
|
1039 |
-
bs = min(bs, max_subplots) # limit plot images
|
1040 |
-
ns = np.ceil(bs ** 0.5) # number of subplots (square)
|
1041 |
-
|
1042 |
-
# Check if we should resize
|
1043 |
-
scale_factor = max_size / max(h, w)
|
1044 |
-
if scale_factor < 1:
|
1045 |
-
h = math.ceil(scale_factor * h)
|
1046 |
-
w = math.ceil(scale_factor * w)
|
1047 |
-
|
1048 |
-
# Empty array for output
|
1049 |
-
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8)
|
1050 |
-
|
1051 |
-
# Fix class - colour map
|
1052 |
-
prop_cycle = plt.rcParams['axes.prop_cycle']
|
1053 |
-
# https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
|
1054 |
-
hex2rgb = lambda h: tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
1055 |
-
color_lut = [hex2rgb(h) for h in prop_cycle.by_key()['color']]
|
1056 |
-
|
1057 |
-
for i, img in enumerate(images):
|
1058 |
-
if i == max_subplots: # if last batch has fewer images than we expect
|
1059 |
-
break
|
1060 |
-
|
1061 |
-
block_x = int(w * (i // ns))
|
1062 |
-
block_y = int(h * (i % ns))
|
1063 |
-
|
1064 |
-
img = img.transpose(1, 2, 0)
|
1065 |
-
if scale_factor < 1:
|
1066 |
-
img = cv2.resize(img, (w, h))
|
1067 |
-
|
1068 |
-
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
|
1069 |
-
if len(targets) > 0:
|
1070 |
-
image_targets = targets[targets[:, 0] == i]
|
1071 |
-
boxes = xywh2xyxy(image_targets[:, 2:6]).T
|
1072 |
-
classes = image_targets[:, 1].astype('int')
|
1073 |
-
labels = image_targets.shape[1] == 6 # labels if no conf column
|
1074 |
-
conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred)
|
1075 |
-
|
1076 |
-
boxes[[0, 2]] *= w
|
1077 |
-
boxes[[0, 2]] += block_x
|
1078 |
-
boxes[[1, 3]] *= h
|
1079 |
-
boxes[[1, 3]] += block_y
|
1080 |
-
for j, box in enumerate(boxes.T):
|
1081 |
-
cls = int(classes[j])
|
1082 |
-
color = color_lut[cls % len(color_lut)]
|
1083 |
-
cls = names[cls] if names else cls
|
1084 |
-
if labels or conf[j] > 0.3: # 0.3 conf thresh
|
1085 |
-
label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j])
|
1086 |
-
plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
|
1087 |
-
|
1088 |
-
# Draw image filename labels
|
1089 |
-
if paths is not None:
|
1090 |
-
label = os.path.basename(paths[i])[:40] # trim to 40 char
|
1091 |
-
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
1092 |
-
cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
|
1093 |
-
lineType=cv2.LINE_AA)
|
1094 |
-
|
1095 |
-
# Image border
|
1096 |
-
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
|
1097 |
-
|
1098 |
-
if fname is not None:
|
1099 |
-
r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size
|
1100 |
-
mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA)
|
1101 |
-
# cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save
|
1102 |
-
Image.fromarray(mosaic).save(fname) # PIL save
|
1103 |
-
return mosaic
|
1104 |
-
|
1105 |
-
|
1106 |
-
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
|
1107 |
-
# Plot LR simulating training for full epochs
|
1108 |
-
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
|
1109 |
-
y = []
|
1110 |
-
for _ in range(epochs):
|
1111 |
-
scheduler.step()
|
1112 |
-
y.append(optimizer.param_groups[0]['lr'])
|
1113 |
-
plt.plot(y, '.-', label='LR')
|
1114 |
-
plt.xlabel('epoch')
|
1115 |
-
plt.ylabel('LR')
|
1116 |
-
plt.grid()
|
1117 |
-
plt.xlim(0, epochs)
|
1118 |
-
plt.ylim(0)
|
1119 |
-
plt.tight_layout()
|
1120 |
-
plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
|
1121 |
-
|
1122 |
-
|
1123 |
-
def plot_test_txt(): # from utils.general import *; plot_test()
|
1124 |
-
# Plot test.txt histograms
|
1125 |
-
x = np.loadtxt('test.txt', dtype=np.float32)
|
1126 |
-
box = xyxy2xywh(x[:, :4])
|
1127 |
-
cx, cy = box[:, 0], box[:, 1]
|
1128 |
-
|
1129 |
-
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
|
1130 |
-
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
|
1131 |
-
ax.set_aspect('equal')
|
1132 |
-
plt.savefig('hist2d.png', dpi=300)
|
1133 |
-
|
1134 |
-
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
|
1135 |
-
ax[0].hist(cx, bins=600)
|
1136 |
-
ax[1].hist(cy, bins=600)
|
1137 |
-
plt.savefig('hist1d.png', dpi=200)
|
1138 |
-
|
1139 |
-
|
1140 |
-
def plot_targets_txt(): # from utils.general import *; plot_targets_txt()
|
1141 |
-
# Plot targets.txt histograms
|
1142 |
-
x = np.loadtxt('targets.txt', dtype=np.float32).T
|
1143 |
-
s = ['x targets', 'y targets', 'width targets', 'height targets']
|
1144 |
-
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
1145 |
-
ax = ax.ravel()
|
1146 |
-
for i in range(4):
|
1147 |
-
ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
|
1148 |
-
ax[i].legend()
|
1149 |
-
ax[i].set_title(s[i])
|
1150 |
-
plt.savefig('targets.jpg', dpi=200)
|
1151 |
-
|
1152 |
-
|
1153 |
-
def plot_study_txt(f='study.txt', x=None): # from utils.general import *; plot_study_txt()
|
1154 |
-
# Plot study.txt generated by test.py
|
1155 |
-
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
1156 |
-
ax = ax.ravel()
|
1157 |
-
|
1158 |
-
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
1159 |
-
for f in ['study/study_coco_yolov5%s.txt' % x for x in ['s', 'm', 'l', 'x']]:
|
1160 |
-
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
1161 |
-
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
1162 |
-
s = ['P', 'R', '[email protected]', '[email protected]:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
1163 |
-
for i in range(7):
|
1164 |
-
ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
1165 |
-
ax[i].set_title(s[i])
|
1166 |
-
|
1167 |
-
j = y[3].argmax() + 1
|
1168 |
-
ax2.plot(y[6, :j], y[3, :j] * 1E2, '.-', linewidth=2, markersize=8,
|
1169 |
-
label=Path(f).stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
1170 |
-
|
1171 |
-
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
1172 |
-
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
1173 |
-
|
1174 |
-
ax2.grid()
|
1175 |
-
ax2.set_xlim(0, 30)
|
1176 |
-
ax2.set_ylim(28, 50)
|
1177 |
-
ax2.set_yticks(np.arange(30, 55, 5))
|
1178 |
-
ax2.set_xlabel('GPU Speed (ms/img)')
|
1179 |
-
ax2.set_ylabel('COCO AP val')
|
1180 |
-
ax2.legend(loc='lower right')
|
1181 |
-
plt.savefig('study_mAP_latency.png', dpi=300)
|
1182 |
-
plt.savefig(f.replace('.txt', '.png'), dpi=300)
|
1183 |
-
|
1184 |
-
|
1185 |
-
def plot_labels(labels, save_dir=''):
|
1186 |
-
# plot dataset labels
|
1187 |
-
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
1188 |
-
nc = int(c.max() + 1) # number of classes
|
1189 |
-
|
1190 |
-
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
1191 |
-
ax = ax.ravel()
|
1192 |
-
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
1193 |
-
ax[0].set_xlabel('classes')
|
1194 |
-
ax[1].scatter(b[0], b[1], c=hist2d(b[0], b[1], 90), cmap='jet')
|
1195 |
-
ax[1].set_xlabel('x')
|
1196 |
-
ax[1].set_ylabel('y')
|
1197 |
-
ax[2].scatter(b[2], b[3], c=hist2d(b[2], b[3], 90), cmap='jet')
|
1198 |
-
ax[2].set_xlabel('width')
|
1199 |
-
ax[2].set_ylabel('height')
|
1200 |
-
plt.savefig(Path(save_dir) / 'labels.png', dpi=200)
|
1201 |
-
plt.close()
|
1202 |
-
|
1203 |
-
# seaborn correlogram
|
1204 |
-
try:
|
1205 |
-
import seaborn as sns
|
1206 |
-
import pandas as pd
|
1207 |
-
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
1208 |
-
sns.pairplot(x, corner=True, diag_kind='hist', kind='scatter', markers='o',
|
1209 |
-
plot_kws=dict(s=3, edgecolor=None, linewidth=1, alpha=0.02),
|
1210 |
-
diag_kws=dict(bins=50))
|
1211 |
-
plt.savefig(Path(save_dir) / 'labels_correlogram.png', dpi=200)
|
1212 |
-
plt.close()
|
1213 |
-
except Exception as e:
|
1214 |
-
pass
|
1215 |
-
|
1216 |
-
|
1217 |
-
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.general import *; plot_evolution()
|
1218 |
-
# Plot hyperparameter evolution results in evolve.txt
|
1219 |
-
with open(yaml_file) as f:
|
1220 |
-
hyp = yaml.load(f, Loader=yaml.FullLoader)
|
1221 |
-
x = np.loadtxt('evolve.txt', ndmin=2)
|
1222 |
-
f = fitness(x)
|
1223 |
-
# weights = (f - f.min()) ** 2 # for weighted results
|
1224 |
-
plt.figure(figsize=(10, 12), tight_layout=True)
|
1225 |
-
matplotlib.rc('font', **{'size': 8})
|
1226 |
-
for i, (k, v) in enumerate(hyp.items()):
|
1227 |
-
y = x[:, i + 7]
|
1228 |
-
# mu = (y * weights).sum() / weights.sum() # best weighted result
|
1229 |
-
mu = y[f.argmax()] # best single result
|
1230 |
-
plt.subplot(6, 5, i + 1)
|
1231 |
-
plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
|
1232 |
-
plt.plot(mu, f.max(), 'k+', markersize=15)
|
1233 |
-
plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
|
1234 |
-
if i % 5 != 0:
|
1235 |
-
plt.yticks([])
|
1236 |
-
print('%15s: %.3g' % (k, mu))
|
1237 |
-
plt.savefig('evolve.png', dpi=200)
|
1238 |
-
print('\nPlot saved as evolve.png')
|
1239 |
-
|
1240 |
-
|
1241 |
-
def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_results_overlay()
|
1242 |
-
# Plot training 'results*.txt', overlaying train and val losses
|
1243 |
-
s = ['train', 'train', 'train', 'Precision', '[email protected]', 'val', 'val', 'val', 'Recall', '[email protected]:0.95'] # legends
|
1244 |
-
t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
|
1245 |
-
for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
|
1246 |
-
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
1247 |
-
n = results.shape[1] # number of rows
|
1248 |
-
x = range(start, min(stop, n) if stop else n)
|
1249 |
-
fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
|
1250 |
-
ax = ax.ravel()
|
1251 |
-
for i in range(5):
|
1252 |
-
for j in [i, i + 5]:
|
1253 |
-
y = results[j, x]
|
1254 |
-
ax[i].plot(x, y, marker='.', label=s[j])
|
1255 |
-
# y_smooth = butter_lowpass_filtfilt(y)
|
1256 |
-
# ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
|
1257 |
-
|
1258 |
-
ax[i].set_title(t[i])
|
1259 |
-
ax[i].legend()
|
1260 |
-
ax[i].set_ylabel(f) if i == 0 else None # add filename
|
1261 |
-
fig.savefig(f.replace('.txt', '.png'), dpi=200)
|
1262 |
-
|
1263 |
-
|
1264 |
-
def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''):
|
1265 |
-
# from utils.general import *; plot_results(save_dir='runs/train/exp0')
|
1266 |
-
# Plot training 'results*.txt' as seen in https://github.com/ultralytics/yolov5#reproduce-our-training
|
1267 |
-
fig, ax = plt.subplots(2, 5, figsize=(12, 6))
|
1268 |
-
ax = ax.ravel()
|
1269 |
-
s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall',
|
1270 |
-
'val Box', 'val Objectness', 'val Classification', '[email protected]', '[email protected]:0.95']
|
1271 |
-
if bucket:
|
1272 |
-
# os.system('rm -rf storage.googleapis.com')
|
1273 |
-
# files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
|
1274 |
-
files = ['results%g.txt' % x for x in id]
|
1275 |
-
c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
|
1276 |
-
os.system(c)
|
1277 |
-
else:
|
1278 |
-
files = glob.glob(str(Path(save_dir) / 'results*.txt')) + glob.glob('../../Downloads/results*.txt')
|
1279 |
-
assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)
|
1280 |
-
for fi, f in enumerate(files):
|
1281 |
-
try:
|
1282 |
-
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
1283 |
-
n = results.shape[1] # number of rows
|
1284 |
-
x = range(start, min(stop, n) if stop else n)
|
1285 |
-
for i in range(10):
|
1286 |
-
y = results[i, x]
|
1287 |
-
if i in [0, 1, 2, 5, 6, 7]:
|
1288 |
-
y[y == 0] = np.nan # don't show zero loss values
|
1289 |
-
# y /= y[0] # normalize
|
1290 |
-
label = labels[fi] if len(labels) else Path(f).stem
|
1291 |
-
ax[i].plot(x, y, marker='.', label=label, linewidth=1, markersize=6)
|
1292 |
-
ax[i].set_title(s[i])
|
1293 |
-
# if i in [5, 6, 7]: # share train and val loss y axes
|
1294 |
-
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
|
1295 |
-
except Exception as e:
|
1296 |
-
print('Warning: Plotting error for %s; %s' % (f, e))
|
1297 |
-
|
1298 |
-
fig.tight_layout()
|
1299 |
-
ax[1].legend()
|
1300 |
-
fig.savefig(Path(save_dir) / 'results.png', dpi=200)
|
|
|
1 |
+
# General utils
|
2 |
+
|
3 |
import glob
|
4 |
import logging
|
5 |
import math
|
|
|
7 |
import platform
|
8 |
import random
|
9 |
import re
|
|
|
10 |
import subprocess
|
11 |
import time
|
|
|
|
|
12 |
from pathlib import Path
|
13 |
|
14 |
import cv2
|
15 |
import matplotlib
|
|
|
16 |
import numpy as np
|
17 |
import torch
|
|
|
18 |
import yaml
|
|
|
|
|
|
|
|
|
19 |
|
20 |
from utils.google_utils import gsutil_getsize
|
21 |
+
from utils.metrics import fitness
|
22 |
+
from utils.torch_utils import init_torch_seeds
|
23 |
|
24 |
# Set printoptions
|
25 |
torch.set_printoptions(linewidth=320, precision=5, profile='long')
|
|
|
30 |
cv2.setNumThreads(0)
|
31 |
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
def set_logging(rank=-1):
|
34 |
logging.basicConfig(
|
35 |
format="%(message)s",
|
|
|
64 |
return new_size
|
65 |
|
66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
def check_file(file):
|
68 |
# Search for file if not found
|
69 |
if os.path.isfile(file) or file == '':
|
|
|
76 |
|
77 |
|
78 |
def check_dataset(dict):
|
79 |
+
# Download dataset if not found locally
|
80 |
val, s = dict.get('val'), dict.get('download')
|
81 |
if val and len(val):
|
82 |
val = [os.path.abspath(x) for x in (val if isinstance(val, list) else [val])] # val path
|
|
|
184 |
boxes[:, 3].clamp_(0, img_shape[0]) # y2
|
185 |
|
186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
|
188 |
# Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
|
189 |
box2 = box2.T
|
|
|
262 |
return inter / (wh1.prod(2) + wh2.prod(2) - inter) # iou = inter / (area1 + area2 - inter)
|
263 |
|
264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
265 |
def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False, classes=None, agnostic=False):
|
266 |
"""Performs Non-Maximum Suppression (NMS) on inference results
|
267 |
|
|
|
327 |
if i.shape[0] > max_det: # limit detections
|
328 |
i = i[:max_det]
|
329 |
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)
|
330 |
+
# update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
|
331 |
+
iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix
|
332 |
+
weights = iou * scores[None] # box weights
|
333 |
+
x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes
|
334 |
+
if redundant:
|
335 |
+
i = i[iou.sum(1) > 1] # require redundancy
|
|
|
|
|
|
|
336 |
|
337 |
output[xi] = x[i]
|
338 |
if (time.time() - t) > time_limit:
|
|
|
355 |
print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
|
356 |
|
357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
|
359 |
# Print mutation results to evolve.txt (for use with train.py --evolve)
|
360 |
a = '%10s' * len(hyp) % tuple(hyp.keys()) # hyperparam keys
|
|
|
421 |
return x
|
422 |
|
423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
424 |
def increment_path(path, exist_ok=True, sep=''):
|
425 |
# Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
|
426 |
path = Path(path) # os-agnostic
|
|
|
432 |
i = [int(m.groups()[0]) for m in matches if m] # indices
|
433 |
n = max(i) + 1 if i else 2 # increment number
|
434 |
return f"{path}{sep}{n}" # update path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/google_utils.py
CHANGED
@@ -1,6 +1,4 @@
|
|
1 |
-
#
|
2 |
-
# pip install --upgrade google-cloud-storage
|
3 |
-
# from google.cloud import storage
|
4 |
|
5 |
import os
|
6 |
import platform
|
|
|
1 |
+
# Google utils: https://cloud.google.com/storage/docs/reference/libraries
|
|
|
|
|
2 |
|
3 |
import os
|
4 |
import platform
|
utils/loss.py
ADDED
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Loss functions
|
2 |
+
|
3 |
+
import torch
|
4 |
+
import torch.nn as nn
|
5 |
+
|
6 |
+
from utils.general import bbox_iou
|
7 |
+
from utils.torch_utils import is_parallel
|
8 |
+
|
9 |
+
|
10 |
+
def smooth_BCE(eps=0.1): # https://github.com/ultralytics/yolov3/issues/238#issuecomment-598028441
|
11 |
+
# return positive, negative label smoothing BCE targets
|
12 |
+
return 1.0 - 0.5 * eps, 0.5 * eps
|
13 |
+
|
14 |
+
|
15 |
+
class BCEBlurWithLogitsLoss(nn.Module):
|
16 |
+
# BCEwithLogitLoss() with reduced missing label effects.
|
17 |
+
def __init__(self, alpha=0.05):
|
18 |
+
super(BCEBlurWithLogitsLoss, self).__init__()
|
19 |
+
self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss()
|
20 |
+
self.alpha = alpha
|
21 |
+
|
22 |
+
def forward(self, pred, true):
|
23 |
+
loss = self.loss_fcn(pred, true)
|
24 |
+
pred = torch.sigmoid(pred) # prob from logits
|
25 |
+
dx = pred - true # reduce only missing label effects
|
26 |
+
# dx = (pred - true).abs() # reduce missing label and false label effects
|
27 |
+
alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4))
|
28 |
+
loss *= alpha_factor
|
29 |
+
return loss.mean()
|
30 |
+
|
31 |
+
|
32 |
+
class FocalLoss(nn.Module):
|
33 |
+
# Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
|
34 |
+
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
|
35 |
+
super(FocalLoss, self).__init__()
|
36 |
+
self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
|
37 |
+
self.gamma = gamma
|
38 |
+
self.alpha = alpha
|
39 |
+
self.reduction = loss_fcn.reduction
|
40 |
+
self.loss_fcn.reduction = 'none' # required to apply FL to each element
|
41 |
+
|
42 |
+
def forward(self, pred, true):
|
43 |
+
loss = self.loss_fcn(pred, true)
|
44 |
+
# p_t = torch.exp(-loss)
|
45 |
+
# loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
|
46 |
+
|
47 |
+
# TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
|
48 |
+
pred_prob = torch.sigmoid(pred) # prob from logits
|
49 |
+
p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
|
50 |
+
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
|
51 |
+
modulating_factor = (1.0 - p_t) ** self.gamma
|
52 |
+
loss *= alpha_factor * modulating_factor
|
53 |
+
|
54 |
+
if self.reduction == 'mean':
|
55 |
+
return loss.mean()
|
56 |
+
elif self.reduction == 'sum':
|
57 |
+
return loss.sum()
|
58 |
+
else: # 'none'
|
59 |
+
return loss
|
60 |
+
|
61 |
+
|
62 |
+
def compute_loss(p, targets, model): # predictions, targets, model
|
63 |
+
device = targets.device
|
64 |
+
lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
|
65 |
+
tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets
|
66 |
+
h = model.hyp # hyperparameters
|
67 |
+
|
68 |
+
# Define criteria
|
69 |
+
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device)
|
70 |
+
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device)
|
71 |
+
|
72 |
+
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
|
73 |
+
cp, cn = smooth_BCE(eps=0.0)
|
74 |
+
|
75 |
+
# Focal loss
|
76 |
+
g = h['fl_gamma'] # focal loss gamma
|
77 |
+
if g > 0:
|
78 |
+
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
|
79 |
+
|
80 |
+
# Losses
|
81 |
+
nt = 0 # number of targets
|
82 |
+
no = len(p) # number of outputs
|
83 |
+
balance = [4.0, 1.0, 0.4] if no == 3 else [4.0, 1.0, 0.4, 0.1] # P3-5 or P3-6
|
84 |
+
for i, pi in enumerate(p): # layer index, layer predictions
|
85 |
+
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
|
86 |
+
tobj = torch.zeros_like(pi[..., 0], device=device) # target obj
|
87 |
+
|
88 |
+
n = b.shape[0] # number of targets
|
89 |
+
if n:
|
90 |
+
nt += n # cumulative targets
|
91 |
+
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets
|
92 |
+
|
93 |
+
# Regression
|
94 |
+
pxy = ps[:, :2].sigmoid() * 2. - 0.5
|
95 |
+
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
|
96 |
+
pbox = torch.cat((pxy, pwh), 1).to(device) # predicted box
|
97 |
+
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target)
|
98 |
+
lbox += (1.0 - iou).mean() # iou loss
|
99 |
+
|
100 |
+
# Objectness
|
101 |
+
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio
|
102 |
+
|
103 |
+
# Classification
|
104 |
+
if model.nc > 1: # cls loss (only if multiple classes)
|
105 |
+
t = torch.full_like(ps[:, 5:], cn, device=device) # targets
|
106 |
+
t[range(n), tcls[i]] = cp
|
107 |
+
lcls += BCEcls(ps[:, 5:], t) # BCE
|
108 |
+
|
109 |
+
# Append targets to text file
|
110 |
+
# with open('targets.txt', 'a') as file:
|
111 |
+
# [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
|
112 |
+
|
113 |
+
lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss
|
114 |
+
|
115 |
+
s = 3 / no # output count scaling
|
116 |
+
lbox *= h['box'] * s
|
117 |
+
lobj *= h['obj'] * s * (1.4 if no == 4 else 1.)
|
118 |
+
lcls *= h['cls'] * s
|
119 |
+
bs = tobj.shape[0] # batch size
|
120 |
+
|
121 |
+
loss = lbox + lobj + lcls
|
122 |
+
return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
|
123 |
+
|
124 |
+
|
125 |
+
def build_targets(p, targets, model):
|
126 |
+
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
|
127 |
+
det = model.module.model[-1] if is_parallel(model) else model.model[-1] # Detect() module
|
128 |
+
na, nt = det.na, targets.shape[0] # number of anchors, targets
|
129 |
+
tcls, tbox, indices, anch = [], [], [], []
|
130 |
+
gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
|
131 |
+
ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
|
132 |
+
targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices
|
133 |
+
|
134 |
+
g = 0.5 # bias
|
135 |
+
off = torch.tensor([[0, 0],
|
136 |
+
[1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
|
137 |
+
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
|
138 |
+
], device=targets.device).float() * g # offsets
|
139 |
+
|
140 |
+
for i in range(det.nl):
|
141 |
+
anchors = det.anchors[i]
|
142 |
+
gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain
|
143 |
+
|
144 |
+
# Match targets to anchors
|
145 |
+
t = targets * gain
|
146 |
+
if nt:
|
147 |
+
# Matches
|
148 |
+
r = t[:, :, 4:6] / anchors[:, None] # wh ratio
|
149 |
+
j = torch.max(r, 1. / r).max(2)[0] < model.hyp['anchor_t'] # compare
|
150 |
+
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
|
151 |
+
t = t[j] # filter
|
152 |
+
|
153 |
+
# Offsets
|
154 |
+
gxy = t[:, 2:4] # grid xy
|
155 |
+
gxi = gain[[2, 3]] - gxy # inverse
|
156 |
+
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
|
157 |
+
l, m = ((gxi % 1. < g) & (gxi > 1.)).T
|
158 |
+
j = torch.stack((torch.ones_like(j), j, k, l, m))
|
159 |
+
t = t.repeat((5, 1, 1))[j]
|
160 |
+
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
|
161 |
+
else:
|
162 |
+
t = targets[0]
|
163 |
+
offsets = 0
|
164 |
+
|
165 |
+
# Define
|
166 |
+
b, c = t[:, :2].long().T # image, class
|
167 |
+
gxy = t[:, 2:4] # grid xy
|
168 |
+
gwh = t[:, 4:6] # grid wh
|
169 |
+
gij = (gxy - offsets).long()
|
170 |
+
gi, gj = gij.T # grid xy indices
|
171 |
+
|
172 |
+
# Append
|
173 |
+
a = t[:, 6].long() # anchor indices
|
174 |
+
indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices
|
175 |
+
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
|
176 |
+
anch.append(anchors[a]) # anchors
|
177 |
+
tcls.append(c) # class
|
178 |
+
|
179 |
+
return tcls, tbox, indices, anch
|
utils/metrics.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Model validation metrics
|
2 |
+
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
import numpy as np
|
5 |
+
|
6 |
+
|
7 |
+
def fitness(x):
|
8 |
+
# Model fitness as a weighted combination of metrics
|
9 |
+
w = [0.0, 0.0, 0.1, 0.9] # weights for [P, R, [email protected], [email protected]:0.95]
|
10 |
+
return (x[:, :4] * w).sum(1)
|
11 |
+
|
12 |
+
|
13 |
+
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, fname='precision-recall_curve.png'):
|
14 |
+
""" Compute the average precision, given the recall and precision curves.
|
15 |
+
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
16 |
+
# Arguments
|
17 |
+
tp: True positives (nparray, nx1 or nx10).
|
18 |
+
conf: Objectness value from 0-1 (nparray).
|
19 |
+
pred_cls: Predicted object classes (nparray).
|
20 |
+
target_cls: True object classes (nparray).
|
21 |
+
plot: Plot precision-recall curve at [email protected]
|
22 |
+
fname: Plot filename
|
23 |
+
# Returns
|
24 |
+
The average precision as computed in py-faster-rcnn.
|
25 |
+
"""
|
26 |
+
|
27 |
+
# Sort by objectness
|
28 |
+
i = np.argsort(-conf)
|
29 |
+
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
30 |
+
|
31 |
+
# Find unique classes
|
32 |
+
unique_classes = np.unique(target_cls)
|
33 |
+
|
34 |
+
# Create Precision-Recall curve and compute AP for each class
|
35 |
+
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
36 |
+
pr_score = 0.1 # score to evaluate P and R https://github.com/ultralytics/yolov3/issues/898
|
37 |
+
s = [unique_classes.shape[0], tp.shape[1]] # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95)
|
38 |
+
ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s)
|
39 |
+
for ci, c in enumerate(unique_classes):
|
40 |
+
i = pred_cls == c
|
41 |
+
n_l = (target_cls == c).sum() # number of labels
|
42 |
+
n_p = i.sum() # number of predictions
|
43 |
+
|
44 |
+
if n_p == 0 or n_l == 0:
|
45 |
+
continue
|
46 |
+
else:
|
47 |
+
# Accumulate FPs and TPs
|
48 |
+
fpc = (1 - tp[i]).cumsum(0)
|
49 |
+
tpc = tp[i].cumsum(0)
|
50 |
+
|
51 |
+
# Recall
|
52 |
+
recall = tpc / (n_l + 1e-16) # recall curve
|
53 |
+
r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) # r at pr_score, negative x, xp because xp decreases
|
54 |
+
|
55 |
+
# Precision
|
56 |
+
precision = tpc / (tpc + fpc) # precision curve
|
57 |
+
p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0]) # p at pr_score
|
58 |
+
|
59 |
+
# AP from recall-precision curve
|
60 |
+
for j in range(tp.shape[1]):
|
61 |
+
ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])
|
62 |
+
if j == 0:
|
63 |
+
py.append(np.interp(px, mrec, mpre)) # precision at [email protected]
|
64 |
+
|
65 |
+
# Compute F1 score (harmonic mean of precision and recall)
|
66 |
+
f1 = 2 * p * r / (p + r + 1e-16)
|
67 |
+
|
68 |
+
if plot:
|
69 |
+
py = np.stack(py, axis=1)
|
70 |
+
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
|
71 |
+
ax.plot(px, py, linewidth=0.5, color='grey') # plot(recall, precision)
|
72 |
+
ax.plot(px, py.mean(1), linewidth=2, color='blue', label='all classes %.3f [email protected]' % ap[:, 0].mean())
|
73 |
+
ax.set_xlabel('Recall')
|
74 |
+
ax.set_ylabel('Precision')
|
75 |
+
ax.set_xlim(0, 1)
|
76 |
+
ax.set_ylim(0, 1)
|
77 |
+
plt.legend()
|
78 |
+
fig.tight_layout()
|
79 |
+
fig.savefig(fname, dpi=200)
|
80 |
+
|
81 |
+
return p, r, ap, f1, unique_classes.astype('int32')
|
82 |
+
|
83 |
+
|
84 |
+
def compute_ap(recall, precision):
|
85 |
+
""" Compute the average precision, given the recall and precision curves.
|
86 |
+
Source: https://github.com/rbgirshick/py-faster-rcnn.
|
87 |
+
# Arguments
|
88 |
+
recall: The recall curve (list).
|
89 |
+
precision: The precision curve (list).
|
90 |
+
# Returns
|
91 |
+
The average precision as computed in py-faster-rcnn.
|
92 |
+
"""
|
93 |
+
|
94 |
+
# Append sentinel values to beginning and end
|
95 |
+
mrec = recall # np.concatenate(([0.], recall, [recall[-1] + 1E-3]))
|
96 |
+
mpre = precision # np.concatenate(([0.], precision, [0.]))
|
97 |
+
|
98 |
+
# Compute the precision envelope
|
99 |
+
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
100 |
+
|
101 |
+
# Integrate area under curve
|
102 |
+
method = 'interp' # methods: 'continuous', 'interp'
|
103 |
+
if method == 'interp':
|
104 |
+
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
105 |
+
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
106 |
+
else: # 'continuous'
|
107 |
+
i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes
|
108 |
+
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
109 |
+
|
110 |
+
return ap, mpre, mrec
|
utils/plots.py
ADDED
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Plotting utils
|
2 |
+
|
3 |
+
import glob
|
4 |
+
import math
|
5 |
+
import os
|
6 |
+
import random
|
7 |
+
from copy import copy
|
8 |
+
from pathlib import Path
|
9 |
+
|
10 |
+
import cv2
|
11 |
+
import matplotlib
|
12 |
+
import matplotlib.pyplot as plt
|
13 |
+
import numpy as np
|
14 |
+
import torch
|
15 |
+
import yaml
|
16 |
+
from PIL import Image
|
17 |
+
from scipy.signal import butter, filtfilt
|
18 |
+
|
19 |
+
from utils.general import xywh2xyxy, xyxy2xywh
|
20 |
+
from utils.metrics import fitness
|
21 |
+
|
22 |
+
|
23 |
+
def color_list():
|
24 |
+
# Return first 10 plt colors as (r,g,b) https://stackoverflow.com/questions/51350872/python-from-color-name-to-rgb
|
25 |
+
def hex2rgb(h):
|
26 |
+
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
|
27 |
+
|
28 |
+
return [hex2rgb(h) for h in plt.rcParams['axes.prop_cycle'].by_key()['color']]
|
29 |
+
|
30 |
+
|
31 |
+
def hist2d(x, y, n=100):
|
32 |
+
# 2d histogram used in labels.png and evolve.png
|
33 |
+
xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
|
34 |
+
hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
|
35 |
+
xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
|
36 |
+
yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
|
37 |
+
return np.log(hist[xidx, yidx])
|
38 |
+
|
39 |
+
|
40 |
+
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
|
41 |
+
# https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
|
42 |
+
def butter_lowpass(cutoff, fs, order):
|
43 |
+
nyq = 0.5 * fs
|
44 |
+
normal_cutoff = cutoff / nyq
|
45 |
+
return butter(order, normal_cutoff, btype='low', analog=False)
|
46 |
+
|
47 |
+
b, a = butter_lowpass(cutoff, fs, order=order)
|
48 |
+
return filtfilt(b, a, data) # forward-backward filter
|
49 |
+
|
50 |
+
|
51 |
+
def plot_one_box(x, img, color=None, label=None, line_thickness=None):
|
52 |
+
# Plots one bounding box on image img
|
53 |
+
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
54 |
+
color = color or [random.randint(0, 255) for _ in range(3)]
|
55 |
+
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
56 |
+
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
57 |
+
if label:
|
58 |
+
tf = max(tl - 1, 1) # font thickness
|
59 |
+
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
60 |
+
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
61 |
+
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
62 |
+
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)
|
63 |
+
|
64 |
+
|
65 |
+
def plot_wh_methods(): # from utils.general import *; plot_wh_methods()
|
66 |
+
# Compares the two methods for width-height anchor multiplication
|
67 |
+
# https://github.com/ultralytics/yolov3/issues/168
|
68 |
+
x = np.arange(-4.0, 4.0, .1)
|
69 |
+
ya = np.exp(x)
|
70 |
+
yb = torch.sigmoid(torch.from_numpy(x)).numpy() * 2
|
71 |
+
|
72 |
+
fig = plt.figure(figsize=(6, 3), dpi=150)
|
73 |
+
plt.plot(x, ya, '.-', label='YOLOv3')
|
74 |
+
plt.plot(x, yb ** 2, '.-', label='YOLOv5 ^2')
|
75 |
+
plt.plot(x, yb ** 1.6, '.-', label='YOLOv5 ^1.6')
|
76 |
+
plt.xlim(left=-4, right=4)
|
77 |
+
plt.ylim(bottom=0, top=6)
|
78 |
+
plt.xlabel('input')
|
79 |
+
plt.ylabel('output')
|
80 |
+
plt.grid()
|
81 |
+
plt.legend()
|
82 |
+
fig.tight_layout()
|
83 |
+
fig.savefig('comparison.png', dpi=200)
|
84 |
+
|
85 |
+
|
86 |
+
def output_to_target(output, width, height):
|
87 |
+
# Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
|
88 |
+
if isinstance(output, torch.Tensor):
|
89 |
+
output = output.cpu().numpy()
|
90 |
+
|
91 |
+
targets = []
|
92 |
+
for i, o in enumerate(output):
|
93 |
+
if o is not None:
|
94 |
+
for pred in o:
|
95 |
+
box = pred[:4]
|
96 |
+
w = (box[2] - box[0]) / width
|
97 |
+
h = (box[3] - box[1]) / height
|
98 |
+
x = box[0] / width + w / 2
|
99 |
+
y = box[1] / height + h / 2
|
100 |
+
conf = pred[4]
|
101 |
+
cls = int(pred[5])
|
102 |
+
|
103 |
+
targets.append([i, cls, x, y, w, h, conf])
|
104 |
+
|
105 |
+
return np.array(targets)
|
106 |
+
|
107 |
+
|
108 |
+
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=640, max_subplots=16):
|
109 |
+
# Plot image grid with labels
|
110 |
+
|
111 |
+
if isinstance(images, torch.Tensor):
|
112 |
+
images = images.cpu().float().numpy()
|
113 |
+
if isinstance(targets, torch.Tensor):
|
114 |
+
targets = targets.cpu().numpy()
|
115 |
+
|
116 |
+
# un-normalise
|
117 |
+
if np.max(images[0]) <= 1:
|
118 |
+
images *= 255
|
119 |
+
|
120 |
+
tl = 3 # line thickness
|
121 |
+
tf = max(tl - 1, 1) # font thickness
|
122 |
+
bs, _, h, w = images.shape # batch size, _, height, width
|
123 |
+
bs = min(bs, max_subplots) # limit plot images
|
124 |
+
ns = np.ceil(bs ** 0.5) # number of subplots (square)
|
125 |
+
|
126 |
+
# Check if we should resize
|
127 |
+
scale_factor = max_size / max(h, w)
|
128 |
+
if scale_factor < 1:
|
129 |
+
h = math.ceil(scale_factor * h)
|
130 |
+
w = math.ceil(scale_factor * w)
|
131 |
+
|
132 |
+
colors = color_list() # list of colors
|
133 |
+
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init
|
134 |
+
for i, img in enumerate(images):
|
135 |
+
if i == max_subplots: # if last batch has fewer images than we expect
|
136 |
+
break
|
137 |
+
|
138 |
+
block_x = int(w * (i // ns))
|
139 |
+
block_y = int(h * (i % ns))
|
140 |
+
|
141 |
+
img = img.transpose(1, 2, 0)
|
142 |
+
if scale_factor < 1:
|
143 |
+
img = cv2.resize(img, (w, h))
|
144 |
+
|
145 |
+
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img
|
146 |
+
if len(targets) > 0:
|
147 |
+
image_targets = targets[targets[:, 0] == i]
|
148 |
+
boxes = xywh2xyxy(image_targets[:, 2:6]).T
|
149 |
+
classes = image_targets[:, 1].astype('int')
|
150 |
+
labels = image_targets.shape[1] == 6 # labels if no conf column
|
151 |
+
conf = None if labels else image_targets[:, 6] # check for confidence presence (label vs pred)
|
152 |
+
|
153 |
+
boxes[[0, 2]] *= w
|
154 |
+
boxes[[0, 2]] += block_x
|
155 |
+
boxes[[1, 3]] *= h
|
156 |
+
boxes[[1, 3]] += block_y
|
157 |
+
for j, box in enumerate(boxes.T):
|
158 |
+
cls = int(classes[j])
|
159 |
+
color = colors[cls % len(colors)]
|
160 |
+
cls = names[cls] if names else cls
|
161 |
+
if labels or conf[j] > 0.3: # 0.3 conf thresh
|
162 |
+
label = '%s' % cls if labels else '%s %.1f' % (cls, conf[j])
|
163 |
+
plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl)
|
164 |
+
|
165 |
+
# Draw image filename labels
|
166 |
+
if paths is not None:
|
167 |
+
label = os.path.basename(paths[i])[:40] # trim to 40 char
|
168 |
+
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
169 |
+
cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf,
|
170 |
+
lineType=cv2.LINE_AA)
|
171 |
+
|
172 |
+
# Image border
|
173 |
+
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3)
|
174 |
+
|
175 |
+
if fname is not None:
|
176 |
+
r = min(1280. / max(h, w) / ns, 1.0) # ratio to limit image size
|
177 |
+
mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA)
|
178 |
+
# cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save
|
179 |
+
Image.fromarray(mosaic).save(fname) # PIL save
|
180 |
+
return mosaic
|
181 |
+
|
182 |
+
|
183 |
+
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
|
184 |
+
# Plot LR simulating training for full epochs
|
185 |
+
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
|
186 |
+
y = []
|
187 |
+
for _ in range(epochs):
|
188 |
+
scheduler.step()
|
189 |
+
y.append(optimizer.param_groups[0]['lr'])
|
190 |
+
plt.plot(y, '.-', label='LR')
|
191 |
+
plt.xlabel('epoch')
|
192 |
+
plt.ylabel('LR')
|
193 |
+
plt.grid()
|
194 |
+
plt.xlim(0, epochs)
|
195 |
+
plt.ylim(0)
|
196 |
+
plt.tight_layout()
|
197 |
+
plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
|
198 |
+
|
199 |
+
|
200 |
+
def plot_test_txt(): # from utils.general import *; plot_test()
|
201 |
+
# Plot test.txt histograms
|
202 |
+
x = np.loadtxt('test.txt', dtype=np.float32)
|
203 |
+
box = xyxy2xywh(x[:, :4])
|
204 |
+
cx, cy = box[:, 0], box[:, 1]
|
205 |
+
|
206 |
+
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
|
207 |
+
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
|
208 |
+
ax.set_aspect('equal')
|
209 |
+
plt.savefig('hist2d.png', dpi=300)
|
210 |
+
|
211 |
+
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
|
212 |
+
ax[0].hist(cx, bins=600)
|
213 |
+
ax[1].hist(cy, bins=600)
|
214 |
+
plt.savefig('hist1d.png', dpi=200)
|
215 |
+
|
216 |
+
|
217 |
+
def plot_targets_txt(): # from utils.general import *; plot_targets_txt()
|
218 |
+
# Plot targets.txt histograms
|
219 |
+
x = np.loadtxt('targets.txt', dtype=np.float32).T
|
220 |
+
s = ['x targets', 'y targets', 'width targets', 'height targets']
|
221 |
+
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
222 |
+
ax = ax.ravel()
|
223 |
+
for i in range(4):
|
224 |
+
ax[i].hist(x[i], bins=100, label='%.3g +/- %.3g' % (x[i].mean(), x[i].std()))
|
225 |
+
ax[i].legend()
|
226 |
+
ax[i].set_title(s[i])
|
227 |
+
plt.savefig('targets.jpg', dpi=200)
|
228 |
+
|
229 |
+
|
230 |
+
def plot_study_txt(f='study.txt', x=None): # from utils.general import *; plot_study_txt()
|
231 |
+
# Plot study.txt generated by test.py
|
232 |
+
fig, ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)
|
233 |
+
ax = ax.ravel()
|
234 |
+
|
235 |
+
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
|
236 |
+
for f in ['study/study_coco_yolov5%s.txt' % x for x in ['s', 'm', 'l', 'x']]:
|
237 |
+
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
|
238 |
+
x = np.arange(y.shape[1]) if x is None else np.array(x)
|
239 |
+
s = ['P', 'R', '[email protected]', '[email protected]:.95', 't_inference (ms/img)', 't_NMS (ms/img)', 't_total (ms/img)']
|
240 |
+
for i in range(7):
|
241 |
+
ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
|
242 |
+
ax[i].set_title(s[i])
|
243 |
+
|
244 |
+
j = y[3].argmax() + 1
|
245 |
+
ax2.plot(y[6, :j], y[3, :j] * 1E2, '.-', linewidth=2, markersize=8,
|
246 |
+
label=Path(f).stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
|
247 |
+
|
248 |
+
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
|
249 |
+
'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
|
250 |
+
|
251 |
+
ax2.grid()
|
252 |
+
ax2.set_xlim(0, 30)
|
253 |
+
ax2.set_ylim(28, 50)
|
254 |
+
ax2.set_yticks(np.arange(30, 55, 5))
|
255 |
+
ax2.set_xlabel('GPU Speed (ms/img)')
|
256 |
+
ax2.set_ylabel('COCO AP val')
|
257 |
+
ax2.legend(loc='lower right')
|
258 |
+
plt.savefig('study_mAP_latency.png', dpi=300)
|
259 |
+
plt.savefig(f.replace('.txt', '.png'), dpi=300)
|
260 |
+
|
261 |
+
|
262 |
+
def plot_labels(labels, save_dir=''):
|
263 |
+
# plot dataset labels
|
264 |
+
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
|
265 |
+
nc = int(c.max() + 1) # number of classes
|
266 |
+
|
267 |
+
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
|
268 |
+
ax = ax.ravel()
|
269 |
+
ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
|
270 |
+
ax[0].set_xlabel('classes')
|
271 |
+
ax[1].scatter(b[0], b[1], c=hist2d(b[0], b[1], 90), cmap='jet')
|
272 |
+
ax[1].set_xlabel('x')
|
273 |
+
ax[1].set_ylabel('y')
|
274 |
+
ax[2].scatter(b[2], b[3], c=hist2d(b[2], b[3], 90), cmap='jet')
|
275 |
+
ax[2].set_xlabel('width')
|
276 |
+
ax[2].set_ylabel('height')
|
277 |
+
plt.savefig(Path(save_dir) / 'labels.png', dpi=200)
|
278 |
+
plt.close()
|
279 |
+
|
280 |
+
# seaborn correlogram
|
281 |
+
try:
|
282 |
+
import seaborn as sns
|
283 |
+
import pandas as pd
|
284 |
+
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
|
285 |
+
sns.pairplot(x, corner=True, diag_kind='hist', kind='scatter', markers='o',
|
286 |
+
plot_kws=dict(s=3, edgecolor=None, linewidth=1, alpha=0.02),
|
287 |
+
diag_kws=dict(bins=50))
|
288 |
+
plt.savefig(Path(save_dir) / 'labels_correlogram.png', dpi=200)
|
289 |
+
plt.close()
|
290 |
+
except Exception as e:
|
291 |
+
pass
|
292 |
+
|
293 |
+
|
294 |
+
def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.general import *; plot_evolution()
|
295 |
+
# Plot hyperparameter evolution results in evolve.txt
|
296 |
+
with open(yaml_file) as f:
|
297 |
+
hyp = yaml.load(f, Loader=yaml.FullLoader)
|
298 |
+
x = np.loadtxt('evolve.txt', ndmin=2)
|
299 |
+
f = fitness(x)
|
300 |
+
# weights = (f - f.min()) ** 2 # for weighted results
|
301 |
+
plt.figure(figsize=(10, 12), tight_layout=True)
|
302 |
+
matplotlib.rc('font', **{'size': 8})
|
303 |
+
for i, (k, v) in enumerate(hyp.items()):
|
304 |
+
y = x[:, i + 7]
|
305 |
+
# mu = (y * weights).sum() / weights.sum() # best weighted result
|
306 |
+
mu = y[f.argmax()] # best single result
|
307 |
+
plt.subplot(6, 5, i + 1)
|
308 |
+
plt.scatter(y, f, c=hist2d(y, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
|
309 |
+
plt.plot(mu, f.max(), 'k+', markersize=15)
|
310 |
+
plt.title('%s = %.3g' % (k, mu), fontdict={'size': 9}) # limit to 40 characters
|
311 |
+
if i % 5 != 0:
|
312 |
+
plt.yticks([])
|
313 |
+
print('%15s: %.3g' % (k, mu))
|
314 |
+
plt.savefig('evolve.png', dpi=200)
|
315 |
+
print('\nPlot saved as evolve.png')
|
316 |
+
|
317 |
+
|
318 |
+
def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_results_overlay()
|
319 |
+
# Plot training 'results*.txt', overlaying train and val losses
|
320 |
+
s = ['train', 'train', 'train', 'Precision', '[email protected]', 'val', 'val', 'val', 'Recall', '[email protected]:0.95'] # legends
|
321 |
+
t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles
|
322 |
+
for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')):
|
323 |
+
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
324 |
+
n = results.shape[1] # number of rows
|
325 |
+
x = range(start, min(stop, n) if stop else n)
|
326 |
+
fig, ax = plt.subplots(1, 5, figsize=(14, 3.5), tight_layout=True)
|
327 |
+
ax = ax.ravel()
|
328 |
+
for i in range(5):
|
329 |
+
for j in [i, i + 5]:
|
330 |
+
y = results[j, x]
|
331 |
+
ax[i].plot(x, y, marker='.', label=s[j])
|
332 |
+
# y_smooth = butter_lowpass_filtfilt(y)
|
333 |
+
# ax[i].plot(x, np.gradient(y_smooth), marker='.', label=s[j])
|
334 |
+
|
335 |
+
ax[i].set_title(t[i])
|
336 |
+
ax[i].legend()
|
337 |
+
ax[i].set_ylabel(f) if i == 0 else None # add filename
|
338 |
+
fig.savefig(f.replace('.txt', '.png'), dpi=200)
|
339 |
+
|
340 |
+
|
341 |
+
def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''):
|
342 |
+
# from utils.general import *; plot_results(save_dir='runs/train/exp0')
|
343 |
+
# Plot training 'results*.txt' as seen in https://github.com/ultralytics/yolov5#reproduce-our-training
|
344 |
+
fig, ax = plt.subplots(2, 5, figsize=(12, 6))
|
345 |
+
ax = ax.ravel()
|
346 |
+
s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall',
|
347 |
+
'val Box', 'val Objectness', 'val Classification', '[email protected]', '[email protected]:0.95']
|
348 |
+
if bucket:
|
349 |
+
# os.system('rm -rf storage.googleapis.com')
|
350 |
+
# files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id]
|
351 |
+
files = ['results%g.txt' % x for x in id]
|
352 |
+
c = ('gsutil cp ' + '%s ' * len(files) + '.') % tuple('gs://%s/results%g.txt' % (bucket, x) for x in id)
|
353 |
+
os.system(c)
|
354 |
+
else:
|
355 |
+
files = glob.glob(str(Path(save_dir) / 'results*.txt')) + glob.glob('../../Downloads/results*.txt')
|
356 |
+
assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)
|
357 |
+
for fi, f in enumerate(files):
|
358 |
+
try:
|
359 |
+
results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T
|
360 |
+
n = results.shape[1] # number of rows
|
361 |
+
x = range(start, min(stop, n) if stop else n)
|
362 |
+
for i in range(10):
|
363 |
+
y = results[i, x]
|
364 |
+
if i in [0, 1, 2, 5, 6, 7]:
|
365 |
+
y[y == 0] = np.nan # don't show zero loss values
|
366 |
+
# y /= y[0] # normalize
|
367 |
+
label = labels[fi] if len(labels) else Path(f).stem
|
368 |
+
ax[i].plot(x, y, marker='.', label=label, linewidth=1, markersize=6)
|
369 |
+
ax[i].set_title(s[i])
|
370 |
+
# if i in [5, 6, 7]: # share train and val loss y axes
|
371 |
+
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
|
372 |
+
except Exception as e:
|
373 |
+
print('Warning: Plotting error for %s; %s' % (f, e))
|
374 |
+
|
375 |
+
fig.tight_layout()
|
376 |
+
ax[1].legend()
|
377 |
+
fig.savefig(Path(save_dir) / 'results.png', dpi=200)
|
utils/torch_utils.py
CHANGED
@@ -1,7 +1,10 @@
|
|
|
|
|
|
1 |
import logging
|
2 |
import math
|
3 |
import os
|
4 |
import time
|
|
|
5 |
from copy import deepcopy
|
6 |
|
7 |
import torch
|
@@ -13,10 +16,21 @@ import torchvision
|
|
13 |
logger = logging.getLogger(__name__)
|
14 |
|
15 |
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
|
|
|
|
19 |
# Speed-reproducibility tradeoff https://pytorch.org/docs/stable/notes/randomness.html
|
|
|
20 |
if seed == 0: # slower, more reproducible
|
21 |
cudnn.deterministic = True
|
22 |
cudnn.benchmark = False
|
@@ -104,8 +118,6 @@ def prune(model, amount=0.3):
|
|
104 |
|
105 |
def fuse_conv_and_bn(conv, bn):
|
106 |
# Fuse convolution and batchnorm layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
|
107 |
-
|
108 |
-
# init
|
109 |
fusedconv = nn.Conv2d(conv.in_channels,
|
110 |
conv.out_channels,
|
111 |
kernel_size=conv.kernel_size,
|
@@ -145,8 +157,7 @@ def model_info(model, verbose=False):
|
|
145 |
except ImportError:
|
146 |
fs = ''
|
147 |
|
148 |
-
logger.info(
|
149 |
-
'Model Summary: %g layers, %g parameters, %g gradients%s' % (len(list(model.parameters())), n_p, n_g, fs))
|
150 |
|
151 |
|
152 |
def load_classifier(name='resnet101', n=2):
|
|
|
1 |
+
# PyTorch utils
|
2 |
+
|
3 |
import logging
|
4 |
import math
|
5 |
import os
|
6 |
import time
|
7 |
+
from contextlib import contextmanager
|
8 |
from copy import deepcopy
|
9 |
|
10 |
import torch
|
|
|
16 |
logger = logging.getLogger(__name__)
|
17 |
|
18 |
|
19 |
+
@contextmanager
|
20 |
+
def torch_distributed_zero_first(local_rank: int):
|
21 |
+
"""
|
22 |
+
Decorator to make all processes in distributed training wait for each local_master to do something.
|
23 |
+
"""
|
24 |
+
if local_rank not in [-1, 0]:
|
25 |
+
torch.distributed.barrier()
|
26 |
+
yield
|
27 |
+
if local_rank == 0:
|
28 |
+
torch.distributed.barrier()
|
29 |
|
30 |
+
|
31 |
+
def init_torch_seeds(seed=0):
|
32 |
# Speed-reproducibility tradeoff https://pytorch.org/docs/stable/notes/randomness.html
|
33 |
+
torch.manual_seed(seed)
|
34 |
if seed == 0: # slower, more reproducible
|
35 |
cudnn.deterministic = True
|
36 |
cudnn.benchmark = False
|
|
|
118 |
|
119 |
def fuse_conv_and_bn(conv, bn):
|
120 |
# Fuse convolution and batchnorm layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
|
|
|
|
|
121 |
fusedconv = nn.Conv2d(conv.in_channels,
|
122 |
conv.out_channels,
|
123 |
kernel_size=conv.kernel_size,
|
|
|
157 |
except ImportError:
|
158 |
fs = ''
|
159 |
|
160 |
+
logger.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
|
|
161 |
|
162 |
|
163 |
def load_classifier(name='resnet101', n=2):
|