glenn-jocher Ayush Chaurasia commited on
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>

Files changed (8) hide show
  1. detect.py +3 -3
  2. models/common.py +19 -14
  3. models/yolo.py +21 -22
  4. train.py +32 -35
  5. utils/datasets.py +17 -18
  6. utils/torch_utils.py +7 -7
  7. utils/wandb_logging/wandb_utils.py +46 -19
  8. 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, time_synchronized
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 = time_synchronized()
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_synchronized()
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 time_synchronized
 
 
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
- print('AutoShape already enabled, skipping... ') # model already converted to model.autoshape()
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 = [time_synchronized()]
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(time_synchronized())
274
 
275
  with amp.autocast(enabled=p.device.type != 'cpu'):
276
  # Inference
277
  y = self.model(x, augment, profile)[0] # forward
278
- t.append(time_synchronized())
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(time_synchronized())
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
- print(str.rstrip(', '))
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
- print(f"{'Saved' * (i == 0)} {f}", end=',' if i < self.n - 1 else f' to {save_dir}\n')
 
333
  if render:
334
  self.imgs[i] = np.asarray(im)
335
 
336
  def print(self):
337
  self.display(pprint=True) # print results
338
- print(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {tuple(self.s)}' % self.t)
 
339
 
340
  def show(self):
341
  self.display(show=True) # show results
342
 
343
- def save(self, save_dir='runs/hub/exp'):
344
- save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/hub/exp', mkdir=True) # increment save_dir
345
  self.display(save=True, save_dir=save_dir) # save results
346
 
347
- def crop(self, save_dir='runs/hub/exp'):
348
- save_dir = increment_path(save_dir, exist_ok=save_dir != 'runs/hub/exp', mkdir=True) # increment save_dir
349
  self.display(crop=True, save_dir=save_dir) # crop results
350
- print(f'Saved results to {save_dir}\n')
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 time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \
22
  select_device, copy_attr
23
 
24
  try:
@@ -26,7 +25,7 @@ try:
26
  except ImportError:
27
  thop = None
28
 
29
- logger = logging.getLogger(__name__)
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
- logger.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
94
  self.yaml['nc'] = nc # override yaml value
95
  if anchors:
96
- logger.info(f'Overriding model.yaml anchors with anchors={anchors}')
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
- # logger.info([x.shape for x in self.forward(torch.zeros(1, ch, 64, 64))])
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
- # logger.info('Strides: %s' % m.stride.tolist())
114
 
115
  # Init weights, biases
116
  initialize_weights(self)
117
  self.info()
118
- logger.info('')
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 = time_synchronized()
147
  for _ in range(10):
148
  _ = m(x)
149
- dt.append((time_synchronized() - t) * 100)
150
  if m == self.model[0]:
151
- logger.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} {'module'}")
152
- logger.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}')
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
- logger.info('%.1fms total' % sum(dt))
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
- logger.info(
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
- # logger.info('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights
202
 
203
  def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
204
- logger.info('Fusing layers... ')
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
- logger.info('Adding NMS... ')
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
- logger.info('Removing NMS... ')
224
  self.model = self.model[:-1] # remove
225
  return self
226
 
227
  def autoshape(self): # add AutoShape module
228
- logger.info('Adding AutoShape... ')
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
- logger.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments'))
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
- logger.info('%3s%18s%3s%10.0f %-40s%-30s' % (i, f, n, np, t, args)) # print
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
- # logger.info("Run 'tensorboard --logdir=models' to view tensorboard at http://localhost:6006/")
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
- 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,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
- 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,7 +94,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary
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,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
- 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,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
- 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,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
- 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,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
- 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,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, imgsz_val = [check_img_size(x, gs) for x in opt.img_size] # verify imgsz are gs-multiples
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
- logger.info('Using SyncBatchNorm()')
223
 
224
  # Trainloader
225
- dataloader, 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,
228
- image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: '))
229
  mlc = np.concatenate(dataset.labels, 0)[:, 0].max() # max label class
230
- nb = len(dataloader) # 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
- valloader = create_dataloader(val_path, imgsz_val, 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,
238
- pad=0.5, 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
- 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
- logger.info(f'Image sizes {imgsz} train, {imgsz_val} val\n'
281
- f'Using {dataloader.num_workers} dataloader workers\n'
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
- dataloader.sampler.set_epoch(epoch)
308
- pbar = enumerate(dataloader)
309
- logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'labels', 'img_size'))
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=imgsz_val,
393
  model=ema.ema,
394
  single_cls=single_cls,
395
- dataloader=valloader,
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
- logger.info(f'{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.\n')
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=imgsz_val,
461
  model=attempt_load(m, device).half(),
462
  single_cls=single_cls,
463
- dataloader=valloader,
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-size', nargs='+', type=int, default=[640, 640], help='[train, val] image sizes')
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
- logger.info('Resuming training from %s' % ckpt)
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://%s/evolve.txt .' % opt.bucket) # download evolve.txt if exists
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, cutout
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
- 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 img_formats]
168
- videos = [x for x in files if x.split('.')[-1].lower() in vid_formats]
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: {img_formats}\nvideos: {vid_formats}'
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 img_formats])
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 {help_url}')
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 {help_url}'
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(num_threads).imap(lambda x: load_image(*x), zip(repeat(self), range(n)))
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(num_threads) as 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 {help_url}')
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 img_formats:
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 img_formats], []) # image files only
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 img_formats, f'invalid image format {im.format}'
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
- logger = logging.getLogger(__name__)
26
 
