Commit
•
f7d8562
1
Parent(s):
9dd33fd
`val.py` refactor (#4053)
Browse files* val.py refactor
* cleanup
* cleanup
* cleanup
* cleanup
* save after eval
* opt.imgsz bug fix
* wandb refactor
* dataloader to train_loader
* capitalize global variables
* runs/hub/exp to runs/detect/exp
* refactor wandb logging
* Refactor wandb operations (#4061)
Co-authored-by: Ayush Chaurasia <ayush.chaurarsia@gmail.com>
- detect.py +3 -3
- models/common.py +19 -14
- models/yolo.py +21 -22
- train.py +32 -35
- utils/datasets.py +17 -18
- utils/torch_utils.py +7 -7
- utils/wandb_logging/wandb_utils.py +46 -19
- val.py +75 -85
detect.py
CHANGED
@@ -21,7 +21,7 @@ from utils.datasets import LoadStreams, LoadImages
|
|
21 |
from utils.general import check_img_size, check_requirements, check_imshow, colorstr, non_max_suppression, \
|
22 |
apply_classifier, scale_coords, xyxy2xywh, strip_optimizer, set_logging, increment_path, save_one_box
|
23 |
from utils.plots import colors, plot_one_box
|
24 |
-
from utils.torch_utils import select_device, load_classifier,
|
25 |
|
26 |
|
27 |
@torch.no_grad()
|
@@ -100,14 +100,14 @@ def run(weights='yolov5s.pt', # model.pt path(s)
|
|
100 |
img = img.unsqueeze(0)
|
101 |
|
102 |
# Inference
|
103 |
-
t1 =
|
104 |
pred = model(img,
|
105 |
augment=augment,
|
106 |
visualize=increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False)[0]
|
107 |
|
108 |
# Apply NMS
|
109 |
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
|
110 |
-
t2 =
|
111 |
|
112 |
# Apply Classifier
|
113 |
if classify:
|
|
|
21 |
from utils.general import check_img_size, check_requirements, check_imshow, colorstr, non_max_suppression, \
|
22 |
apply_classifier, scale_coords, xyxy2xywh, strip_optimizer, set_logging, increment_path, save_one_box
|
23 |
from utils.plots import colors, plot_one_box
|
24 |
+
from utils.torch_utils import select_device, load_classifier, time_sync
|
25 |
|
26 |
|
27 |
@torch.no_grad()
|
|
|
100 |
img = img.unsqueeze(0)
|
101 |
|
102 |
# Inference
|
103 |
+
t1 = time_sync()
|
104 |
pred = model(img,
|
105 |
augment=augment,
|
106 |
visualize=increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False)[0]
|
107 |
|
108 |
# Apply NMS
|
109 |
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
|
110 |
+
t2 = time_sync()
|
111 |
|
112 |
# Apply Classifier
|
113 |
if classify:
|
models/common.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
# YOLOv5 common modules
|
2 |
|
|
|
3 |
from copy import copy
|
4 |
from pathlib import Path, PosixPath
|
5 |
|
@@ -15,7 +16,9 @@ from torch.cuda import amp
|
|
15 |
from utils.datasets import exif_transpose, letterbox
|
16 |
from utils.general import non_max_suppression, make_divisible, scale_coords, increment_path, xyxy2xywh, save_one_box
|
17 |
from utils.plots import colors, plot_one_box
|
18 |
-
from utils.torch_utils import
|
|
|
|
|
19 |
|
20 |
|
21 |
def autopad(k, p=None): # kernel, padding
|
@@ -226,7 +229,7 @@ class AutoShape(nn.Module):
|
|
226 |
self.model = model.eval()
|
227 |
|
228 |
def autoshape(self):
|
229 |
-
|
230 |
return self
|
231 |
|
232 |
@torch.no_grad()
|
@@ -240,7 +243,7 @@ class AutoShape(nn.Module):
|
|
240 |
# torch: = torch.zeros(16,3,320,640) # BCHW (scaled to size=640, 0-1 values)
|
241 |
# multiple: = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...] # list of images
|
242 |
|
243 |
-
t = [
|
244 |
p = next(self.model.parameters()) # for device and type
|
245 |
if isinstance(imgs, torch.Tensor): # torch
|
246 |
with amp.autocast(enabled=p.device.type != 'cpu'):
|
@@ -270,19 +273,19 @@ class AutoShape(nn.Module):
|
|
270 |
x = np.stack(x, 0) if n > 1 else x[0][None] # stack
|
271 |
x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW
|
272 |
x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32
|
273 |
-
t.append(
|
274 |
|
275 |
with amp.autocast(enabled=p.device.type != 'cpu'):
|
276 |
# Inference
|
277 |
y = self.model(x, augment, profile)[0] # forward
|
278 |
-
t.append(
|
279 |
|
280 |
# Post-process
|
281 |
y = non_max_suppression(y, self.conf, iou_thres=self.iou, classes=self.classes, max_det=self.max_det) # NMS
|
282 |
for i in range(n):
|
283 |
scale_coords(shape1, y[i][:, :4], shape0[i])
|
284 |
|
285 |
-
t.append(
|
286 |
return Detections(imgs, y, files, t, self.names, x.shape)
|
287 |
|
288 |
|
@@ -323,31 +326,33 @@ class Detections:
|
|
323 |
|
324 |
im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np
|
325 |
if pprint:
|
326 |
-
|
327 |
if show:
|
328 |
im.show(self.files[i]) # show
|
329 |
if save:
|
330 |
f = self.files[i]
|
331 |
im.save(save_dir / f) # save
|
332 |
-
|
|
|
333 |
if render:
|
334 |
self.imgs[i] = np.asarray(im)
|
335 |
|
336 |
def print(self):
|
337 |
self.display(pprint=True) # print results
|
338 |
-
|
|
|
339 |
|
340 |
def show(self):
|
341 |
self.display(show=True) # show results
|
342 |
|
343 |
-
def save(self, save_dir='runs/
|
344 |
-
save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/
|
345 |
self.display(save=True, save_dir=save_dir) # save results
|
346 |
|
347 |
-
def crop(self, save_dir='runs/
|
348 |
-
save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/
|
349 |
self.display(crop=True, save_dir=save_dir) # crop results
|
350 |
-
|
351 |
|
352 |
def render(self):
|
353 |
self.display(render=True) # render results
|
|
|
1 |
# YOLOv5 common modules
|
2 |
|
3 |
+
import logging
|
4 |
from copy import copy
|
5 |
from pathlib import Path, PosixPath
|
6 |
|
|
|
16 |
from utils.datasets import exif_transpose, letterbox
|
17 |
from utils.general import non_max_suppression, make_divisible, scale_coords, increment_path, xyxy2xywh, save_one_box
|
18 |
from utils.plots import colors, plot_one_box
|
19 |
+
from utils.torch_utils import time_sync
|
20 |
+
|
21 |
+
LOGGER = logging.getLogger(__name__)
|
22 |
|
23 |
|
24 |
def autopad(k, p=None): # kernel, padding
|
|
|
229 |
self.model = model.eval()
|
230 |
|
231 |
def autoshape(self):
|
232 |
+
LOGGER.info('AutoShape already enabled, skipping... ') # model already converted to model.autoshape()
|
233 |
return self
|
234 |
|
235 |
@torch.no_grad()
|
|
|
243 |
# torch: = torch.zeros(16,3,320,640) # BCHW (scaled to size=640, 0-1 values)
|
244 |
# multiple: = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...] # list of images
|
245 |
|
246 |
+
t = [time_sync()]
|
247 |
p = next(self.model.parameters()) # for device and type
|
248 |
if isinstance(imgs, torch.Tensor): # torch
|
249 |
with amp.autocast(enabled=p.device.type != 'cpu'):
|
|
|
273 |
x = np.stack(x, 0) if n > 1 else x[0][None] # stack
|
274 |
x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW
|
275 |
x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32
|
276 |
+
t.append(time_sync())
|
277 |
|
278 |
with amp.autocast(enabled=p.device.type != 'cpu'):
|
279 |
# Inference
|
280 |
y = self.model(x, augment, profile)[0] # forward
|
281 |
+
t.append(time_sync())
|
282 |
|
283 |
# Post-process
|
284 |
y = non_max_suppression(y, self.conf, iou_thres=self.iou, classes=self.classes, max_det=self.max_det) # NMS
|
285 |
for i in range(n):
|
286 |
scale_coords(shape1, y[i][:, :4], shape0[i])
|
287 |
|
288 |
+
t.append(time_sync())
|
289 |
return Detections(imgs, y, files, t, self.names, x.shape)
|
290 |
|
291 |
|
|
|
326 |
|
327 |
im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np
|
328 |
if pprint:
|
329 |
+
LOGGER.info(str.rstrip(', '))
|
330 |
if show:
|
331 |
im.show(self.files[i]) # show
|
332 |
if save:
|
333 |
f = self.files[i]
|
334 |
im.save(save_dir / f) # save
|
335 |
+
if i == self.n - 1:
|
336 |
+
LOGGER.info(f"Saved {self.n} image{'s' * (self.n > 1)} to '{save_dir}'")
|
337 |
if render:
|
338 |
self.imgs[i] = np.asarray(im)
|
339 |
|
340 |
def print(self):
|
341 |
self.display(pprint=True) # print results
|
342 |
+
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {tuple(self.s)}' %
|
343 |
+
self.t)
|
344 |
|
345 |
def show(self):
|
346 |
self.display(show=True) # show results
|
347 |
|
348 |
+
def save(self, save_dir='runs/detect/exp'):
|
349 |
+
save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True) # increment save_dir
|
350 |
self.display(save=True, save_dir=save_dir) # save results
|
351 |
|
352 |
+
def crop(self, save_dir='runs/detect/exp'):
|
353 |
+
save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/detect/exp', mkdir=True) # increment save_dir
|
354 |
self.display(crop=True, save_dir=save_dir) # crop results
|
355 |
+
LOGGER.info(f'Saved results to {save_dir}\n')
|
356 |
|
357 |
def render(self):
|
358 |
self.display(render=True) # render results
|
models/yolo.py
CHANGED
@@ -5,7 +5,6 @@ Usage:
|
|
5 |
"""
|
6 |
|
7 |
import argparse
|
8 |
-
import logging
|
9 |
import sys
|
10 |
from copy import deepcopy
|
11 |
from pathlib import Path
|
@@ -18,7 +17,7 @@ from models.experimental import *
|
|
18 |
from utils.autoanchor import check_anchor_order
|
19 |
from utils.general import make_divisible, check_file, set_logging
|
20 |
from utils.plots import feature_visualization
|
21 |
-
from utils.torch_utils import
|
22 |
select_device, copy_attr
|
23 |
|
24 |
try:
|
@@ -26,7 +25,7 @@ try:
|
|
26 |
except ImportError:
|
27 |
thop = None
|
28 |
|
29 |
-
|
30 |
|
31 |
|
32 |
class Detect(nn.Module):
|
@@ -90,15 +89,15 @@ class Model(nn.Module):
|
|
90 |
# Define model
|
91 |
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
|
92 |
if nc and nc != self.yaml['nc']:
|
93 |
-
|
94 |
self.yaml['nc'] = nc # override yaml value
|
95 |
if anchors:
|
96 |
-
|
97 |
self.yaml['anchors'] = round(anchors) # override yaml value
|
98 |
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
|
99 |
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
|
100 |
self.inplace = self.yaml.get('inplace', True)
|
101 |
-
#
|
102 |
|
103 |
# Build strides, anchors
|
104 |
m = self.model[-1] # Detect()
|
@@ -110,12 +109,12 @@ class Model(nn.Module):
|
|
110 |
check_anchor_order(m)
|
111 |
self.stride = m.stride
|
112 |
self._initialize_biases() # only run once
|
113 |
-
#
|
114 |
|
115 |
# Init weights, biases
|
116 |
initialize_weights(self)
|
117 |
self.info()
|
118 |
-
|
119 |
|
120 |
def forward(self, x, augment=False, profile=False, visualize=False):
|
121 |
if augment:
|
@@ -143,13 +142,13 @@ class Model(nn.Module):
|
|
143 |
|
144 |
if profile:
|
145 |
o = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs
|
146 |
-
t =
|
147 |
for _ in range(10):
|
148 |
_ = m(x)
|
149 |
-
dt.append((
|
150 |
if m == self.model[0]:
|
151 |
-
|
152 |
-
|
153 |
|
154 |
x = m(x) # run
|
155 |
y.append(x if m.i in self.save else None) # save output
|
@@ -158,7 +157,7 @@ class Model(nn.Module):
|
|
158 |
feature_visualization(x, m.type, m.i, save_dir=visualize)
|
159 |
|
160 |
if profile:
|
161 |
-
|
162 |
return x
|
163 |
|
164 |
def _descale_pred(self, p, flips, scale, img_size):
|
@@ -192,16 +191,16 @@ class Model(nn.Module):
|
|
192 |
m = self.model[-1] # Detect() module
|
193 |
for mi in m.m: # from
|
194 |
b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85)
|
195 |
-
|
196 |
('%6g Conv2d.bias:' + '%10.3g' * 6) % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean()))
|
197 |
|
198 |
# def _print_weights(self):
|
199 |
# for m in self.model.modules():
|
200 |
# if type(m) is Bottleneck:
|
201 |
-
#
|
202 |
|
203 |
def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
|
204 |
-
|
205 |
for m in self.model.modules():
|
206 |
if type(m) is Conv and hasattr(m, 'bn'):
|
207 |
m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv
|
@@ -213,19 +212,19 @@ class Model(nn.Module):
|
|
213 |
def nms(self, mode=True): # add or remove NMS module
|
214 |
present = type(self.model[-1]) is NMS # last layer is NMS
|
215 |
if mode and not present:
|
216 |
-
|
217 |
m = NMS() # module
|
218 |
m.f = -1 # from
|
219 |
m.i = self.model[-1].i + 1 # index
|
220 |
self.model.add_module(name='%s' % m.i, module=m) # add
|
221 |
self.eval()
|
222 |
elif not mode and present:
|
223 |
-
|
224 |
self.model = self.model[:-1] # remove
|
225 |
return self
|
226 |
|
227 |
def autoshape(self): # add AutoShape module
|
228 |
-
|
229 |
m = AutoShape(self) # wrap model
|
230 |
copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes
|
231 |
return m
|
@@ -235,7 +234,7 @@ class Model(nn.Module):
|
|
235 |
|
236 |
|
237 |
def parse_model(d, ch): # model_dict, input_channels(3)
|
238 |
-
|
239 |
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
|
240 |
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
|
241 |
no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
|
@@ -279,7 +278,7 @@ def parse_model(d, ch): # model_dict, input_channels(3)
|
|
279 |
t = str(m)[8:-2].replace('__main__.', '') # module type
|
280 |
np = sum([x.numel() for x in m_.parameters()]) # number params
|
281 |
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
|
282 |
-
|
283 |
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
|
284 |
layers.append(m_)
|
285 |
if i == 0:
|
@@ -308,5 +307,5 @@ if __name__ == '__main__':
|
|
308 |
# Tensorboard (not working https://github.com/ultralytics/yolov5/issues/2898)
|
309 |
# from torch.utils.tensorboard import SummaryWriter
|
310 |
# tb_writer = SummaryWriter('.')
|
311 |
-
#
|
312 |
# tb_writer.add_graph(torch.jit.trace(model, img, strict=False), []) # add model graph
|
|
|
5 |
"""
|
6 |
|
7 |
import argparse
|
|
|
8 |
import sys
|
9 |
from copy import deepcopy
|
10 |
from pathlib import Path
|
|
|
17 |
from utils.autoanchor import check_anchor_order
|
18 |
from utils.general import make_divisible, check_file, set_logging
|
19 |
from utils.plots import feature_visualization
|
20 |
+
from utils.torch_utils import time_sync, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
|
21 |
select_device, copy_attr
|
22 |
|
23 |
try:
|
|
|
25 |
except ImportError:
|
26 |
thop = None
|
27 |
|
28 |
+
LOGGER = logging.getLogger(__name__)
|
29 |
|
30 |
|
31 |
class Detect(nn.Module):
|
|
|
89 |
# Define model
|
90 |
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
|
91 |
if nc and nc != self.yaml['nc']:
|
92 |
+
LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
|
93 |
self.yaml['nc'] = nc # override yaml value
|
94 |
if anchors:
|
95 |
+
LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
|
96 |
self.yaml['anchors'] = round(anchors) # override yaml value
|
97 |
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
|
98 |
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
|
99 |
self.inplace = self.yaml.get('inplace', True)
|
100 |
+
# LOGGER.info([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))])
|
101 |
|
102 |
# Build strides, anchors
|
103 |
m = self.model[-1] # Detect()
|
|
|
109 |
check_anchor_order(m)
|
110 |
self.stride = m.stride
|
111 |
self._initialize_biases() # only run once
|
112 |
+
# LOGGER.info('Strides: %s' % m.stride.tolist())
|
113 |
|
114 |
# Init weights, biases
|
115 |
initialize_weights(self)
|
116 |
self.info()
|
117 |
+
LOGGER.info('')
|
118 |
|
119 |
def forward(self, x, augment=False, profile=False, visualize=False):
|
120 |
if augment:
|
|
|
142 |
|
143 |
if profile:
|
144 |
o = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs
|
145 |
+
t = time_sync()
|
146 |
for _ in range(10):
|
147 |
_ = m(x)
|
148 |
+
dt.append((time_sync() - t) * 100)
|
149 |
if m == self.model[0]:
|
150 |
+
LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} {'module'}")
|
151 |
+
LOGGER.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}')
|
152 |
|
153 |
x = m(x) # run
|
154 |
y.append(x if m.i in self.save else None) # save output
|
|
|
157 |
feature_visualization(x, m.type, m.i, save_dir=visualize)
|
158 |
|
159 |
if profile:
|
160 |
+
LOGGER.info('%.1fms total' % sum(dt))
|
161 |
return x
|
162 |
|
163 |
def _descale_pred(self, p, flips, scale, img_size):
|
|
|
191 |
m = self.model[-1] # Detect() module
|
192 |
for mi in m.m: # from
|
193 |
b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85)
|
194 |
+
LOGGER.info(
|
195 |
('%6g Conv2d.bias:' + '%10.3g' * 6) % (mi.weight.shape[1], *b[:5].mean(1).tolist(), b[5:].mean()))
|
196 |
|
197 |
# def _print_weights(self):
|
198 |
# for m in self.model.modules():
|
199 |
# if type(m) is Bottleneck:
|
200 |
+
# LOGGER.info('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights
|
201 |
|
202 |
def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
|
203 |
+
LOGGER.info('Fusing layers... ')
|
204 |
for m in self.model.modules():
|
205 |
if type(m) is Conv and hasattr(m, 'bn'):
|
206 |
m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv
|
|
|
212 |
def nms(self, mode=True): # add or remove NMS module
|
213 |
present = type(self.model[-1]) is NMS # last layer is NMS
|
214 |
if mode and not present:
|
215 |
+
LOGGER.info('Adding NMS... ')
|
216 |
m = NMS() # module
|
217 |
m.f = -1 # from
|
218 |
m.i = self.model[-1].i + 1 # index
|
219 |
self.model.add_module(name='%s' % m.i, module=m) # add
|
220 |
self.eval()
|
221 |
elif not mode and present:
|
222 |
+
LOGGER.info('Removing NMS... ')
|
223 |
self.model = self.model[:-1] # remove
|
224 |
return self
|
225 |
|
226 |
def autoshape(self): # add AutoShape module
|
227 |
+
LOGGER.info('Adding AutoShape... ')
|
228 |
m = AutoShape(self) # wrap model
|
229 |
copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes
|
230 |
return m
|
|
|
234 |
|
235 |
|
236 |
def parse_model(d, ch): # model_dict, input_channels(3)
|
237 |
+
LOGGER.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
|
238 |
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
|
239 |
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
|
240 |
no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
|
|
|
278 |
t = str(m)[8:-2].replace('__main__.', '') # module type
|
279 |
np = sum([x.numel() for x in m_.parameters()]) # number params
|
280 |
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
|
281 |
+
LOGGER.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
|
282 |
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
|
283 |
layers.append(m_)
|
284 |
if i == 0:
|
|
|
307 |
# Tensorboard (not working https://github.com/ultralytics/yolov5/issues/2898)
|
308 |
# from torch.utils.tensorboard import SummaryWriter
|
309 |
# tb_writer = SummaryWriter('.')
|
310 |
+
# LOGGER.info("Run 'tensorboard --logdir=models' to view tensorboard at http://localhost:6006/")
|
311 |
# tb_writer.add_graph(torch.jit.trace(model, img, strict=False), []) # add model graph
|
train.py
CHANGED
@@ -47,7 +47,7 @@ from utils.torch_utils import ModelEMA, select_device, intersect_dicts, torch_di
|
|
47 |
from utils.wandb_logging.wandb_utils import WandbLogger, check_wandb_resume
|
48 |
from utils.metrics import fitness
|
49 |
|
50 |
-
|
51 |
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
|
52 |
RANK = int(os.getenv('RANK', -1))
|
53 |
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))
|
@@ -73,7 +73,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
73 |
if isinstance(hyp, str):
|
74 |
with open(hyp) as f:
|
75 |
hyp = yaml.safe_load(f) # load hyps dict
|
76 |
-
|
77 |
|
78 |
# Save run settings
|
79 |
with open(save_dir / 'hyp.yaml', 'w') as f:
|
@@ -94,7 +94,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
94 |
# TensorBoard
|
95 |
if not evolve:
|
96 |
prefix = colorstr('tensorboard: ')
|
97 |
-
|
98 |
loggers['tb'] = SummaryWriter(str(save_dir))
|
99 |
|
100 |
# W&B
|
@@ -123,7 +123,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
123 |
state_dict = ckpt['model'].float().state_dict() # to FP32
|
124 |
state_dict = intersect_dicts(state_dict, model.state_dict(), exclude=exclude) # intersect
|
125 |
model.load_state_dict(state_dict, strict=False) # load
|
126 |
-
|
127 |
else:
|
128 |
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
|
129 |
with torch_distributed_zero_first(RANK):
|
@@ -143,7 +143,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
143 |
nbs = 64 # nominal batch size
|
144 |
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
|
145 |
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay
|
146 |
-
|
147 |
|
148 |
pg0, pg1, pg2 = [], [], [] # optimizer parameter groups
|
149 |
for k, v in model.named_modules():
|
@@ -161,7 +161,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
161 |
|
162 |
optimizer.add_param_group({'params': pg1, 'weight_decay': hyp['weight_decay']}) # add pg1 with weight_decay
|
163 |
optimizer.add_param_group({'params': pg2}) # add pg2 (biases)
|
164 |
-
|
165 |
del pg0, pg1, pg2
|
166 |
|
167 |
# Scheduler https://arxiv.org/pdf/1812.01187.pdf
|
@@ -198,7 +198,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
198 |
if resume:
|
199 |
assert start_epoch > 0, '%s training to %g epochs is finished, nothing to resume.' % (weights, epochs)
|
200 |
if epochs < start_epoch:
|
201 |
-
|
202 |
(weights, ckpt['epoch'], epochs))
|
203 |
epochs += ckpt['epoch'] # finetune additional epochs
|
204 |
|
@@ -207,7 +207,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
207 |
# Image sizes
|
208 |
gs = max(int(model.stride.max()), 32) # grid size (max stride)
|
209 |
nl = model.model[-1].nl # number of detection layers (used for scaling hyp['obj'])
|
210 |
-
imgsz
|
211 |
|
212 |
# DP mode
|
213 |
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
|
@@ -219,33 +219,31 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
219 |
if opt.sync_bn and cuda and RANK != -1:
|
220 |
raise Exception('can not train with --sync-bn, known issue https://github.com/ultralytics/yolov5/issues/3998')
|
221 |
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
|
222 |
-
|
223 |
|
224 |
# Trainloader
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
mlc = np.concatenate(dataset.labels, 0)[:, 0].max() # max label class
|
230 |
-
nb = len(
|
231 |
assert mlc < nc, 'Label class %g exceeds nc=%g in %s. Possible class labels are 0-%g' % (mlc, nc, data, nc - 1)
|
232 |
|
233 |
# Process 0
|
234 |
if RANK in [-1, 0]:
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
|
240 |
if not resume:
|
241 |
labels = np.concatenate(dataset.labels, 0)
|
242 |
-
c = torch.tensor(labels[:, 0]) # classes
|
243 |
# cf = torch.bincount(c.long(), minlength=nc) + 1. # frequency
|
244 |
# model._initialize_biases(cf.to(device))
|
245 |
if plots:
|
246 |
plot_labels(labels, names, save_dir, loggers)
|
247 |
-
if loggers['tb']:
|
248 |
-
loggers['tb'].add_histogram('classes', c, 0) # TensorBoard
|
249 |
|
250 |
# Anchors
|
251 |
if not opt.noautoanchor:
|
@@ -277,8 +275,8 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
277 |
scheduler.last_epoch = start_epoch - 1 # do not move
|
278 |
scaler = amp.GradScaler(enabled=cuda)
|
279 |
compute_loss = ComputeLoss(model) # init loss class
|
280 |
-
|
281 |
-
f'Using {
|
282 |
f'Logging results to {save_dir}\n'
|
283 |
f'Starting training for {epochs} epochs...')
|
284 |
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
|
@@ -304,9 +302,9 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
304 |
|
305 |
mloss = torch.zeros(4, device=device) # mean losses
|
306 |
if RANK != -1:
|
307 |
-
|
308 |
-
pbar = enumerate(
|
309 |
-
|
310 |
if RANK in [-1, 0]:
|
311 |
pbar = tqdm(pbar, total=nb) # progress bar
|
312 |
optimizer.zero_grad()
|
@@ -389,10 +387,10 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
389 |
wandb_logger.current_epoch = epoch + 1
|
390 |
results, maps, _ = val.run(data_dict,
|
391 |
batch_size=batch_size // WORLD_SIZE * 2,
|
392 |
-
imgsz=
|
393 |
model=ema.ema,
|
394 |
single_cls=single_cls,
|
395 |
-
dataloader=
|
396 |
save_dir=save_dir,
|
397 |
save_json=is_coco and final_epoch,
|
398 |
verbose=nc < 50 and final_epoch,
|
@@ -444,7 +442,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
444 |
# end epoch ----------------------------------------------------------------------------------------------------
|
445 |
# end training -----------------------------------------------------------------------------------------------------
|
446 |
if RANK in [-1, 0]:
|
447 |
-
|
448 |
if plots:
|
449 |
plot_results(save_dir=save_dir) # save as results.png
|
450 |
if loggers['wandb']:
|
@@ -457,10 +455,10 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
|
|
457 |
for m in [last, best] if best.exists() else [last]: # speed, mAP tests
|
458 |
results, _, _ = val.run(data_dict,
|
459 |
batch_size=batch_size // WORLD_SIZE * 2,
|
460 |
-
imgsz=
|
461 |
model=attempt_load(m, device).half(),
|
462 |
single_cls=single_cls,
|
463 |
-
dataloader=
|
464 |
save_dir=save_dir,
|
465 |
save_json=True,
|
466 |
plots=False)
|
@@ -487,7 +485,7 @@ def parse_opt(known=False):
|
|
487 |
parser.add_argument('--hyp', type=str, default='data/hyps/hyp.scratch.yaml', help='hyperparameters path')
|
488 |
parser.add_argument('--epochs', type=int, default=300)
|
489 |
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
|
490 |
-
parser.add_argument('--img
|
491 |
parser.add_argument('--rect', action='store_true', help='rectangular training')
|
492 |
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
|
493 |
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
|
@@ -534,12 +532,11 @@ def main(opt):
|
|
534 |
with open(Path(ckpt).parent.parent / 'opt.yaml') as f:
|
535 |
opt = argparse.Namespace(**yaml.safe_load(f)) # replace
|
536 |
opt.cfg, opt.weights, opt.resume = '', ckpt, True # reinstate
|
537 |
-
|
538 |
else:
|
539 |
# opt.hyp = opt.hyp or ('hyp.finetune.yaml' if opt.weights else 'hyp.scratch.yaml')
|
540 |
opt.data, opt.cfg, opt.hyp = check_file(opt.data), check_file(opt.cfg), check_file(opt.hyp) # check files
|
541 |
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified'
|
542 |
-
opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size))) # extend to 2 sizes (train, val)
|
543 |
opt.name = 'evolve' if opt.evolve else opt.name
|
544 |
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve))
|
545 |
|
@@ -602,7 +599,7 @@ def main(opt):
|
|
602 |
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
|
603 |
yaml_file = Path(opt.save_dir) / 'hyp_evolved.yaml' # save best result here
|
604 |
if opt.bucket:
|
605 |
-
os.system('gsutil cp gs
|
606 |
|
607 |
for _ in range(opt.evolve): # generations to evolve
|
608 |
if Path('evolve.txt').exists(): # if evolve.txt exists: select best hyps and mutate
|
|
|
47 |
from utils.wandb_logging.wandb_utils import WandbLogger, check_wandb_resume
|
48 |
from utils.metrics import fitness
|
49 |
|
50 |
+
LOGGER = logging.getLogger(__name__)
|
51 |
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
|
52 |
RANK = int(os.getenv('RANK', -1))
|
53 |
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))
|
|
|
73 |
if isinstance(hyp, str):
|
74 |
with open(hyp) as f:
|
75 |
hyp = yaml.safe_load(f) # load hyps dict
|
76 |
+
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
|
77 |
|
78 |
# Save run settings
|
79 |
with open(save_dir / 'hyp.yaml', 'w') as f:
|
|
|
94 |
# TensorBoard
|
95 |
if not evolve:
|
96 |
prefix = colorstr('tensorboard: ')
|
97 |
+
LOGGER.info(f"{prefix}Start with 'tensorboard --logdir {opt.project}', view at http://localhost:6006/")
|
98 |
loggers['tb'] = SummaryWriter(str(save_dir))
|
99 |
|
100 |
# W&B
|
|
|
123 |
state_dict = ckpt['model'].float().state_dict() # to FP32
|
124 |
state_dict = intersect_dicts(state_dict, model.state_dict(), exclude=exclude) # intersect
|
125 |
model.load_state_dict(state_dict, strict=False) # load
|
126 |
+
LOGGER.info('Transferred %g/%g items from %s' % (len(state_dict), len(model.state_dict()), weights)) # report
|
127 |
else:
|
128 |
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
|
129 |
with torch_distributed_zero_first(RANK):
|
|
|
143 |
nbs = 64 # nominal batch size
|
144 |
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
|
145 |
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay
|
146 |
+
LOGGER.info(f"Scaled weight_decay = {hyp['weight_decay']}")
|
147 |
|
148 |
pg0, pg1, pg2 = [], [], [] # optimizer parameter groups
|
149 |
for k, v in model.named_modules():
|
|
|
161 |
|
162 |
optimizer.add_param_group({'params': pg1, 'weight_decay': hyp['weight_decay']}) # add pg1 with weight_decay
|
163 |
optimizer.add_param_group({'params': pg2}) # add pg2 (biases)
|
164 |
+
LOGGER.info('Optimizer groups: %g .bias, %g conv.weight, %g other' % (len(pg2), len(pg1), len(pg0)))
|
165 |
del pg0, pg1, pg2
|
166 |
|
167 |
# Scheduler https://arxiv.org/pdf/1812.01187.pdf
|
|
|
198 |
if resume:
|
199 |
assert start_epoch > 0, '%s training to %g epochs is finished, nothing to resume.' % (weights, epochs)
|
200 |
if epochs < start_epoch:
|
201 |
+
LOGGER.info('%s has been trained for %g epochs. Fine-tuning for %g additional epochs.' %
|
202 |
(weights, ckpt['epoch'], epochs))
|
203 |
epochs += ckpt['epoch'] # finetune additional epochs
|
204 |
|
|
|
207 |
# Image sizes
|
208 |
gs = max(int(model.stride.max()), 32) # grid size (max stride)
|
209 |
nl = model.model[-1].nl # number of detection layers (used for scaling hyp['obj'])
|
210 |
+
imgsz = check_img_size(opt.imgsz, gs) # verify imgsz is gs-multiple
|
211 |
|
212 |
# DP mode
|
213 |
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
|
|
|
219 |
if opt.sync_bn and cuda and RANK != -1:
|
220 |
raise Exception('can not train with --sync-bn, known issue https://github.com/ultralytics/yolov5/issues/3998')
|
221 |
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
|
222 |
+
LOGGER.info('Using SyncBatchNorm()')
|
223 |
|
224 |
# Trainloader
|
225 |
+
train_loader, dataset = create_dataloader(train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls,
|
226 |
+
hyp=hyp, augment=True, cache=opt.cache_images, rect=opt.rect, rank=RANK,
|
227 |
+
workers=workers, image_weights=opt.image_weights, quad=opt.quad,
|
228 |
+
prefix=colorstr('train: '))
|
229 |
mlc = np.concatenate(dataset.labels, 0)[:, 0].max() # max label class
|
230 |
+
nb = len(train_loader) # number of batches
|
231 |
assert mlc < nc, 'Label class %g exceeds nc=%g in %s. Possible class labels are 0-%g' % (mlc, nc, data, nc - 1)
|
232 |
|
233 |
# Process 0
|
234 |
if RANK in [-1, 0]:
|
235 |
+
val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls,
|
236 |
+
hyp=hyp, cache=opt.cache_images and not noval, rect=True, rank=-1,
|
237 |
+
workers=workers, pad=0.5,
|
238 |
+
prefix=colorstr('val: '))[0]
|
239 |
|
240 |
if not resume:
|
241 |
labels = np.concatenate(dataset.labels, 0)
|
242 |
+
# c = torch.tensor(labels[:, 0]) # classes
|
243 |
# cf = torch.bincount(c.long(), minlength=nc) + 1. # frequency
|
244 |
# model._initialize_biases(cf.to(device))
|
245 |
if plots:
|
246 |
plot_labels(labels, names, save_dir, loggers)
|
|
|
|
|
247 |
|
248 |
# Anchors
|
249 |
if not opt.noautoanchor:
|
|
|
275 |
scheduler.last_epoch = start_epoch - 1 # do not move
|
276 |
scaler = amp.GradScaler(enabled=cuda)
|
277 |
compute_loss = ComputeLoss(model) # init loss class
|
278 |
+
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n'
|
279 |
+
f'Using {train_loader.num_workers} dataloader workers\n'
|
280 |
f'Logging results to {save_dir}\n'
|
281 |
f'Starting training for {epochs} epochs...')
|
282 |
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
|
|
|
302 |
|
303 |
mloss = torch.zeros(4, device=device) # mean losses
|
304 |
if RANK != -1:
|
305 |
+
train_loader.sampler.set_epoch(epoch)
|
306 |
+
pbar = enumerate(train_loader)
|
307 |
+
LOGGER.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'labels', 'img_size'))
|
308 |
if RANK in [-1, 0]:
|
309 |
pbar = tqdm(pbar, total=nb) # progress bar
|
310 |
optimizer.zero_grad()
|
|
|
387 |
wandb_logger.current_epoch = epoch + 1
|
388 |
results, maps, _ = val.run(data_dict,
|
389 |
batch_size=batch_size // WORLD_SIZE * 2,
|
390 |
+
imgsz=imgsz,
|
391 |
model=ema.ema,
|
392 |
single_cls=single_cls,
|
393 |
+
dataloader=val_loader,
|
394 |
save_dir=save_dir,
|
395 |
save_json=is_coco and final_epoch,
|
396 |
verbose=nc < 50 and final_epoch,
|
|
|
442 |
# end epoch ----------------------------------------------------------------------------------------------------
|
443 |
# end training -----------------------------------------------------------------------------------------------------
|
444 |
if RANK in [-1, 0]:
|
445 |
+
LOGGER.info(f'{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.\n')
|
446 |
if plots:
|
447 |
plot_results(save_dir=save_dir) # save as results.png
|
448 |
if loggers['wandb']:
|
|
|
455 |
for m in [last, best] if best.exists() else [last]: # speed, mAP tests
|
456 |
results, _, _ = val.run(data_dict,
|
457 |
batch_size=batch_size // WORLD_SIZE * 2,
|
458 |
+
imgsz=imgsz,
|
459 |
model=attempt_load(m, device).half(),
|
460 |
single_cls=single_cls,
|
461 |
+
dataloader=val_loader,
|
462 |
save_dir=save_dir,
|
463 |
save_json=True,
|
464 |
plots=False)
|
|
|
485 |
parser.add_argument('--hyp', type=str, default='data/hyps/hyp.scratch.yaml', help='hyperparameters path')
|
486 |
parser.add_argument('--epochs', type=int, default=300)
|
487 |
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
|
488 |
+
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
|
489 |
parser.add_argument('--rect', action='store_true', help='rectangular training')
|
490 |
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
|
491 |
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
|
|
|
532 |
with open(Path(ckpt).parent.parent / 'opt.yaml') as f:
|
533 |
opt = argparse.Namespace(**yaml.safe_load(f)) # replace
|
534 |
opt.cfg, opt.weights, opt.resume = '', ckpt, True # reinstate
|
535 |
+
LOGGER.info(f'Resuming training from {ckpt}')
|
536 |
else:
|
537 |
# opt.hyp = opt.hyp or ('hyp.finetune.yaml' if opt.weights else 'hyp.scratch.yaml')
|
538 |
opt.data, opt.cfg, opt.hyp = check_file(opt.data), check_file(opt.cfg), check_file(opt.hyp) # check files
|
539 |
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified'
|
|
|
540 |
opt.name = 'evolve' if opt.evolve else opt.name
|
541 |
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve))
|
542 |
|
|
|
599 |
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
|
600 |
yaml_file = Path(opt.save_dir) / 'hyp_evolved.yaml' # save best result here
|
601 |
if opt.bucket:
|
602 |
+
os.system(f'gsutil cp gs://{opt.bucket}/evolve.txt .') # download evolve.txt if exists
|
603 |
|
604 |
for _ in range(opt.evolve): # generations to evolve
|
605 |
if Path('evolve.txt').exists(): # if evolve.txt exists: select best hyps and mutate
|
utils/datasets.py
CHANGED
@@ -22,17 +22,16 @@ from PIL import Image, ExifTags
|
|
22 |
from torch.utils.data import Dataset
|
23 |
from tqdm import tqdm
|
24 |
|
25 |
-
from utils.augmentations import Albumentations, augment_hsv, copy_paste, letterbox, mixup, random_perspective
|
26 |
from utils.general import check_requirements, check_file, check_dataset, xywh2xyxy, xywhn2xyxy, xyxy2xywhn, \
|
27 |
xyn2xy, segments2boxes, clean_str
|
28 |
from utils.torch_utils import torch_distributed_zero_first
|
29 |
|
30 |
# Parameters
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
logger = logging.getLogger(__name__)
|
36 |
|
37 |
# Get orientation exif tag
|
38 |
for orientation in ExifTags.TAGS.keys():
|
@@ -164,8 +163,8 @@ class LoadImages: # for inference
|
|
164 |
else:
|
165 |
raise Exception(f'ERROR: {p} does not exist')
|
166 |
|
167 |
-
images = [x for x in files if x.split('.')[-1].lower() in
|
168 |
-
videos = [x for x in files if x.split('.')[-1].lower() in
|
169 |
ni, nv = len(images), len(videos)
|
170 |
|
171 |
self.img_size = img_size
|
@@ -179,7 +178,7 @@ class LoadImages: # for inference
|
|
179 |
else:
|
180 |
self.cap = None
|
181 |
assert self.nf > 0, f'No images or videos found in {p}. ' \
|
182 |
-
f'Supported formats are:\nimages: {
|
183 |
|
184 |
def __iter__(self):
|
185 |
self.count = 0
|
@@ -389,11 +388,11 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
|
389 |
# f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
|
390 |
else:
|
391 |
raise Exception(f'{prefix}{p} does not exist')
|
392 |
-
self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in
|
393 |
# self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib
|
394 |
assert self.img_files, f'{prefix}No images found'
|
395 |
except Exception as e:
|
396 |
-
raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {
|
397 |
|
398 |
# Check cache
|
399 |
self.label_files = img2label_paths(self.img_files) # labels
|
@@ -411,7 +410,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
|
411 |
tqdm(None, desc=prefix + d, total=n, initial=n) # display cache results
|
412 |
if cache['msgs']:
|
413 |
logging.info('\n'.join(cache['msgs'])) # display warnings
|
414 |
-
assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {
|
415 |
|
416 |
# Read cache
|
417 |
[cache.pop(k) for k in ('hash', 'version', 'msgs')] # remove items
|
@@ -460,7 +459,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
|
460 |
if cache_images:
|
461 |
gb = 0 # Gigabytes of cached images
|
462 |
self.img_hw0, self.img_hw = [None] * n, [None] * n
|
463 |
-
results = ThreadPool(
|
464 |
pbar = tqdm(enumerate(results), total=n)
|
465 |
for i, x in pbar:
|
466 |
self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # img, hw_original, hw_resized = load_image(self, i)
|
@@ -473,7 +472,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
|
473 |
x = {} # dict
|
474 |
nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages
|
475 |
desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..."
|
476 |
-
with Pool(
|
477 |
pbar = tqdm(pool.imap_unordered(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))),
|
478 |
desc=desc, total=len(self.img_files))
|
479 |
for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
|
@@ -491,7 +490,7 @@ class LoadImagesAndLabels(Dataset): # for training/testing
|
|
491 |
if msgs:
|
492 |
logging.info('\n'.join(msgs))
|
493 |
if nf == 0:
|
494 |
-
logging.info(f'{prefix}WARNING: No labels found in {path}. See {
|
495 |
x['hash'] = get_hash(self.label_files + self.img_files)
|
496 |
x['results'] = nf, nm, ne, nc, len(self.img_files)
|
497 |
x['msgs'] = msgs # warnings
|
@@ -789,7 +788,7 @@ def extract_boxes(path='../datasets/coco128'): # from utils.datasets import *;
|
|
789 |
files = list(path.rglob('*.*'))
|
790 |
n = len(files) # number of files
|
791 |
for im_file in tqdm(files, total=n):
|
792 |
-
if im_file.suffix[1:] in
|
793 |
# image
|
794 |
im = cv2.imread(str(im_file))[..., ::-1] # BGR to RGB
|
795 |
h, w = im.shape[:2]
|
@@ -825,7 +824,7 @@ def autosplit(path='../datasets/coco128/images', weights=(0.9, 0.1, 0.0), annota
|
|
825 |
annotated_only: Only use images with an annotated txt file
|
826 |
"""
|
827 |
path = Path(path) # images dir
|
828 |
-
files = sum([list(path.rglob(f"*.{img_ext}")) for img_ext in
|
829 |
n = len(files) # number of files
|
830 |
random.seed(0) # for reproducibility
|
831 |
indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split
|
@@ -850,7 +849,7 @@ def verify_image_label(args):
|
|
850 |
im.verify() # PIL verify
|
851 |
shape = exif_size(im) # image size
|
852 |
assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels'
|
853 |
-
assert im.format.lower() in
|
854 |
if im.format.lower() in ('jpg', 'jpeg'):
|
855 |
with open(im_file, 'rb') as f:
|
856 |
f.seek(-2, 2)
|
|
|
22 |
from torch.utils.data import Dataset
|
23 |
from tqdm import tqdm
|
24 |
|
25 |
+
from utils.augmentations import Albumentations, augment_hsv, copy_paste, letterbox, mixup, random_perspective
|
26 |
from utils.general import check_requirements, check_file, check_dataset, xywh2xyxy, xywhn2xyxy, xyxy2xywhn, \
|
27 |
xyn2xy, segments2boxes, clean_str
|
28 |
from utils.torch_utils import torch_distributed_zero_first
|
29 |
|
30 |
# Parameters
|
31 |
+
HELP_URL = 'https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data'
|
32 |
+
IMG_FORMATS = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo'] # acceptable image suffixes
|
33 |
+
VID_FORMATS = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv'] # acceptable video suffixes
|
34 |
+
NUM_THREADS = min(8, os.cpu_count()) # number of multiprocessing threads
|
|
|
35 |
|
36 |
# Get orientation exif tag
|
37 |
for orientation in ExifTags.TAGS.keys():
|
|
|
163 |
else:
|
164 |
raise Exception(f'ERROR: {p} does not exist')
|
165 |
|
166 |
+
images = [x for x in files if x.split('.')[-1].lower() in IMG_FORMATS]
|
167 |
+
videos = [x for x in files if x.split('.')[-1].lower() in VID_FORMATS]
|
168 |
ni, nv = len(images), len(videos)
|
169 |
|
170 |
self.img_size = img_size
|
|
|
178 |
else:
|
179 |
self.cap = None
|
180 |
assert self.nf > 0, f'No images or videos found in {p}. ' \
|
181 |
+
f'Supported formats are:\nimages: {IMG_FORMATS}\nvideos: {VID_FORMATS}'
|
182 |
|
183 |
def __iter__(self):
|
184 |
self.count = 0
|
|
|
388 |
# f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
|
389 |
else:
|
390 |
raise Exception(f'{prefix}{p} does not exist')
|
391 |
+
self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS])
|
392 |
# self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib
|
393 |
assert self.img_files, f'{prefix}No images found'
|
394 |
except Exception as e:
|
395 |
+
raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {HELP_URL}')
|
396 |
|
397 |
# Check cache
|
398 |
self.label_files = img2label_paths(self.img_files) # labels
|
|
|
410 |
tqdm(None, desc=prefix + d, total=n, initial=n) # display cache results
|
411 |
if cache['msgs']:
|
412 |
logging.info('\n'.join(cache['msgs'])) # display warnings
|
413 |
+
assert nf > 0 or not augment, f'{prefix}No labels in {cache_path}. Can not train without labels. See {HELP_URL}'
|
414 |
|
415 |
# Read cache
|
416 |
[cache.pop(k) for k in ('hash', 'version', 'msgs')] # remove items
|
|
|
459 |
if cache_images:
|
460 |
gb = 0 # Gigabytes of cached images
|
461 |
self.img_hw0, self.img_hw = [None] * n, [None] * n
|
462 |
+
results = ThreadPool(NUM_THREADS).imap(lambda x: load_image(*x), zip(repeat(self), range(n)))
|
463 |
pbar = tqdm(enumerate(results), total=n)
|
464 |
for i, x in pbar:
|
465 |
self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # img, hw_original, hw_resized = load_image(self, i)
|
|
|
472 |
x = {} # dict
|
473 |
nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages
|
474 |
desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..."
|
475 |
+
with Pool(NUM_THREADS) as pool:
|
476 |
pbar = tqdm(pool.imap_unordered(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))),
|
477 |
desc=desc, total=len(self.img_files))
|
478 |
for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
|
|
|
490 |
if msgs:
|
491 |
logging.info('\n'.join(msgs))
|
492 |
if nf == 0:
|
493 |
+
logging.info(f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}')
|
494 |
x['hash'] = get_hash(self.label_files + self.img_files)
|
495 |
x['results'] = nf, nm, ne, nc, len(self.img_files)
|
496 |
x['msgs'] = msgs # warnings
|
|
|
788 |
files = list(path.rglob('*.*'))
|
789 |
n = len(files) # number of files
|
790 |
for im_file in tqdm(files, total=n):
|
791 |
+
if im_file.suffix[1:] in IMG_FORMATS:
|
792 |
# image
|
793 |
im = cv2.imread(str(im_file))[..., ::-1] # BGR to RGB
|
794 |
h, w = im.shape[:2]
|
|
|
824 |
annotated_only: Only use images with an annotated txt file
|
825 |
"""
|
826 |
path = Path(path) # images dir
|
827 |
+
files = sum([list(path.rglob(f"*.{img_ext}")) for img_ext in IMG_FORMATS], []) # image files only
|
828 |
n = len(files) # number of files
|
829 |
random.seed(0) # for reproducibility
|
830 |
indices = random.choices([0, 1, 2], weights=weights, k=n) # assign each image to a split
|
|
|
849 |
im.verify() # PIL verify
|
850 |
shape = exif_size(im) # image size
|
851 |
assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels'
|
852 |
+
assert im.format.lower() in IMG_FORMATS, f'invalid image format {im.format}'
|
853 |
if im.format.lower() in ('jpg', 'jpeg'):
|
854 |
with open(im_file, 'rb') as f:
|
855 |
f.seek(-2, 2)
|
utils/torch_utils.py
CHANGED
@@ -22,7 +22,7 @@ try:
|
|
22 |
import thop # for FLOPs computation
|
23 |
except ImportError:
|
24 |
thop = None
|
25 |
-
|
26 |
|
27 |
|
28 |
@contextmanager
|
@@ -85,11 +85,11 @@ def select_device(device='', batch_size=None):
|
|
85 |
else:
|
86 |
s += 'CPU\n'
|
87 |
|
88 |
-
|
89 |
return torch.device('cuda:0' if cuda else 'cpu')
|
90 |
|
91 |
|
92 |
-
def
|
93 |
# pytorch-accurate time
|
94 |
if torch.cuda.is_available():
|
95 |
torch.cuda.synchronize()
|
@@ -118,12 +118,12 @@ def profile(x, ops, n=100, device=None):
|
|
118 |
flops = 0
|
119 |
|
120 |
for _ in range(n):
|
121 |
-
t[0] =
|
122 |
y = m(x)
|
123 |
-
t[1] =
|
124 |
try:
|
125 |
_ = y.sum().backward()
|
126 |
-
t[2] =
|
127 |
except: # no backward method
|
128 |
t[2] = float('nan')
|
129 |
dtf += (t[1] - t[0]) * 1000 / n # ms per op forward
|
@@ -231,7 +231,7 @@ def model_info(model, verbose=False, img_size=640):
|
|
231 |
except (ImportError, Exception):
|
232 |
fs = ''
|
233 |
|
234 |
-
|
235 |
|
236 |
|
237 |
def load_classifier(name='resnet101', n=2):
|
|
|
22 |
import thop # for FLOPs computation
|
23 |
except ImportError:
|
24 |
thop = None
|
25 |
+
LOGGER = logging.getLogger(__name__)
|
26 |
|
27 |
|
28 |
@contextmanager
|
|
|
85 |
else:
|
86 |
s += 'CPU\n'
|
87 |
|
88 |
+
LOGGER.info(s.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else s) # emoji-safe
|
89 |
return torch.device('cuda:0' if cuda else 'cpu')
|
90 |
|
91 |
|
92 |
+
def time_sync():
|
93 |
# pytorch-accurate time
|
94 |
if torch.cuda.is_available():
|
95 |
torch.cuda.synchronize()
|
|
|
118 |
flops = 0
|
119 |
|
120 |
for _ in range(n):
|
121 |
+
t[0] = time_sync()
|
122 |
y = m(x)
|
123 |
+
t[1] = time_sync()
|
124 |
try:
|
125 |
_ = y.sum().backward()
|
126 |
+
t[2] = time_sync()
|
127 |
except: # no backward method
|
128 |
t[2] = float('nan')
|
129 |
dtf += (t[1] - t[0]) * 1000 / n # ms per op forward
|
|
|
231 |
except (ImportError, Exception):
|
232 |
fs = ''
|
233 |
|
234 |
+
LOGGER.info(f"Model Summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
|
235 |
|
236 |
|
237 |
def load_classifier(name='resnet101', n=2):
|
utils/wandb_logging/wandb_utils.py
CHANGED
@@ -98,7 +98,14 @@ class WandbLogger():
|
|
98 |
def __init__(self, opt, name, run_id, data_dict, job_type='Training'):
|
99 |
# Pre-training routine --
|
100 |
self.job_type = job_type
|
101 |
-
self.wandb, self.wandb_run
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
# It's more elegant to stick to 1 wandb.init call, but useful config data is overwritten in the WandbLogger's wandb.init call
|
103 |
if isinstance(opt.resume, str): # checks resume from artifact
|
104 |
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
@@ -156,25 +163,27 @@ class WandbLogger():
|
|
156 |
self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \
|
157 |
config.opt['hyp']
|
158 |
data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume
|
159 |
-
if
|
160 |
self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'),
|
161 |
opt.artifact_alias)
|
162 |
self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'),
|
163 |
opt.artifact_alias)
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
wandb.log({"validation dataset": self.val_table})
|
174 |
-
|
175 |
if self.val_artifact is not None:
|
176 |
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
177 |
self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"])
|
|
|
|
|
|
|
|
|
178 |
if opt.bbox_interval == -1:
|
179 |
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
|
180 |
return data_dict
|
@@ -182,7 +191,7 @@ class WandbLogger():
|
|
182 |
def download_dataset_artifact(self, path, alias):
|
183 |
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
|
184 |
artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
|
185 |
-
dataset_artifact = wandb.use_artifact(artifact_path.as_posix().replace("\\","/"))
|
186 |
assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
|
187 |
datadir = dataset_artifact.download()
|
188 |
return datadir, dataset_artifact
|
@@ -246,10 +255,10 @@ class WandbLogger():
|
|
246 |
return path
|
247 |
|
248 |
def map_val_table_path(self):
|
249 |
-
self.
|
250 |
print("Mapping dataset")
|
251 |
for i, data in enumerate(tqdm(self.val_table.data)):
|
252 |
-
self.
|
253 |
|
254 |
def create_dataset_table(self, dataset, class_to_id, name='dataset'):
|
255 |
# TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
|
@@ -283,7 +292,6 @@ class WandbLogger():
|
|
283 |
return artifact
|
284 |
|
285 |
def log_training_progress(self, predn, path, names):
|
286 |
-
if self.val_table and self.result_table:
|
287 |
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
|
288 |
box_data = []
|
289 |
total_conf = 0
|
@@ -297,7 +305,7 @@ class WandbLogger():
|
|
297 |
"domain": "pixel"})
|
298 |
total_conf = total_conf + conf
|
299 |
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
300 |
-
id = self.
|
301 |
self.result_table.add_data(self.current_epoch,
|
302 |
id,
|
303 |
self.val_table.data[id][1],
|
@@ -305,6 +313,22 @@ class WandbLogger():
|
|
305 |
total_conf / max(1, len(box_data))
|
306 |
)
|
307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
308 |
def log(self, log_dict):
|
309 |
if self.wandb_run:
|
310 |
for key, value in log_dict.items():
|
@@ -313,13 +337,16 @@ class WandbLogger():
|
|
313 |
def end_epoch(self, best_result=False):
|
314 |
if self.wandb_run:
|
315 |
with all_logging_disabled():
|
|
|
|
|
316 |
wandb.log(self.log_dict)
|
317 |
self.log_dict = {}
|
|
|
318 |
if self.result_artifact:
|
319 |
self.result_artifact.add(self.result_table, 'result')
|
320 |
wandb.log_artifact(self.result_artifact, aliases=['latest', 'last', 'epoch ' + str(self.current_epoch),
|
321 |
('best' if best_result else '')])
|
322 |
-
|
323 |
wandb.log({"evaluation": self.result_table})
|
324 |
self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"])
|
325 |
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
|
|
98 |
def __init__(self, opt, name, run_id, data_dict, job_type='Training'):
|
99 |
# Pre-training routine --
|
100 |
self.job_type = job_type
|
101 |
+
self.wandb, self.wandb_run = wandb, None if not wandb else wandb.run
|
102 |
+
self.val_artifact, self.train_artifact = None, None
|
103 |
+
self.train_artifact_path, self.val_artifact_path = None, None
|
104 |
+
self.result_artifact = None
|
105 |
+
self.val_table, self.result_table = None, None
|
106 |
+
self.data_dict = data_dict
|
107 |
+
self.bbox_media_panel_images = []
|
108 |
+
self.val_table_path_map = None
|
109 |
# It's more elegant to stick to 1 wandb.init call, but useful config data is overwritten in the WandbLogger's wandb.init call
|
110 |
if isinstance(opt.resume, str): # checks resume from artifact
|
111 |
if opt.resume.startswith(WANDB_ARTIFACT_PREFIX):
|
|
|
163 |
self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \
|
164 |
config.opt['hyp']
|
165 |
data_dict = dict(self.wandb_run.config.data_dict) # eliminates the need for config file to resume
|
166 |
+
if self.val_artifact is None: # If --upload_dataset is set, use the existing artifact, don't download
|
167 |
self.train_artifact_path, self.train_artifact = self.download_dataset_artifact(data_dict.get('train'),
|
168 |
opt.artifact_alias)
|
169 |
self.val_artifact_path, self.val_artifact = self.download_dataset_artifact(data_dict.get('val'),
|
170 |
opt.artifact_alias)
|
171 |
+
|
172 |
+
if self.train_artifact_path is not None:
|
173 |
+
train_path = Path(self.train_artifact_path) / 'data/images/'
|
174 |
+
data_dict['train'] = str(train_path)
|
175 |
+
if self.val_artifact_path is not None:
|
176 |
+
val_path = Path(self.val_artifact_path) / 'data/images/'
|
177 |
+
data_dict['val'] = str(val_path)
|
178 |
+
|
179 |
+
|
|
|
|
|
180 |
if self.val_artifact is not None:
|
181 |
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
182 |
self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"])
|
183 |
+
self.val_table = self.val_artifact.get("val")
|
184 |
+
if self.val_table_path_map is None:
|
185 |
+
self.map_val_table_path()
|
186 |
+
wandb.log({"validation dataset": self.val_table})
|
187 |
if opt.bbox_interval == -1:
|
188 |
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
|
189 |
return data_dict
|
|
|
191 |
def download_dataset_artifact(self, path, alias):
|
192 |
if isinstance(path, str) and path.startswith(WANDB_ARTIFACT_PREFIX):
|
193 |
artifact_path = Path(remove_prefix(path, WANDB_ARTIFACT_PREFIX) + ":" + alias)
|
194 |
+
dataset_artifact = wandb.use_artifact(artifact_path.as_posix().replace("\\", "/"))
|
195 |
assert dataset_artifact is not None, "'Error: W&B dataset artifact doesn\'t exist'"
|
196 |
datadir = dataset_artifact.download()
|
197 |
return datadir, dataset_artifact
|
|
|
255 |
return path
|
256 |
|
257 |
def map_val_table_path(self):
|
258 |
+
self.val_table_path_map = {}
|
259 |
print("Mapping dataset")
|
260 |
for i, data in enumerate(tqdm(self.val_table.data)):
|
261 |
+
self.val_table_path_map[data[3]] = data[0]
|
262 |
|
263 |
def create_dataset_table(self, dataset, class_to_id, name='dataset'):
|
264 |
# TODO: Explore multiprocessing to slpit this loop parallely| This is essential for speeding up the the logging
|
|
|
292 |
return artifact
|
293 |
|
294 |
def log_training_progress(self, predn, path, names):
|
|
|
295 |
class_set = wandb.Classes([{'id': id, 'name': name} for id, name in names.items()])
|
296 |
box_data = []
|
297 |
total_conf = 0
|
|
|
305 |
"domain": "pixel"})
|
306 |
total_conf = total_conf + conf
|
307 |
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
308 |
+
id = self.val_table_path_map[Path(path).name]
|
309 |
self.result_table.add_data(self.current_epoch,
|
310 |
id,
|
311 |
self.val_table.data[id][1],
|
|
|
313 |
total_conf / max(1, len(box_data))
|
314 |
)
|
315 |
|
316 |
+
def val_one_image(self, pred, predn, path, names, im):
|
317 |
+
if self.val_table and self.result_table: # Log Table if Val dataset is uploaded as artifact
|
318 |
+
self.log_training_progress(predn, path, names)
|
319 |
+
else: # Default to bbox media panelif Val artifact not found
|
320 |
+
log_imgs = min(self.log_imgs, 100)
|
321 |
+
if len(self.bbox_media_panel_images) < log_imgs and self.current_epoch > 0:
|
322 |
+
if self.current_epoch % self.bbox_interval == 0:
|
323 |
+
box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
324 |
+
"class_id": int(cls),
|
325 |
+
"box_caption": "%s %.3f" % (names[cls], conf),
|
326 |
+
"scores": {"class_score": conf},
|
327 |
+
"domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
|
328 |
+
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
329 |
+
self.bbox_media_panel_images.append(wandb.Image(im, boxes=boxes, caption=path.name))
|
330 |
+
|
331 |
+
|
332 |
def log(self, log_dict):
|
333 |
if self.wandb_run:
|
334 |
for key, value in log_dict.items():
|
|
|
337 |
def end_epoch(self, best_result=False):
|
338 |
if self.wandb_run:
|
339 |
with all_logging_disabled():
|
340 |
+
if self.bbox_media_panel_images:
|
341 |
+
self.log_dict["Bounding Box Debugger/Images"] = self.bbox_media_panel_images
|
342 |
wandb.log(self.log_dict)
|
343 |
self.log_dict = {}
|
344 |
+
self.bbox_media_panel_images = []
|
345 |
if self.result_artifact:
|
346 |
self.result_artifact.add(self.result_table, 'result')
|
347 |
wandb.log_artifact(self.result_artifact, aliases=['latest', 'last', 'epoch ' + str(self.current_epoch),
|
348 |
('best' if best_result else '')])
|
349 |
+
|
350 |
wandb.log({"evaluation": self.result_table})
|
351 |
self.result_table = wandb.Table(["epoch", "id", "ground truth", "prediction", "avg_confidence"])
|
352 |
self.result_artifact = wandb.Artifact("run_" + wandb.run.id + "_progress", "evaluation")
|
val.py
CHANGED
@@ -25,7 +25,52 @@ from utils.general import coco80_to_coco91_class, check_dataset, check_file, che
|
|
25 |
box_iou, non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, set_logging, increment_path, colorstr
|
26 |
from utils.metrics import ap_per_class, ConfusionMatrix
|
27 |
from utils.plots import plot_images, output_to_target, plot_study_txt
|
28 |
-
from utils.torch_utils import select_device,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
|
31 |
@torch.no_grad()
|
@@ -43,7 +88,7 @@ def run(data,
|
|
43 |
save_txt=False, # save results to *.txt
|
44 |
save_hybrid=False, # save label+prediction hybrid results to *.txt
|
45 |
save_conf=False, # save confidences in --save-txt labels
|
46 |
-
save_json=False, # save a
|
47 |
project='runs/val', # save to project/name
|
48 |
name='exp', # save to project/name
|
49 |
exist_ok=False, # existing project/name ok, do not increment
|
@@ -93,10 +138,6 @@ def run(data,
|
|
93 |
iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95
|
94 |
niou = iouv.numel()
|
95 |
|
96 |
-
# Logging
|
97 |
-
log_imgs = 0
|
98 |
-
if wandb_logger and wandb_logger.wandb:
|
99 |
-
log_imgs = min(wandb_logger.log_imgs, 100)
|
100 |
# Dataloader
|
101 |
if not training:
|
102 |
if device.type != 'cpu':
|
@@ -108,24 +149,24 @@ def run(data,
|
|
108 |
seen = 0
|
109 |
confusion_matrix = ConfusionMatrix(nc=nc)
|
110 |
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
|
111 |
-
|
112 |
s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
|
113 |
p, r, f1, mp, mr, map50, map, t0, t1, t2 = 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
|
114 |
loss = torch.zeros(3, device=device)
|
115 |
-
jdict, stats, ap, ap_class
|
116 |
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
|
117 |
-
t_ =
|
118 |
img = img.to(device, non_blocking=True)
|
119 |
img = img.half() if half else img.float() # uint8 to fp16/32
|
120 |
img /= 255.0 # 0 - 255 to 0.0 - 1.0
|
121 |
targets = targets.to(device)
|
122 |
nb, _, height, width = img.shape # batch size, channels, height, width
|
123 |
-
t =
|
124 |
t0 += t - t_
|
125 |
|
126 |
# Run model
|
127 |
out, train_out = model(img, augment=augment) # inference and training outputs
|
128 |
-
t1 +=
|
129 |
|
130 |
# Compute loss
|
131 |
if compute_loss:
|
@@ -134,16 +175,16 @@ def run(data,
|
|
134 |
# Run NMS
|
135 |
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
|
136 |
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
|
137 |
-
t =
|
138 |
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
|
139 |
-
t2 +=
|
140 |
|
141 |
# Statistics per image
|
142 |
for si, pred in enumerate(out):
|
143 |
labels = targets[targets[:, 0] == si, 1:]
|
144 |
nl = len(labels)
|
145 |
tcls = labels[:, 0].tolist() if nl else [] # target class
|
146 |
-
path = Path(paths[si])
|
147 |
seen += 1
|
148 |
|
149 |
if len(pred) == 0:
|
@@ -155,76 +196,27 @@ def run(data,
|
|
155 |
if single_cls:
|
156 |
pred[:, 5] = 0
|
157 |
predn = pred.clone()
|
158 |
-
scale_coords(img[si].shape[1:], predn[:, :4],
|
159 |
|
160 |
-
#
|
161 |
-
if save_txt:
|
162 |
-
gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]] # normalization gain whwh
|
163 |
-
for *xyxy, conf, cls in predn.tolist():
|
164 |
-
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
|
165 |
-
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
|
166 |
-
with open(save_dir / 'labels' / (path.stem + '.txt'), 'a') as f:
|
167 |
-
f.write(('%g ' * len(line)).rstrip() % line + '\n')
|
168 |
-
|
169 |
-
# W&B logging - Media Panel plots
|
170 |
-
if len(wandb_images) < log_imgs and wandb_logger.current_epoch > 0: # Check for test operation
|
171 |
-
if wandb_logger.current_epoch % wandb_logger.bbox_interval == 0:
|
172 |
-
box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
|
173 |
-
"class_id": int(cls),
|
174 |
-
"box_caption": "%s %.3f" % (names[cls], conf),
|
175 |
-
"scores": {"class_score": conf},
|
176 |
-
"domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
|
177 |
-
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
|
178 |
-
wandb_images.append(wandb_logger.wandb.Image(img[si], boxes=boxes, caption=path.name))
|
179 |
-
wandb_logger.log_training_progress(predn, path, names) if wandb_logger and wandb_logger.wandb_run else None
|
180 |
-
|
181 |
-
# Append to pycocotools JSON dictionary
|
182 |
-
if save_json:
|
183 |
-
# [{"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}, ...
|
184 |
-
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
|
185 |
-
box = xyxy2xywh(predn[:, :4]) # xywh
|
186 |
-
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
187 |
-
for p, b in zip(pred.tolist(), box.tolist()):
|
188 |
-
jdict.append({'image_id': image_id,
|
189 |
-
'category_id': coco91class[int(p[5])] if is_coco else int(p[5]),
|
190 |
-
'bbox': [round(x, 3) for x in b],
|
191 |
-
'score': round(p[4], 5)})
|
192 |
-
|
193 |
-
# Assign all predictions as incorrect
|
194 |
-
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device)
|
195 |
if nl:
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
tbox = xywh2xyxy(labels[:, 1:5])
|
201 |
-
scale_coords(img[si].shape[1:], tbox, shapes[si][0], shapes[si][1]) # native-space labels
|
202 |
if plots:
|
203 |
-
confusion_matrix.process_batch(predn,
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
# Append detections
|
216 |
-
detected_set = set()
|
217 |
-
for j in (ious > iouv[0]).nonzero(as_tuple=False):
|
218 |
-
d = ti[i[j]] # detected target
|
219 |
-
if d.item() not in detected_set:
|
220 |
-
detected_set.add(d.item())
|
221 |
-
detected.append(d)
|
222 |
-
correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn
|
223 |
-
if len(detected) == nl: # all targets already located in image
|
224 |
-
break
|
225 |
-
|
226 |
-
# Append statistics (correct, conf, pcls, tcls)
|
227 |
-
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
|
228 |
|
229 |
# Plot images
|
230 |
if plots and batch_i < 3:
|
@@ -264,15 +256,13 @@ def run(data,
|
|
264 |
if wandb_logger and wandb_logger.wandb:
|
265 |
val_batches = [wandb_logger.wandb.Image(str(f), caption=f.name) for f in sorted(save_dir.glob('val*.jpg'))]
|
266 |
wandb_logger.log({"Validation": val_batches})
|
267 |
-
if wandb_images:
|
268 |
-
wandb_logger.log({"Bounding Box Debugger/Images": wandb_images})
|
269 |
|
270 |
# Save JSON
|
271 |
if save_json and len(jdict):
|
272 |
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
|
273 |
anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
|
274 |
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
|
275 |
-
print('\nEvaluating pycocotools mAP... saving
|
276 |
with open(pred_json, 'w') as f:
|
277 |
json.dump(jdict, f)
|
278 |
|
@@ -320,7 +310,7 @@ def parse_opt():
|
|
320 |
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
|
321 |
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
|
322 |
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
|
323 |
-
parser.add_argument('--save-json', action='store_true', help='save a
|
324 |
parser.add_argument('--project', default='runs/val', help='save to project/name')
|
325 |
parser.add_argument('--name', default='exp', help='save to project/name')
|
326 |
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
|
|
|
25 |
box_iou, non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, set_logging, increment_path, colorstr
|
26 |
from utils.metrics import ap_per_class, ConfusionMatrix
|
27 |
from utils.plots import plot_images, output_to_target, plot_study_txt
|
28 |
+
from utils.torch_utils import select_device, time_sync
|
29 |
+
|
30 |
+
|
31 |
+
def save_one_txt(predn, save_conf, shape, file):
|
32 |
+
# Save one txt result
|
33 |
+
gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
|
34 |
+
for *xyxy, conf, cls in predn.tolist():
|
35 |
+
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
|
36 |
+
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
|
37 |
+
with open(file, 'a') as f:
|
38 |
+
f.write(('%g ' * len(line)).rstrip() % line + '\n')
|
39 |
+
|
40 |
+
|
41 |
+
def save_one_json(predn, jdict, path, class_map):
|
42 |
+
# Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
|
43 |
+
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
|
44 |
+
box = xyxy2xywh(predn[:, :4]) # xywh
|
45 |
+
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
46 |
+
for p, b in zip(predn.tolist(), box.tolist()):
|
47 |
+
jdict.append({'image_id': image_id,
|
48 |
+
'category_id': class_map[int(p[5])],
|
49 |
+
'bbox': [round(x, 3) for x in b],
|
50 |
+
'score': round(p[4], 5)})
|
51 |
+
|
52 |
+
|
53 |
+
def process_batch(predictions, labels, iouv):
|
54 |
+
# Evaluate 1 batch of predictions
|
55 |
+
correct = torch.zeros(predictions.shape[0], len(iouv), dtype=torch.bool, device=iouv.device)
|
56 |
+
detected = [] # label indices
|
57 |
+
tcls, pcls = labels[:, 0], predictions[:, 5]
|
58 |
+
nl = labels.shape[0] # number of labels
|
59 |
+
for cls in torch.unique(tcls):
|
60 |
+
ti = (cls == tcls).nonzero().view(-1) # label indices
|
61 |
+
pi = (cls == pcls).nonzero().view(-1) # prediction indices
|
62 |
+
if pi.shape[0]: # find detections
|
63 |
+
ious, i = box_iou(predictions[pi, 0:4], labels[ti, 1:5]).max(1) # best ious, indices
|
64 |
+
detected_set = set()
|
65 |
+
for j in (ious > iouv[0]).nonzero():
|
66 |
+
d = ti[i[j]] # detected label
|
67 |
+
if d.item() not in detected_set:
|
68 |
+
detected_set.add(d.item())
|
69 |
+
detected.append(d) # append detections
|
70 |
+
correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn
|
71 |
+
if len(detected) == nl: # all labels already located in image
|
72 |
+
break
|
73 |
+
return correct
|
74 |
|
75 |
|
76 |
@torch.no_grad()
|
|
|
88 |
save_txt=False, # save results to *.txt
|
89 |
save_hybrid=False, # save label+prediction hybrid results to *.txt
|
90 |
save_conf=False, # save confidences in --save-txt labels
|
91 |
+
save_json=False, # save a COCO-JSON results file
|
92 |
project='runs/val', # save to project/name
|
93 |
name='exp', # save to project/name
|
94 |
exist_ok=False, # existing project/name ok, do not increment
|
|
|
138 |
iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95
|
139 |
niou = iouv.numel()
|
140 |
|
|
|
|
|
|
|
|
|
141 |
# Dataloader
|
142 |
if not training:
|
143 |
if device.type != 'cpu':
|
|
|
149 |
seen = 0
|
150 |
confusion_matrix = ConfusionMatrix(nc=nc)
|
151 |
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
|
152 |
+
class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
|
153 |
s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
|
154 |
p, r, f1, mp, mr, map50, map, t0, t1, t2 = 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.
|
155 |
loss = torch.zeros(3, device=device)
|
156 |
+
jdict, stats, ap, ap_class = [], [], [], []
|
157 |
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
|
158 |
+
t_ = time_sync()
|
159 |
img = img.to(device, non_blocking=True)
|
160 |
img = img.half() if half else img.float() # uint8 to fp16/32
|
161 |
img /= 255.0 # 0 - 255 to 0.0 - 1.0
|
162 |
targets = targets.to(device)
|
163 |
nb, _, height, width = img.shape # batch size, channels, height, width
|
164 |
+
t = time_sync()
|
165 |
t0 += t - t_
|
166 |
|
167 |
# Run model
|
168 |
out, train_out = model(img, augment=augment) # inference and training outputs
|
169 |
+
t1 += time_sync() - t
|
170 |
|
171 |
# Compute loss
|
172 |
if compute_loss:
|
|
|
175 |
# Run NMS
|
176 |
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
|
177 |
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
|
178 |
+
t = time_sync()
|
179 |
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
|
180 |
+
t2 += time_sync() - t
|
181 |
|
182 |
# Statistics per image
|
183 |
for si, pred in enumerate(out):
|
184 |
labels = targets[targets[:, 0] == si, 1:]
|
185 |
nl = len(labels)
|
186 |
tcls = labels[:, 0].tolist() if nl else [] # target class
|
187 |
+
path, shape = Path(paths[si]), shapes[si][0]
|
188 |
seen += 1
|
189 |
|
190 |
if len(pred) == 0:
|
|
|
196 |
if single_cls:
|
197 |
pred[:, 5] = 0
|
198 |
predn = pred.clone()
|
199 |
+
scale_coords(img[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
|
200 |
|
201 |
+
# Evaluate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
if nl:
|
203 |
+
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
|
204 |
+
scale_coords(img[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
|
205 |
+
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
|
206 |
+
correct = process_batch(predn, labelsn, iouv)
|
|
|
|
|
207 |
if plots:
|
208 |
+
confusion_matrix.process_batch(predn, labelsn)
|
209 |
+
else:
|
210 |
+
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
|
211 |
+
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) # (correct, conf, pcls, tcls)
|
212 |
+
|
213 |
+
# Save/log
|
214 |
+
if save_txt:
|
215 |
+
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
|
216 |
+
if save_json:
|
217 |
+
save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
|
218 |
+
if wandb_logger:
|
219 |
+
wandb_logger.val_one_image(pred, predn, path, names, img[si])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
|
221 |
# Plot images
|
222 |
if plots and batch_i < 3:
|
|
|
256 |
if wandb_logger and wandb_logger.wandb:
|
257 |
val_batches = [wandb_logger.wandb.Image(str(f), caption=f.name) for f in sorted(save_dir.glob('val*.jpg'))]
|
258 |
wandb_logger.log({"Validation": val_batches})
|
|
|
|
|
259 |
|
260 |
# Save JSON
|
261 |
if save_json and len(jdict):
|
262 |
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
|
263 |
anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
|
264 |
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
|
265 |
+
print(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
|
266 |
with open(pred_json, 'w') as f:
|
267 |
json.dump(jdict, f)
|
268 |
|
|
|
310 |
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
|
311 |
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
|
312 |
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
|
313 |
+
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
|
314 |
parser.add_argument('--project', default='runs/val', help='save to project/name')
|
315 |
parser.add_argument('--name', default='exp', help='save to project/name')
|
316 |
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
|