27
 
28
  @contextmanager
@@ -85,11 +85,11 @@ def select_device(device='', batch_size=None):
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_synchronized():
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] = time_synchronized()
122
  y = m(x)
123
- t[1] = time_synchronized()
124
  try:
125
  _ = y.sum().backward()
126
- t[2] = time_synchronized()
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
- 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):
 
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, self.data_dict = wandb, None if not wandb else wandb.run, data_dict
 
 
 
 
 
 
 
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 'val_artifact' not in self.__dict__: # If --upload_dataset is set, use the existing artifact, don't download
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
- self.result_artifact, self.result_table, self.val_table, self.weights = None, None, None, None
165
- if self.train_artifact_path is not None:
166
- train_path = Path(self.train_artifact_path) / 'data/images/'
167
- data_dict['train'] = str(train_path)
168
- if self.val_artifact_path is not None:
169
- val_path = Path(self.val_artifact_path) / 'data/images/'
170
- data_dict['val'] = str(val_path)
171
- self.val_table = self.val_artifact.get("val")
172
- self.map_val_table_path()
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.val_table_map = {}
250
  print("Mapping dataset")
251
  for i, data in enumerate(tqdm(self.val_table.data)):
252
- self.val_table_map[data[3]] = data[0]
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.val_table_map[Path(path).name]
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, time_synchronized
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 cocoapi-compatible JSON results file
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
- coco91class = coco80_to_coco91_class()
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, wandb_images = [], [], [], [], []
116
  for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
117
- t_ = time_synchronized()
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 = time_synchronized()
124
  t0 += t - t_
125
 
126
  # Run model
127
  out, train_out = model(img, augment=augment) # inference and training outputs
128
- t1 += time_synchronized() - t
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 = time_synchronized()
138
  out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
139
- t2 += time_synchronized() - t
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], shapes[si][0], shapes[si][1]) # native-space pred
159
 
160
- # Append to text file
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
- detected = [] # target indices
197
- tcls_tensor = labels[:, 0]
198
-
199
- # target boxes
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, torch.cat((labels[:, 0:1], tbox), 1))
204
-
205
- # Per target class
206
- for cls in torch.unique(tcls_tensor):
207
- ti = (cls == tcls_tensor).nonzero(as_tuple=False).view(-1) # target indices
208
- pi = (cls == pred[:, 5]).nonzero(as_tuple=False).view(-1) # prediction indices
209
-
210
- # Search for detections
211
- if pi.shape[0]:
212
- # Prediction to target ious
213
- ious, i = box_iou(predn[pi, :4], tbox[ti]).max(1) # best ious, indices
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 %s...' % pred_json)
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 cocoapi-compatible JSON results file')
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')